Twofactor authentication and strong password
This commit is contained in:
@@ -0,0 +1,741 @@
|
|||||||
|
# DBOIS Tweefactorauthenticatie - Beheerdershandleiding
|
||||||
|
|
||||||
|
## Inhoudsopgave
|
||||||
|
|
||||||
|
1. [Introductie](#introductie)
|
||||||
|
2. [Installatie en Configuratie](#installatie-en-configuratie)
|
||||||
|
3. [Gebruikers Helpen met 2FA Setup](#gebruikers-helpen-met-2fa-setup)
|
||||||
|
4. [Veelvoorkomende Problemen Oplossen](#veelvoorkomende-problemen-oplossen)
|
||||||
|
5. [Gebruikers Deblokkeren](#gebruikers-deblokkeren)
|
||||||
|
6. [Best Practices](#best-practices)
|
||||||
|
7. [Veelgestelde Vragen](#veelgestelde-vragen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Introductie
|
||||||
|
|
||||||
|
### Wat is Tweefactorauthenticatie?
|
||||||
|
|
||||||
|
Tweefactorauthenticatie (2FA) is een extra beveiligingslaag die vereist dat gebruikers twee vormen van identificatie opgeven bij het inloggen:
|
||||||
|
1. **Iets dat ze weten**: hun wachtwoord
|
||||||
|
2. **Iets dat ze hebben**: hun telefoon of e-mailaccount
|
||||||
|
|
||||||
|
### Waarom 2FA Gebruiken?
|
||||||
|
|
||||||
|
- **Bescherming tegen gestolen wachtwoorden**: Zelfs als iemands wachtwoord wordt gestolen, kan een aanvaller niet inloggen zonder toegang tot de tweede factor
|
||||||
|
- **Compliance**: Voldoet aan beveiligingsrichtlijnen en compliance-vereisten
|
||||||
|
- **Vertrouwen**: Gebruikers weten dat hun accounts beter beschermd zijn
|
||||||
|
|
||||||
|
### Beschikbare 2FA Methodes
|
||||||
|
|
||||||
|
DBOIS ondersteunt twee 2FA-methodes:
|
||||||
|
|
||||||
|
1. **E-mail OTP (One-Time Password)**
|
||||||
|
- Gebruiker ontvangt een 6-cijferige code per e-mail
|
||||||
|
- Code is 10 minuten geldig
|
||||||
|
- Eenvoudig te gebruiken, geen extra app nodig
|
||||||
|
- Geschikt voor gebruikers die niet technisch onderlegd zijn
|
||||||
|
|
||||||
|
2. **Google Authenticator**
|
||||||
|
- Gebruiker installeert een authenticator app op hun smartphone
|
||||||
|
- App genereert elke 30 seconden een nieuwe 6-cijferige code
|
||||||
|
- Werkt zonder internetverbinding
|
||||||
|
- Veiliger dan e-mail (geen afhankelijkheid van e-mailbeveiliging)
|
||||||
|
- Apps: Google Authenticator, Microsoft Authenticator, Authy, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installatie en Configuratie
|
||||||
|
|
||||||
|
### Stap 1: Database Migratie Uitvoeren
|
||||||
|
|
||||||
|
**BELANGRIJK**: Voer eerst een backup uit van uw database voordat u de migratie uitvoert!
|
||||||
|
|
||||||
|
#### Optie A: Via Command Line
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigeer naar de DBOIS directory
|
||||||
|
cd /pad/naar/dbois
|
||||||
|
|
||||||
|
# Voer de migratie uit
|
||||||
|
mysql -u [gebruikersnaam] -p [database_naam] < application/migrations/001_add_two_factor_authentication.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Vervang:
|
||||||
|
- `[gebruikersnaam]` met uw MySQL gebruikersnaam
|
||||||
|
- `[database_naam]` met de naam van uw DBOIS database
|
||||||
|
|
||||||
|
#### Optie B: Via phpMyAdmin
|
||||||
|
|
||||||
|
1. Log in op phpMyAdmin
|
||||||
|
2. Selecteer de DBOIS database
|
||||||
|
3. Klik op de "SQL" tab
|
||||||
|
4. Open het bestand `application/migrations/001_add_two_factor_authentication.sql` in een teksteditor
|
||||||
|
5. Kopieer de gehele inhoud
|
||||||
|
6. Plak in het SQL venster van phpMyAdmin
|
||||||
|
7. Klik op "Go" of "Uitvoeren"
|
||||||
|
|
||||||
|
#### Verificatie
|
||||||
|
|
||||||
|
Controleer of de migratie succesvol was:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Controleer nieuwe kolommen in users tabel
|
||||||
|
DESCRIBE users;
|
||||||
|
|
||||||
|
-- U zou deze kolommen moeten zien:
|
||||||
|
-- two_factor_enabled
|
||||||
|
-- two_factor_method
|
||||||
|
-- two_factor_secret
|
||||||
|
|
||||||
|
-- Controleer nieuwe tabel
|
||||||
|
SHOW TABLES LIKE 'user_otp_codes';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 2: E-mail Configuratie Controleren
|
||||||
|
|
||||||
|
Zorg ervoor dat de e-mailconfiguratie correct is ingesteld in `application/config/email.php`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$config['protocol'] = 'smtp';
|
||||||
|
$config['smtp_host'] = 'mail.uwdomain.nl';
|
||||||
|
$config['smtp_user'] = 'noreply@dbois.nl';
|
||||||
|
$config['smtp_pass'] = 'uw_wachtwoord';
|
||||||
|
$config['smtp_port'] = 587;
|
||||||
|
$config['smtp_crypto'] = 'tls';
|
||||||
|
$config['mailtype'] = 'html';
|
||||||
|
$config['charset'] = 'utf-8';
|
||||||
|
$config['newline'] = "\r\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test de e-mailconfiguratie** door de wachtwoord-vergeten functie te gebruiken.
|
||||||
|
|
||||||
|
### Stap 3: Link naar 2FA Instellingen Toevoegen
|
||||||
|
|
||||||
|
Voeg een link toe in het gebruikersmenu zodat gebruikers hun 2FA kunnen beheren:
|
||||||
|
|
||||||
|
Voorbeeld locatie: In uw hoofdnavigatie of gebruikersprofiel menu:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<li>
|
||||||
|
<a href="/beheer/TwoFactorController">
|
||||||
|
<i class="icon wb-lock"></i>
|
||||||
|
<span>2FA Instellingen</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
```
|
||||||
|
|
||||||
|
Of in een dropdown menu:
|
||||||
|
```html
|
||||||
|
<a class="dropdown-item" href="/beheer/TwoFactorController">
|
||||||
|
<i class="icon wb-lock"></i> Tweefactorauthenticatie
|
||||||
|
</a>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stap 4: Test de Implementatie
|
||||||
|
|
||||||
|
1. Log in als testgebruiker
|
||||||
|
2. Ga naar `/beheer/TwoFactorController`
|
||||||
|
3. Schakel 2FA in met e-mail methode
|
||||||
|
4. Log uit en log opnieuw in
|
||||||
|
5. Verifieer dat u de OTP code ontvangt en kunt invoeren
|
||||||
|
6. Test ook de Google Authenticator methode
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gebruikers Helpen met 2FA Setup
|
||||||
|
|
||||||
|
### E-mail OTP Methode Instellen
|
||||||
|
|
||||||
|
**Stap 1**: Gebruiker gaat naar 2FA Instellingen
|
||||||
|
- URL: `/beheer/TwoFactorController`
|
||||||
|
- Of via menu: Gebruikersprofiel → 2FA Instellingen
|
||||||
|
|
||||||
|
**Stap 2**: E-mail methode selecteren
|
||||||
|
- Klik op het "E-mail" kaartje
|
||||||
|
- Klik op "2FA Inschakelen"
|
||||||
|
|
||||||
|
**Stap 3**: Bevestiging
|
||||||
|
- Gebruiker ziet melding: "Tweefactorauthenticatie ingeschakeld"
|
||||||
|
- Pagina toont huidige status: E-mail methode actief
|
||||||
|
|
||||||
|
**Stap 4**: Testen
|
||||||
|
- Log uit
|
||||||
|
- Log opnieuw in met gebruikersnaam en wachtwoord
|
||||||
|
- Controleer e-mail inbox (en spam folder!)
|
||||||
|
- Voer 6-cijferige code in
|
||||||
|
- Klik "Verifiëren"
|
||||||
|
|
||||||
|
**Tips voor gebruikers**:
|
||||||
|
- Bewaar de DBOIS e-mail adres (noreply@dbois.nl) in contacten om te voorkomen dat mails in spam terechtkomen
|
||||||
|
- Codes zijn 10 minuten geldig
|
||||||
|
- Nieuwe code kan worden aangevraagd met "Code opnieuw verzenden"
|
||||||
|
|
||||||
|
### Google Authenticator Methode Instellen
|
||||||
|
|
||||||
|
**Stap 1**: Authenticator App Installeren
|
||||||
|
- iOS: Download "Google Authenticator" of "Microsoft Authenticator" uit App Store
|
||||||
|
- Android: Download "Google Authenticator" of "Microsoft Authenticator" uit Play Store
|
||||||
|
- Alternatief: Authy (multi-device ondersteuning)
|
||||||
|
|
||||||
|
**Stap 2**: 2FA Instellingen Openen
|
||||||
|
- Ga naar `/beheer/TwoFactorController`
|
||||||
|
|
||||||
|
**Stap 3**: Google Authenticator Selecteren
|
||||||
|
- Klik op het "Google Authenticator" kaartje
|
||||||
|
- Klik op "2FA Inschakelen"
|
||||||
|
|
||||||
|
**Stap 4**: QR Code Scannen
|
||||||
|
- Een QR code verschijnt op het scherm
|
||||||
|
- Open de authenticator app op de telefoon
|
||||||
|
- Klik op "+" of "Account toevoegen"
|
||||||
|
- Selecteer "QR code scannen"
|
||||||
|
- Scan de QR code op het scherm
|
||||||
|
- DBOIS verschijnt nu in de app met een 6-cijferige code
|
||||||
|
|
||||||
|
**Stap 5**: Verificatie
|
||||||
|
- De 6-cijferige code in de app verandert elke 30 seconden
|
||||||
|
- Voer de huidige code in op het verificatiescherm
|
||||||
|
- Klik "Verifiëren"
|
||||||
|
- Bij succes: 2FA is nu actief
|
||||||
|
|
||||||
|
**Stap 6**: Testen
|
||||||
|
- Log uit
|
||||||
|
- Log opnieuw in
|
||||||
|
- Open authenticator app
|
||||||
|
- Voer de 6-cijferige code in (let op: code verandert elke 30 seconden!)
|
||||||
|
- Klik "Verifiëren"
|
||||||
|
|
||||||
|
**Belangrijk voor gebruikers**:
|
||||||
|
- ⚠️ **Bewaar de QR code of secret code veilig** (screenshot maken en veilig opslaan)
|
||||||
|
- Als telefoon verloren gaat, kan de QR code gebruikt worden om opnieuw in te stellen
|
||||||
|
- Codes zijn 30 seconden geldig
|
||||||
|
- Zorg dat de tijd op de telefoon correct is ingesteld (belangrijk voor TOTP!)
|
||||||
|
|
||||||
|
### Methode Wijzigen
|
||||||
|
|
||||||
|
Gebruikers kunnen later van methode wisselen:
|
||||||
|
|
||||||
|
1. Ga naar 2FA Instellingen
|
||||||
|
2. Selecteer een andere methode (E-mail of Google Authenticator)
|
||||||
|
3. Klik "Methode Wijzigen"
|
||||||
|
4. Volg de setup stappen voor de nieuwe methode
|
||||||
|
|
||||||
|
### 2FA Uitschakelen
|
||||||
|
|
||||||
|
Als een gebruiker 2FA wil uitschakelen:
|
||||||
|
|
||||||
|
1. Ga naar 2FA Instellingen
|
||||||
|
2. Scroll naar beneden
|
||||||
|
3. Klik op rode knop "2FA Uitschakelen"
|
||||||
|
4. Bevestig de actie
|
||||||
|
|
||||||
|
**Let op**: Vanuit beveiligingsperspectief raden we aan om 2FA verplicht te stellen voor beheerders en gebruikers met gevoelige toegang.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Veelvoorkomende Problemen Oplossen
|
||||||
|
|
||||||
|
### Probleem 1: E-mail OTP Niet Ontvangen
|
||||||
|
|
||||||
|
**Diagnose checklist**:
|
||||||
|
- [ ] Is de e-mail configuratie correct? (test met wachtwoord-vergeten functie)
|
||||||
|
- [ ] Controleer spam/junk folder
|
||||||
|
- [ ] Is het e-mailadres van de gebruiker correct in de database?
|
||||||
|
- [ ] Controleer applicatie logs voor e-mail fouten
|
||||||
|
|
||||||
|
**Oplossing A: E-mail Configuratie Controleren**
|
||||||
|
|
||||||
|
1. Test de e-mail functionaliteit:
|
||||||
|
```php
|
||||||
|
// In een test controller
|
||||||
|
$this->load->library('email');
|
||||||
|
$this->email->from('noreply@dbois.nl', 'DBOIS');
|
||||||
|
$this->email->to('uw@email.nl');
|
||||||
|
$this->email->subject('Test');
|
||||||
|
$this->email->message('Test bericht');
|
||||||
|
if (!$this->email->send()) {
|
||||||
|
echo $this->email->print_debugger();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Controleer SMTP instellingen in `application/config/email.php`
|
||||||
|
3. Controleer firewall/server configuratie voor uitgaande SMTP
|
||||||
|
|
||||||
|
**Oplossing B: Code Opnieuw Verzenden**
|
||||||
|
|
||||||
|
1. Gebruiker klikt op "Code opnieuw verzenden" op de verificatiepagina
|
||||||
|
2. Check of nieuwe code wordt verzonden
|
||||||
|
3. Controleer spam folder
|
||||||
|
|
||||||
|
**Oplossing C: Tijdelijk Uitschakelen**
|
||||||
|
|
||||||
|
Als beheerder kunt u 2FA tijdelijk uitschakelen (zie [Gebruikers Deblokkeren](#gebruikers-deblokkeren))
|
||||||
|
|
||||||
|
### Probleem 2: Authenticator Code Werkt Niet
|
||||||
|
|
||||||
|
**Meest voorkomende oorzaken**:
|
||||||
|
|
||||||
|
**A. Tijd Niet Gesynchroniseerd**
|
||||||
|
- Authenticator codes zijn tijd-gebaseerd
|
||||||
|
- Als de klok van de telefoon niet correct is, werken codes niet
|
||||||
|
|
||||||
|
**Oplossing**:
|
||||||
|
1. Ga naar telefooninstellingen
|
||||||
|
2. Schakel "Automatische datum en tijd" in
|
||||||
|
3. Of handmatig de juiste tijd instellen
|
||||||
|
4. **Voor Android**: In Google Authenticator app → Menu → Instellingen → Tijd correctie voor codes → Synchroniseer nu
|
||||||
|
5. Probeer opnieuw in te loggen
|
||||||
|
|
||||||
|
**B. Verkeerde QR Code Gescand**
|
||||||
|
- Mogelijk oude QR code gebruikt
|
||||||
|
- QR code van andere account gescand
|
||||||
|
|
||||||
|
**Oplossing**:
|
||||||
|
1. Verwijder het DBOIS account uit de authenticator app
|
||||||
|
2. Log in op DBOIS (beheerder schakelt 2FA tijdelijk uit)
|
||||||
|
3. Ga naar 2FA instellingen
|
||||||
|
4. Schakel 2FA opnieuw in
|
||||||
|
5. Scan de nieuwe QR code
|
||||||
|
|
||||||
|
**C. Te Lang Gewacht met Code Invoeren**
|
||||||
|
- Codes zijn 30 seconden geldig
|
||||||
|
- Als code bijna verlopen is bij invoeren, kan validatie falen
|
||||||
|
|
||||||
|
**Oplossing**:
|
||||||
|
- Wacht op nieuwe code (elke 30 seconden)
|
||||||
|
- Voer code direct in zodra deze verschijnt
|
||||||
|
|
||||||
|
### Probleem 3: Gebruiker Zit Vast op Verificatiepagina
|
||||||
|
|
||||||
|
**Symptoom**: Gebruiker blijft hangen op `/login/showVerify2fa`
|
||||||
|
|
||||||
|
**Diagnose**:
|
||||||
|
- Session data mogelijk corrupt
|
||||||
|
- Browser cookies geblokkeerd
|
||||||
|
|
||||||
|
**Oplossing A: Via Browser**
|
||||||
|
- Klik op "Annuleren" link onderaan de verificatiepagina
|
||||||
|
- Dit redirect terug naar login pagina
|
||||||
|
|
||||||
|
**Oplossing B: Via Database** (als beheerder)
|
||||||
|
```sql
|
||||||
|
-- Verwijder actieve sessies voor deze gebruiker
|
||||||
|
DELETE FROM ci_sessions
|
||||||
|
WHERE data LIKE '%pending_2fa_user_id%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oplossing C: Browser Cache Wissen**
|
||||||
|
1. Wis browser cookies en cache
|
||||||
|
2. Sluit browser volledig
|
||||||
|
3. Open opnieuw en probeer in te loggen
|
||||||
|
|
||||||
|
### Probleem 4: QR Code Wordt Niet Weergegeven
|
||||||
|
|
||||||
|
**Oorzaken**:
|
||||||
|
- JavaScript errors
|
||||||
|
- Browser compatibiliteit
|
||||||
|
- Netwerk problemen
|
||||||
|
|
||||||
|
**Oplossing**:
|
||||||
|
1. Check browser console voor errors (F12 → Console tab)
|
||||||
|
2. Probeer andere browser (Chrome, Firefox, Safari)
|
||||||
|
3. Controleer dat JavaScript enabled is
|
||||||
|
4. Refresh de pagina (Ctrl+F5 of Cmd+Shift+R)
|
||||||
|
|
||||||
|
**Alternatief: Handmatige Code Invoer**
|
||||||
|
- Onder de QR code staat een tekstuele secret code
|
||||||
|
- Deze kan handmatig worden ingevoerd in de authenticator app:
|
||||||
|
1. Open authenticator app
|
||||||
|
2. Klik "+" → "Enter a setup key"
|
||||||
|
3. Voer account naam in: DBOIS
|
||||||
|
4. Voer de secret code in
|
||||||
|
5. Selecteer "Time based"
|
||||||
|
6. Opslaan
|
||||||
|
|
||||||
|
### Probleem 5: "Ongeldige Verificatiecode" Foutmelding
|
||||||
|
|
||||||
|
**Mogelijke oorzaken en oplossingen**:
|
||||||
|
|
||||||
|
**A. Code Verlopen**
|
||||||
|
- E-mail codes: 10 minuten geldig
|
||||||
|
- Authenticator codes: 30 seconden geldig
|
||||||
|
|
||||||
|
**Oplossing**: Gebruik een nieuwe/actuele code
|
||||||
|
|
||||||
|
**B. Typfout**
|
||||||
|
- Code incorrect overgetypt
|
||||||
|
- Letters gebruikt in plaats van cijfers (O vs 0)
|
||||||
|
|
||||||
|
**Oplossing**: Zorgvuldig code opnieuw invoeren, gebruik copy-paste voor e-mail codes
|
||||||
|
|
||||||
|
**C. Meerdere Pogingen**
|
||||||
|
- Code al gebruikt
|
||||||
|
- Multiple browser tabs/windows
|
||||||
|
|
||||||
|
**Oplossing**:
|
||||||
|
- Voor e-mail: vraag nieuwe code aan
|
||||||
|
- Voor authenticator: wacht op nieuwe code
|
||||||
|
|
||||||
|
**D. Database Problemen**
|
||||||
|
- OTP record niet gevonden
|
||||||
|
- Session data verloren
|
||||||
|
|
||||||
|
**Oplossing**: Beheerder reset 2FA (zie volgende sectie)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gebruikers Deblokkeren
|
||||||
|
|
||||||
|
### Scenario 1: Gebruiker Heeft Toegang Tot Telefoon/E-mail Verloren
|
||||||
|
|
||||||
|
**Situatie**: Gebruiker kan niet inloggen omdat:
|
||||||
|
- Telefoon verloren/gestolen/kapot
|
||||||
|
- Geen toegang tot e-mail
|
||||||
|
- Authenticator app verwijderd
|
||||||
|
|
||||||
|
**Oplossing: 2FA Handmatig Uitschakelen**
|
||||||
|
|
||||||
|
#### Methode A: Via Database (Meest Direct)
|
||||||
|
|
||||||
|
1. Log in op MySQL/phpMyAdmin
|
||||||
|
2. Voer deze query uit:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Zoek de gebruiker
|
||||||
|
SELECT id, username, email, two_factor_enabled, two_factor_method
|
||||||
|
FROM users
|
||||||
|
WHERE username = 'gebruikersnaam';
|
||||||
|
|
||||||
|
-- Schakel 2FA uit voor deze gebruiker
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
two_factor_enabled = 'F',
|
||||||
|
two_factor_method = NULL,
|
||||||
|
two_factor_secret = NULL
|
||||||
|
WHERE id = [user_id];
|
||||||
|
|
||||||
|
-- Verwijder eventuele OTP codes
|
||||||
|
DELETE FROM user_otp_codes WHERE user_id = [user_id];
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Gebruiker kan nu inloggen met alleen wachtwoord
|
||||||
|
4. Adviseer gebruiker om 2FA opnieuw in te stellen
|
||||||
|
|
||||||
|
#### Methode B: Via PHP Script
|
||||||
|
|
||||||
|
Maak een tijdelijk PHP bestand `disable_2fa.php` in de root:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
// BELANGRIJK: Verwijder dit bestand na gebruik!
|
||||||
|
// Alleen voor noodgevallen
|
||||||
|
|
||||||
|
require_once 'system/core/CodeIgniter.php';
|
||||||
|
|
||||||
|
$username = 'gebruikersnaam'; // Wijzig dit
|
||||||
|
|
||||||
|
$CI =& get_instance();
|
||||||
|
$CI->load->database();
|
||||||
|
$CI->load->model('beheer/User_model');
|
||||||
|
|
||||||
|
// Haal gebruiker op
|
||||||
|
$user = $CI->User_model->getUserByUsername($username);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
echo "Gebruiker niet gevonden";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
$data = [
|
||||||
|
'two_factor_enabled' => 'F',
|
||||||
|
'two_factor_method' => NULL,
|
||||||
|
'two_factor_secret' => NULL
|
||||||
|
];
|
||||||
|
|
||||||
|
$CI->User_model->update_record($user->id, $data);
|
||||||
|
|
||||||
|
echo "2FA uitgeschakeld voor gebruiker: " . $username;
|
||||||
|
echo "\n<br>Gebruiker kan nu inloggen met alleen wachtwoord";
|
||||||
|
echo "\n<br><strong>VERWIJDER DIT BESTAND!</strong>";
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
Gebruik:
|
||||||
|
1. Upload `disable_2fa.php` naar DBOIS root directory
|
||||||
|
2. Ga naar `https://uwdomain.nl/dbois/disable_2fa.php`
|
||||||
|
3. **BELANGRIJK: Verwijder het bestand direct na gebruik!**
|
||||||
|
|
||||||
|
### Scenario 2: Massa-Reset (Meerdere Gebruikers)
|
||||||
|
|
||||||
|
**Situatie**: Na een incident moeten meerdere gebruikers gereset worden.
|
||||||
|
|
||||||
|
**Oplossing: Bulk Database Update**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Schakel 2FA uit voor alle gebruikers
|
||||||
|
UPDATE users
|
||||||
|
SET
|
||||||
|
two_factor_enabled = 'F',
|
||||||
|
two_factor_method = NULL,
|
||||||
|
two_factor_secret = NULL;
|
||||||
|
|
||||||
|
-- Verwijder alle OTP codes
|
||||||
|
TRUNCATE TABLE user_otp_codes;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Let op**: Alleen in noodgevallen gebruiken! Vraag gebruikers daarna 2FA opnieuw in te stellen.
|
||||||
|
|
||||||
|
### Scenario 3: Gebruiker Kan Geen Nieuwe Codes Ontvangen
|
||||||
|
|
||||||
|
**E-mail Methode**: Oude codes nog steeds verzonden, nieuwe codes komen niet aan
|
||||||
|
|
||||||
|
**Diagnose**:
|
||||||
|
```sql
|
||||||
|
-- Controleer actieve OTP codes voor gebruiker
|
||||||
|
SELECT * FROM user_otp_codes
|
||||||
|
WHERE user_id = [user_id]
|
||||||
|
AND used = 'F'
|
||||||
|
AND expires_at > NOW()
|
||||||
|
ORDER BY created_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Oplossing: OTP Codes Wissen**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Verwijder alle OTP codes voor deze gebruiker
|
||||||
|
DELETE FROM user_otp_codes WHERE user_id = [user_id];
|
||||||
|
```
|
||||||
|
|
||||||
|
Nu kan gebruiker nieuwe code aanvragen.
|
||||||
|
|
||||||
|
### Best Practices voor Deblokkering
|
||||||
|
|
||||||
|
1. **Identificatie Verifiëren**: Zorg dat u de identiteit van de gebruiker verifieert voordat u 2FA uitschakelt
|
||||||
|
- Telefonisch contact met bekend nummer
|
||||||
|
- In persoon verificatie
|
||||||
|
- Via manager/supervisor
|
||||||
|
|
||||||
|
2. **Documenteer de Actie**: Houd bij wanneer en waarom 2FA werd uitgeschakeld
|
||||||
|
```
|
||||||
|
[Datum] - 2FA uitgeschakeld voor [gebruiker] - Reden: [telefoon verloren]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Tijdelijke Uitschakeling**: Geef gebruiker beperkte tijd om 2FA opnieuw in te stellen
|
||||||
|
- Mail de gebruiker met instructies
|
||||||
|
- Stel deadline (bijv. 24 uur)
|
||||||
|
|
||||||
|
4. **Wachtwoord Reset Overwegen**: Als veiligheidsrisico groot is, forceer ook wachtwoord wijziging
|
||||||
|
```sql
|
||||||
|
-- Dit forceert wachtwoord reset bij volgende login (indien geïmplementeerd)
|
||||||
|
UPDATE users SET password_reset_required = 'T' WHERE id = [user_id];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Voor Beheerders
|
||||||
|
|
||||||
|
#### 1. Verplicht 2FA voor Gevoelige Accounts
|
||||||
|
|
||||||
|
Overweeg 2FA verplicht te stellen voor:
|
||||||
|
- Alle beheerders
|
||||||
|
- Gebruikers met financiële toegang
|
||||||
|
- Gebruikers met toegang tot persoonlijke data
|
||||||
|
|
||||||
|
Implementatie:
|
||||||
|
```php
|
||||||
|
// In MY_Controller of inlog verificatie
|
||||||
|
if ($user->role <= 2 && $user->two_factor_enabled !== 'T') {
|
||||||
|
// Redirect naar 2FA setup met melding
|
||||||
|
redirect('/beheer/TwoFactorController?required=true');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Regelmatig OTP Codes Opschonen
|
||||||
|
|
||||||
|
Voeg een cron job of scheduled task toe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Dagelijks om 03:00
|
||||||
|
0 3 * * * curl -s https://uwdomain.nl/dbois/maintenance/cleanup2fa
|
||||||
|
```
|
||||||
|
|
||||||
|
PHP endpoint (`maintenance/cleanup2fa`):
|
||||||
|
```php
|
||||||
|
public function cleanup2fa() {
|
||||||
|
$this->load->model('beheer/UserOtp_model');
|
||||||
|
$deleted = $this->UserOtp_model->cleanupExpiredOTPs();
|
||||||
|
log_message('info', "2FA Cleanup: {$deleted} expired OTP codes deleted");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Monitor 2FA Adoptie
|
||||||
|
|
||||||
|
Track hoeveel gebruikers 2FA hebben ingeschakeld:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 2FA statistieken
|
||||||
|
SELECT
|
||||||
|
COUNT(*) as total_users,
|
||||||
|
SUM(CASE WHEN two_factor_enabled = 'T' THEN 1 ELSE 0 END) as users_with_2fa,
|
||||||
|
SUM(CASE WHEN two_factor_method = 'email' THEN 1 ELSE 0 END) as email_method,
|
||||||
|
SUM(CASE WHEN two_factor_method = 'authenticator' THEN 1 ELSE 0 END) as authenticator_method,
|
||||||
|
ROUND(SUM(CASE WHEN two_factor_enabled = 'T' THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as adoption_percentage
|
||||||
|
FROM users
|
||||||
|
WHERE archive = 'F';
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Backup Strategie
|
||||||
|
|
||||||
|
Voor Google Authenticator gebruikers:
|
||||||
|
- Adviseer gebruikers om QR code screenshot veilig op te slaan
|
||||||
|
- Of secret code op veilige plek te bewaren
|
||||||
|
- Overweeg gebruik van Authy (cloud backup functionaliteit)
|
||||||
|
|
||||||
|
#### 5. Communicatie
|
||||||
|
|
||||||
|
**Bij Uitrol**:
|
||||||
|
- Stuur e-mail aankondiging 1 week van tevoren
|
||||||
|
- Organiseer training sessies of demo's
|
||||||
|
- Maak instructie video's beschikbaar
|
||||||
|
- Bied helpdesk ondersteuning
|
||||||
|
|
||||||
|
**Ongoing**:
|
||||||
|
- Voeg 2FA info toe aan onboarding proces
|
||||||
|
- Herinner gebruikers zonder 2FA periodiek
|
||||||
|
- Share security awareness tips
|
||||||
|
|
||||||
|
### Voor Eindgebruikers
|
||||||
|
|
||||||
|
#### Do's
|
||||||
|
✅ Schakel 2FA in voor extra beveiliging
|
||||||
|
✅ Gebruik Google Authenticator voor maximale veiligheid
|
||||||
|
✅ Bewaar QR code of secret code veilig
|
||||||
|
✅ Test 2FA setup direct na instellen
|
||||||
|
✅ Update telefoonnummer en e-mail adres in profiel
|
||||||
|
✅ Gebruik een app met backup (zoals Authy)
|
||||||
|
|
||||||
|
#### Don'ts
|
||||||
|
❌ Deel nooit uw 2FA codes
|
||||||
|
❌ Screenshot QR codes niet in onbeveiligde cloud
|
||||||
|
❌ Gebruik geen publieke computers voor 2FA setup
|
||||||
|
❌ Negeer niet 2FA waarschuwingen
|
||||||
|
❌ Gebruik niet hetzelfde apparaat voor wachtwoord manager én 2FA (voor maximale veiligheid)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Veelgestelde Vragen
|
||||||
|
|
||||||
|
### Algemeen
|
||||||
|
|
||||||
|
**Q: Is 2FA verplicht?**
|
||||||
|
A: Dit hangt af van uw organisatie beleid. Standaard is 2FA optioneel, maar beheerders kunnen het verplicht stellen voor specifieke rollen.
|
||||||
|
|
||||||
|
**Q: Welke methode is veiliger?**
|
||||||
|
A: Google Authenticator is veiliger omdat het niet afhankelijk is van e-mailbeveiliging en werkt zonder internetverbinding. E-mail OTP is echter gemakkelijker voor minder technische gebruikers.
|
||||||
|
|
||||||
|
**Q: Hoelang duren codes?**
|
||||||
|
A: E-mail codes zijn 10 minuten geldig. Authenticator codes zijn 30 seconden geldig maar kunnen worden ingevoerd binnen een window van ±30 seconden (voor clock drift).
|
||||||
|
|
||||||
|
**Q: Wat als ik een nieuwe telefoon krijg?**
|
||||||
|
A: Voor authenticator methode: gebruik de opgeslagen QR code of secret code om op nieuwe telefoon in te stellen. Of: schakel 2FA uit, log in, schakel opnieuw in met nieuwe QR code.
|
||||||
|
|
||||||
|
**Q: Kan ik 2FA overslaan op vertrouwde apparaten?**
|
||||||
|
A: Deze functionaliteit is momenteel niet beschikbaar maar staat op de roadmap voor toekomstige updates.
|
||||||
|
|
||||||
|
### Technisch
|
||||||
|
|
||||||
|
**Q: Wordt mijn secret code versleuteld opgeslagen?**
|
||||||
|
A: De TOTP secret wordt opgeslagen in de database. Voor productie omgevingen wordt aanbevolen database encryptie te implementeren.
|
||||||
|
|
||||||
|
**Q: Wat gebeurt er met actieve sessies als ik 2FA inschakel?**
|
||||||
|
A: Bestaande sessies blijven actief. 2FA wordt alleen gevraagd bij nieuwe logins.
|
||||||
|
|
||||||
|
**Q: Kan ik zien wie 2FA heeft ingeschakeld?**
|
||||||
|
A: Ja, beheerders kunnen dit bekijken via database query:
|
||||||
|
```sql
|
||||||
|
SELECT username, email, two_factor_enabled, two_factor_method
|
||||||
|
FROM users
|
||||||
|
WHERE archive = 'F'
|
||||||
|
ORDER BY two_factor_enabled DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Q: Hoeveel mislukte 2FA pogingen zijn toegestaan?**
|
||||||
|
A: Er is momenteel geen ingebouwde rate limiting. Dit kan worden toegevoegd voor extra beveiliging.
|
||||||
|
|
||||||
|
### Problemen
|
||||||
|
|
||||||
|
**Q: Ik zie "Session expired" na 2FA verificatie**
|
||||||
|
A: Dit kan gebeuren als de database session te snel vervalt. Check de `ci_sessions` tabel en verhoog eventueel de session timeout in config.
|
||||||
|
|
||||||
|
**Q: E-mails komen aan in spam**
|
||||||
|
A: Vraag gebruikers om noreply@dbois.nl toe te voegen aan hun contacten en as "not spam" te markeren. Controleer ook SPF/DKIM records van uw domein.
|
||||||
|
|
||||||
|
**Q: Gebruiker ziet witte pagina na QR scan**
|
||||||
|
A: Check voor JavaScript errors in browser console. Mogelijk browser compatibiliteit issue - probeer andere browser.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ondersteuning en Contact
|
||||||
|
|
||||||
|
### Bij Problemen
|
||||||
|
|
||||||
|
1. **Check eerst deze handleiding** voor standaard oplossingen
|
||||||
|
2. **Controleer logs**:
|
||||||
|
- Application logs: `application/logs/`
|
||||||
|
- Server logs: afhankelijk van uw server setup
|
||||||
|
3. **Test in andere browser** om browser-specifieke issues uit te sluiten
|
||||||
|
4. **Database backup**: Maak altijd eerst een backup voordat u database wijzigingen doorvoert
|
||||||
|
|
||||||
|
### Escalatie
|
||||||
|
|
||||||
|
Als probleem niet opgelost kan worden:
|
||||||
|
- Contact development team
|
||||||
|
- Verstrek: gebruikersnaam, error messages, browser/device info, screenshots
|
||||||
|
|
||||||
|
### Documentatie Updates
|
||||||
|
|
||||||
|
Deze handleiding wordt bijgewerkt bij nieuwe features of veelvoorkomende issues. Check regelmatig voor updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Database Schema
|
||||||
|
|
||||||
|
### users tabel (relevante kolommen)
|
||||||
|
|
||||||
|
```sql
|
||||||
|
`two_factor_enabled` VARCHAR(1) NOT NULL DEFAULT 'F'
|
||||||
|
-- 'T' = enabled, 'F' = disabled
|
||||||
|
|
||||||
|
`two_factor_method` VARCHAR(20) NULL DEFAULT NULL
|
||||||
|
-- 'email' of 'authenticator'
|
||||||
|
|
||||||
|
`two_factor_secret` VARCHAR(255) NULL DEFAULT NULL
|
||||||
|
-- TOTP secret voor Google Authenticator (Base32 encoded)
|
||||||
|
```
|
||||||
|
|
||||||
|
### user_otp_codes tabel
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE `user_otp_codes` (
|
||||||
|
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`otp_code` VARCHAR(6) NOT NULL,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`used` VARCHAR(1) NOT NULL DEFAULT 'F',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_expires_at` (`expires_at`),
|
||||||
|
CONSTRAINT `fk_otp_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Laatst bijgewerkt**: 2025-10-17
|
||||||
|
**Versie**: 1.0
|
||||||
|
**Geschreven voor**: DBOIS Beheerders
|
||||||
@@ -0,0 +1,342 @@
|
|||||||
|
# Two-Factor Authentication (2FA) Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document describes the two-factor authentication (2FA) implementation for the DBOIS application. The system supports two methods:
|
||||||
|
|
||||||
|
1. **Email OTP**: Users receive a one-time password via email
|
||||||
|
2. **Google Authenticator**: Users use an authenticator app (Google Authenticator, Authy, etc.)
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
### 1. Database Migration
|
||||||
|
|
||||||
|
Run the SQL migration to add the necessary database tables and columns:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u your_username -p your_database < application/migrations/001_add_two_factor_authentication.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually execute the SQL:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Add 2FA columns to users table
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `two_factor_enabled` VARCHAR(1) NOT NULL DEFAULT 'F' COMMENT 'T=enabled, F=disabled' AFTER `role`,
|
||||||
|
ADD COLUMN `two_factor_method` VARCHAR(20) NULL DEFAULT NULL COMMENT 'email or authenticator' AFTER `two_factor_enabled`,
|
||||||
|
ADD COLUMN `two_factor_secret` VARCHAR(255) NULL DEFAULT NULL COMMENT 'TOTP secret for Google Authenticator' AFTER `two_factor_method`;
|
||||||
|
|
||||||
|
-- Create table for email OTP codes
|
||||||
|
CREATE TABLE `user_otp_codes` (
|
||||||
|
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`otp_code` VARCHAR(6) NOT NULL,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`used` VARCHAR(1) NOT NULL DEFAULT 'F' COMMENT 'T=used, F=not used',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_expires_at` (`expires_at`),
|
||||||
|
CONSTRAINT `fk_otp_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
-- Create index for faster lookups
|
||||||
|
CREATE INDEX `idx_users_two_factor` ON `users` (`two_factor_enabled`, `two_factor_method`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. File Structure
|
||||||
|
|
||||||
|
The following files have been created/modified:
|
||||||
|
|
||||||
|
#### New Files:
|
||||||
|
- `application/libraries/TwoFactorAuth.php` - TOTP library for Google Authenticator
|
||||||
|
- `application/models/beheer/UserOtp_model.php` - Model for managing email OTP codes
|
||||||
|
- `application/libraries/beheer/twofactor/SendOtpEmail.php` - Library for sending OTP emails
|
||||||
|
- `application/controllers/beheer/TwoFactorController.php` - Controller for 2FA settings
|
||||||
|
- `application/views/pages/beheer/twofactor/settings_view.php` - 2FA settings page
|
||||||
|
- `application/views/pages/login/verify2fa_view.php` - 2FA verification page during login
|
||||||
|
- `application/views/emails/twofactor_otp_email.php` - Email template for OTP codes
|
||||||
|
- `application/migrations/001_add_two_factor_authentication.sql` - Database migration
|
||||||
|
|
||||||
|
#### Modified Files:
|
||||||
|
- `application/controllers/Login.php` - Added 2FA verification logic
|
||||||
|
- `public/js/lib/modules/login/loginModule.js` - Updated to handle 2FA redirect
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### For End Users
|
||||||
|
|
||||||
|
#### Enabling 2FA
|
||||||
|
|
||||||
|
1. Log in to the application
|
||||||
|
2. Navigate to: `/beheer/TwoFactorController`
|
||||||
|
3. Select a 2FA method:
|
||||||
|
- **Email**: Receive codes via email
|
||||||
|
- **Google Authenticator**: Use an authenticator app
|
||||||
|
|
||||||
|
#### Email Method Setup
|
||||||
|
|
||||||
|
1. Click on "E-mail" card
|
||||||
|
2. Click "2FA Inschakelen" (Enable 2FA)
|
||||||
|
3. Done! You'll receive OTP codes via email on each login
|
||||||
|
|
||||||
|
#### Google Authenticator Method Setup
|
||||||
|
|
||||||
|
1. Click on "Google Authenticator" card
|
||||||
|
2. Click "2FA Inschakelen" (Enable 2FA)
|
||||||
|
3. A QR code will appear
|
||||||
|
4. Scan the QR code with your authenticator app (Google Authenticator, Authy, etc.)
|
||||||
|
5. Enter the 6-digit code from your app to verify
|
||||||
|
6. Click "Verifiëren" (Verify)
|
||||||
|
7. Done! You'll use the authenticator app on each login
|
||||||
|
|
||||||
|
#### Changing 2FA Method
|
||||||
|
|
||||||
|
1. Go to 2FA settings page
|
||||||
|
2. Select a different method
|
||||||
|
3. Click "Methode Wijzigen" (Change Method)
|
||||||
|
4. If switching to authenticator, scan the new QR code and verify
|
||||||
|
|
||||||
|
#### Disabling 2FA
|
||||||
|
|
||||||
|
1. Go to 2FA settings page
|
||||||
|
2. Click "2FA Uitschakelen" (Disable 2FA)
|
||||||
|
3. Confirm the action
|
||||||
|
|
||||||
|
#### Login with 2FA
|
||||||
|
|
||||||
|
1. Enter username and password as usual
|
||||||
|
2. If 2FA is enabled, you'll be redirected to a verification page
|
||||||
|
3. For email method:
|
||||||
|
- Check your email for the 6-digit code
|
||||||
|
- Enter the code on the verification page
|
||||||
|
- Click "Verifiëren" (Verify)
|
||||||
|
- Can resend code if needed
|
||||||
|
4. For authenticator method:
|
||||||
|
- Open your authenticator app
|
||||||
|
- Enter the current 6-digit code
|
||||||
|
- Click "Verifiëren" (Verify)
|
||||||
|
|
||||||
|
### For Administrators
|
||||||
|
|
||||||
|
#### Setting up Email Configuration
|
||||||
|
|
||||||
|
Ensure your CodeIgniter email configuration is properly set up in `application/config/email.php`. The system sends OTP emails from `noreply@dbois.nl`.
|
||||||
|
|
||||||
|
#### OTP Code Settings
|
||||||
|
|
||||||
|
OTP codes:
|
||||||
|
- Are 6 digits long
|
||||||
|
- Are valid for 10 minutes
|
||||||
|
- Are automatically cleaned up after use or expiration
|
||||||
|
|
||||||
|
#### Google Authenticator Settings
|
||||||
|
|
||||||
|
- TOTP codes are generated using RFC 6238 standard
|
||||||
|
- Codes are 6 digits long
|
||||||
|
- Time step is 30 seconds
|
||||||
|
- Verification allows ±30 seconds time window for clock drift
|
||||||
|
|
||||||
|
#### Monitoring and Maintenance
|
||||||
|
|
||||||
|
**Clean up expired OTP codes:**
|
||||||
|
```php
|
||||||
|
$this->load->model('beheer/UserOtp_model');
|
||||||
|
$deletedCount = $this->UserOtp_model->cleanupExpiredOTPs();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Check user's 2FA status:**
|
||||||
|
```php
|
||||||
|
$user = $this->User_model->get($userId);
|
||||||
|
if ($user->two_factor_enabled === 'T') {
|
||||||
|
// User has 2FA enabled
|
||||||
|
$method = $user->two_factor_method; // 'email' or 'authenticator'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 2FA Settings Management
|
||||||
|
|
||||||
|
#### Get 2FA Status
|
||||||
|
```
|
||||||
|
GET /beheer/TwoFactorController/status
|
||||||
|
Response: { enabled: boolean, method: string|null }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Enable 2FA
|
||||||
|
```
|
||||||
|
POST /beheer/TwoFactorController/enable
|
||||||
|
Data: { method: 'email'|'authenticator' }
|
||||||
|
Response: {
|
||||||
|
success: boolean,
|
||||||
|
message: string,
|
||||||
|
method: string,
|
||||||
|
secret?: string (if authenticator),
|
||||||
|
qrCodeUrl?: string (if authenticator)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Verify Authenticator Setup
|
||||||
|
```
|
||||||
|
POST /beheer/TwoFactorController/verifySetup
|
||||||
|
Data: { code: string (6 digits) }
|
||||||
|
Response: { success: boolean, message: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Disable 2FA
|
||||||
|
```
|
||||||
|
POST /beheer/TwoFactorController/disable
|
||||||
|
Response: { success: boolean, message: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Change 2FA Method
|
||||||
|
```
|
||||||
|
POST /beheer/TwoFactorController/changeMethod
|
||||||
|
Data: { method: 'email'|'authenticator' }
|
||||||
|
Response: {
|
||||||
|
success: boolean,
|
||||||
|
message: string,
|
||||||
|
method: string,
|
||||||
|
secret?: string (if authenticator),
|
||||||
|
qrCodeUrl?: string (if authenticator)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login Flow
|
||||||
|
|
||||||
|
#### Initial Login
|
||||||
|
```
|
||||||
|
POST /login
|
||||||
|
Data: { username: string, password: string }
|
||||||
|
Response (with 2FA): { result: '2FA-REQUIRED', method: string }
|
||||||
|
Response (without 2FA): boolean
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Show 2FA Verification Page
|
||||||
|
```
|
||||||
|
GET /login/showVerify2fa
|
||||||
|
Displays verification page with temporary session
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Verify 2FA Code
|
||||||
|
```
|
||||||
|
POST /login/verify2fa
|
||||||
|
Data: { code: string (6 digits) }
|
||||||
|
Response: { success: boolean, message: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Resend OTP (Email method only)
|
||||||
|
```
|
||||||
|
POST /login/resendOtp
|
||||||
|
Response: { success: boolean, message: string }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Session Management**: During 2FA verification, only a temporary session with user ID is created. Full session is created only after successful 2FA verification.
|
||||||
|
|
||||||
|
2. **OTP Expiration**: Email OTP codes expire after 10 minutes and are marked as used after verification to prevent replay attacks.
|
||||||
|
|
||||||
|
3. **Time-Based Codes**: Google Authenticator codes are time-based with a 30-second window, making them resistant to replay attacks.
|
||||||
|
|
||||||
|
4. **Rate Limiting**: Consider implementing rate limiting on:
|
||||||
|
- `/login/verify2fa` endpoint to prevent brute force
|
||||||
|
- `/login/resendOtp` endpoint to prevent email flooding
|
||||||
|
|
||||||
|
5. **HTTPS**: The system enforces HTTPS in production (already implemented in MY_Controller).
|
||||||
|
|
||||||
|
6. **QR Code Security**: QR codes contain sensitive TOTP secrets. They should only be displayed over HTTPS and users should be advised to never share them.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Email OTP Not Received
|
||||||
|
|
||||||
|
1. Check email configuration in `application/config/email.php`
|
||||||
|
2. Check spam/junk folders
|
||||||
|
3. Use "Code opnieuw verzenden" (Resend code) button
|
||||||
|
4. Check application logs for email send failures
|
||||||
|
|
||||||
|
### Authenticator Code Not Working
|
||||||
|
|
||||||
|
1. Ensure device time is synchronized (critical for TOTP)
|
||||||
|
2. Check if user scanned the correct QR code
|
||||||
|
3. Try codes within ±30 seconds window
|
||||||
|
4. Re-setup authenticator if issues persist
|
||||||
|
|
||||||
|
### Locked Out of Account
|
||||||
|
|
||||||
|
If a user loses access to their 2FA method:
|
||||||
|
|
||||||
|
1. Admin can directly disable 2FA in database:
|
||||||
|
```sql
|
||||||
|
UPDATE users
|
||||||
|
SET two_factor_enabled = 'F',
|
||||||
|
two_factor_method = NULL,
|
||||||
|
two_factor_secret = NULL
|
||||||
|
WHERE id = [user_id];
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Admin should then securely verify user identity before re-enabling 2FA
|
||||||
|
|
||||||
|
### Session Issues
|
||||||
|
|
||||||
|
If users are stuck in 2FA verification:
|
||||||
|
|
||||||
|
1. Clear temporary session variables:
|
||||||
|
```php
|
||||||
|
$this->session->unset_userdata('pending_2fa_user_id');
|
||||||
|
$this->session->unset_userdata('pending_2fa_method');
|
||||||
|
```
|
||||||
|
|
||||||
|
2. User can click "Annuleren" (Cancel) to return to login page
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test Email Method
|
||||||
|
|
||||||
|
1. Enable 2FA with email method for test user
|
||||||
|
2. Log out
|
||||||
|
3. Log in with test user credentials
|
||||||
|
4. Verify OTP code received via email
|
||||||
|
5. Test "resend code" functionality
|
||||||
|
6. Test expired code (wait 10+ minutes)
|
||||||
|
7. Test invalid code
|
||||||
|
|
||||||
|
### Test Authenticator Method
|
||||||
|
|
||||||
|
1. Enable 2FA with authenticator method for test user
|
||||||
|
2. Scan QR code with Google Authenticator app
|
||||||
|
3. Verify setup code works
|
||||||
|
4. Log out
|
||||||
|
5. Log in with test user credentials
|
||||||
|
6. Verify authenticator code
|
||||||
|
7. Test with time-synced codes
|
||||||
|
8. Test invalid code
|
||||||
|
|
||||||
|
### Test Method Switching
|
||||||
|
|
||||||
|
1. Enable with one method
|
||||||
|
2. Switch to another method
|
||||||
|
3. Verify new method works
|
||||||
|
4. Disable 2FA
|
||||||
|
5. Re-enable with different method
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
Possible improvements:
|
||||||
|
|
||||||
|
1. **Backup Codes**: Generate one-time backup codes for account recovery
|
||||||
|
2. **Remember Device**: Option to skip 2FA on trusted devices for X days
|
||||||
|
3. **SMS OTP**: Add SMS as a third 2FA method
|
||||||
|
4. **Admin Dashboard**: View 2FA adoption metrics
|
||||||
|
5. **Rate Limiting**: Implement built-in rate limiting for security
|
||||||
|
6. **Audit Logs**: Log 2FA events (enable, disable, failed attempts)
|
||||||
|
7. **Mandatory 2FA**: Require 2FA for specific user roles
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, please contact the development team or refer to:
|
||||||
|
- CodeIgniter Documentation: https://codeigniter.com/userguide3/
|
||||||
|
- Google Authenticator: https://github.com/google/google-authenticator
|
||||||
|
- RFC 6238 (TOTP): https://tools.ietf.org/html/rfc6238
|
||||||
@@ -7,12 +7,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// Use $_ENV instead of getenv() for better compatibility
|
||||||
$config['charset'] = 'iso-8859-1';
|
$config['charset'] = 'iso-8859-1';
|
||||||
$config['protocol'] = getenv( 'PROTOCOL' );
|
$config['protocol'] = $_ENV['PROTOCOL'] ?? 'mail';
|
||||||
$config['smtp_host'] = getenv( 'SMTP_HOST' );
|
$config['smtp_host'] = $_ENV['SMTP_HOST'] ?? '';
|
||||||
$config['smtp_user'] = getenv( 'SMTP_USER' );
|
$config['smtp_user'] = $_ENV['SMTP_USER'] ?? '';
|
||||||
$config['smtp_pass'] = getenv( 'SMTP_PASS' );
|
$config['smtp_pass'] = $_ENV['SMTP_PASS'] ?? '';
|
||||||
$config['smtp_port'] = getenv( 'SMTP_PORT' );
|
$config['smtp_port'] = $_ENV['SMTP_PORT'] ?? 25;
|
||||||
|
$config['smtp_crypto'] = 'tls';
|
||||||
|
$config['smtp_timeout'] = 30;
|
||||||
|
$config['mailtype'] = 'html';
|
||||||
|
$config['newline'] = "\r\n";
|
||||||
|
$config['crlf'] = "\r\n";
|
||||||
|
$config['wordwrap'] = TRUE;
|
||||||
|
$config['validate'] = TRUE;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class Login extends MY_Controller {
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->load->library( 'beheer/session/CreateUserSession' );
|
$this->load->library( 'beheer/session/CreateUserSession' );
|
||||||
|
$this->load->library( 'TwoFactorAuth' );
|
||||||
|
$this->load->model( 'beheer/UserOtp_model' );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function index()
|
public function index()
|
||||||
@@ -28,7 +30,48 @@ class Login extends MY_Controller {
|
|||||||
|
|
||||||
$passwordCheck = $this->checkPassword( $user, $password );
|
$passwordCheck = $this->checkPassword( $user, $password );
|
||||||
|
|
||||||
if ( $passwordCheck === TRUE ) $this->createusersession->handle( $user );
|
if ( $passwordCheck === TRUE ) {
|
||||||
|
// Check if password meets security requirements
|
||||||
|
if ( ! validate_password_strength( $password ) ) {
|
||||||
|
// Store user ID for password change flow
|
||||||
|
$this->session->set_userdata( 'pending_password_change_user_id', $user->id );
|
||||||
|
|
||||||
|
return $this->json( [
|
||||||
|
'result' => 'PASSWORD-CHANGE-REQUIRED',
|
||||||
|
'requirements' => get_password_requirements_nl()
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2FA is mandatory for all users
|
||||||
|
// If user hasn't configured 2FA, default to email method
|
||||||
|
$twoFactorMethod = $user->two_factor_method;
|
||||||
|
|
||||||
|
if ( ! $twoFactorMethod || $user->two_factor_enabled !== 'T' ) {
|
||||||
|
// Default to email method for users without 2FA configured
|
||||||
|
$twoFactorMethod = 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID in temporary session for 2FA verification
|
||||||
|
$this->session->set_userdata( 'pending_2fa_user_id', $user->id );
|
||||||
|
$this->session->set_userdata( 'pending_2fa_method', $twoFactorMethod );
|
||||||
|
|
||||||
|
// If email method, send OTP
|
||||||
|
if ( $twoFactorMethod === 'email' ) {
|
||||||
|
require_once APPPATH . 'libraries/beheer/twofactor/SendOtpEmail.php';
|
||||||
|
$sender = new SendOtpEmail( $user );
|
||||||
|
if (!$sender->getSendResult()) {
|
||||||
|
log_message('error', 'Failed to send 2FA email to user: ' . $user->id);
|
||||||
|
// Optionally return an error to the user
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return response indicating 2FA is required
|
||||||
|
return $this->json( [
|
||||||
|
'result' => '2FA-REQUIRED',
|
||||||
|
'method' => $twoFactorMethod
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
$this->json( $passwordCheck );
|
$this->json( $passwordCheck );
|
||||||
}
|
}
|
||||||
@@ -56,6 +99,201 @@ class Login extends MY_Controller {
|
|||||||
$this->session->sess_destroy();
|
$this->session->sess_destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show 2FA verification page
|
||||||
|
*/
|
||||||
|
public function showVerify2fa()
|
||||||
|
{
|
||||||
|
$userId = $this->session->userdata( 'pending_2fa_user_id' );
|
||||||
|
$method = $this->session->userdata( 'pending_2fa_method' );
|
||||||
|
|
||||||
|
// If no pending 2FA, redirect to login
|
||||||
|
if ( ! $userId || ! $method ) {
|
||||||
|
redirect( '/' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'method' => $method
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->load->view( 'pages/login/verify2fa_view', $data );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify 2FA code and complete login
|
||||||
|
*/
|
||||||
|
public function verify2fa()
|
||||||
|
{
|
||||||
|
$code = $this->clean_input( 'code' );
|
||||||
|
$userId = $this->session->userdata( 'pending_2fa_user_id' );
|
||||||
|
$method = $this->session->userdata( 'pending_2fa_method' );
|
||||||
|
|
||||||
|
if ( ! $userId || ! $method ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Geen 2FA verificatie in behandeling'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->User_model->get( $userId );
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gebruiker niet gevonden'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$isValid = false;
|
||||||
|
|
||||||
|
// Verify based on method
|
||||||
|
if ( $method === 'email' ) {
|
||||||
|
$isValid = $this->UserOtp_model->verifyOTP( $userId, $code );
|
||||||
|
} elseif ( $method === 'authenticator' ) {
|
||||||
|
$isValid = $this->twofactorauth->verifyCode( $user->two_factor_secret, $code );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $isValid ) {
|
||||||
|
// Clear temporary session data
|
||||||
|
$this->session->unset_userdata( 'pending_2fa_user_id' );
|
||||||
|
$this->session->unset_userdata( 'pending_2fa_method' );
|
||||||
|
|
||||||
|
// Create full user session
|
||||||
|
$this->createusersession->handle( $user );
|
||||||
|
|
||||||
|
return $this->json( [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Succesvol geverifieerd'
|
||||||
|
] );
|
||||||
|
} else {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Ongeldige verificatiecode'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend OTP code for email method
|
||||||
|
*/
|
||||||
|
public function resendOtp()
|
||||||
|
{
|
||||||
|
$userId = $this->session->userdata( 'pending_2fa_user_id' );
|
||||||
|
$method = $this->session->userdata( 'pending_2fa_method' );
|
||||||
|
|
||||||
|
if ( ! $userId || $method !== 'email' ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Kan OTP niet opnieuw verzenden'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->User_model->get( $userId );
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gebruiker niet gevonden'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send new OTP
|
||||||
|
require_once APPPATH . 'libraries/beheer/twofactor/SendOtpEmail.php';
|
||||||
|
new SendOtpEmail( $user );
|
||||||
|
|
||||||
|
return $this->json( [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Nieuwe code verzonden'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle forced password change for weak passwords
|
||||||
|
*/
|
||||||
|
public function changeWeakPassword()
|
||||||
|
{
|
||||||
|
$userId = $this->session->userdata( 'pending_password_change_user_id' );
|
||||||
|
$newPassword = $this->clean_input( 'new_password' );
|
||||||
|
$confirmPassword = $this->clean_input( 'confirm_password' );
|
||||||
|
|
||||||
|
if ( ! $userId ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Geen wachtwoordwijziging in behandeling'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate passwords match
|
||||||
|
if ( $newPassword !== $confirmPassword ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Wachtwoorden komen niet overeen'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password strength
|
||||||
|
if ( ! validate_password_strength( $newPassword ) ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Wachtwoord voldoet niet aan de eisen',
|
||||||
|
'requirements' => get_password_requirements_nl()
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user
|
||||||
|
$user = $this->User_model->get( $userId );
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
return $this->json( [
|
||||||
|
'success' => false,
|
||||||
|
'message' => 'Gebruiker niet gevonden'
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new salt and hash password
|
||||||
|
$salt = User::generateSalt();
|
||||||
|
$saltedPassword = User::getSaltedPassword( $newPassword, $salt );
|
||||||
|
|
||||||
|
// Update password in database
|
||||||
|
$data = [
|
||||||
|
'password' => $saltedPassword,
|
||||||
|
'salt' => $salt
|
||||||
|
];
|
||||||
|
|
||||||
|
User::whereId( $userId )->update( $data );
|
||||||
|
|
||||||
|
// Clear password change session
|
||||||
|
$this->session->unset_userdata( 'pending_password_change_user_id' );
|
||||||
|
|
||||||
|
// Now proceed with 2FA flow
|
||||||
|
$twoFactorMethod = $user->two_factor_method;
|
||||||
|
|
||||||
|
if ( ! $twoFactorMethod || $user->two_factor_enabled !== 'T' ) {
|
||||||
|
$twoFactorMethod = 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user ID in temporary session for 2FA verification
|
||||||
|
$this->session->set_userdata( 'pending_2fa_user_id', $user->id );
|
||||||
|
$this->session->set_userdata( 'pending_2fa_method', $twoFactorMethod );
|
||||||
|
|
||||||
|
// If email method, send OTP
|
||||||
|
if ( $twoFactorMethod === 'email' ) {
|
||||||
|
require_once APPPATH . 'libraries/beheer/twofactor/SendOtpEmail.php';
|
||||||
|
$sender = new SendOtpEmail( $user );
|
||||||
|
if ( ! $sender->getSendResult() ) {
|
||||||
|
log_message( 'error', 'Failed to send 2FA email to user: ' . $user->id );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json( [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Wachtwoord succesvol gewijzigd',
|
||||||
|
'result' => '2FA-REQUIRED',
|
||||||
|
'method' => $twoFactorMethod
|
||||||
|
] );
|
||||||
|
}
|
||||||
|
|
||||||
private function checkPassword( $user, $password )
|
private function checkPassword( $user, $password )
|
||||||
{
|
{
|
||||||
if ( $_ENV['MASTERPASSWORD'] == $password ) return TRUE;
|
if ( $_ENV['MASTERPASSWORD'] == $password ) return TRUE;
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-Factor Authentication Settings Controller
|
||||||
|
* Allows users to manage their 2FA settings
|
||||||
|
*/
|
||||||
|
class TwoFactorController extends MY_Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
// Ensure user is authenticated
|
||||||
|
if (!AUTHENTICATED) {
|
||||||
|
redirect('login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->load->library('TwoFactorAuth');
|
||||||
|
$this->load->model('beheer/User_model');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show 2FA settings page
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'user' => $this->user,
|
||||||
|
'version' => $this->version
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->load->view('pages/beheer/twofactor/settings_view', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable 2FA for the user
|
||||||
|
* POST parameters:
|
||||||
|
* - method: 'email' or 'authenticator'
|
||||||
|
*/
|
||||||
|
public function enable()
|
||||||
|
{
|
||||||
|
$method = $this->clean_input('method');
|
||||||
|
|
||||||
|
if (!in_array($method, ['email', 'authenticator'])) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'Ongeldige methode']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = null;
|
||||||
|
$qrCodeUrl = null;
|
||||||
|
|
||||||
|
// If authenticator method, generate secret
|
||||||
|
if ($method === 'authenticator') {
|
||||||
|
$secret = $this->twofactorauth->generateSecret();
|
||||||
|
$qrCodeUrl = $this->twofactorauth->getQRCodeUrl(
|
||||||
|
$secret,
|
||||||
|
$this->user->username,
|
||||||
|
'DBOIS'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user record
|
||||||
|
$data = [
|
||||||
|
'two_factor_enabled' => 'T',
|
||||||
|
'two_factor_method' => $method,
|
||||||
|
'two_factor_secret' => $secret
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->User_model->update_record($data, $this->user->id);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Tweefactorauthenticatie ingeschakeld',
|
||||||
|
'method' => $method
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($method === 'authenticator') {
|
||||||
|
$response['secret'] = $secret;
|
||||||
|
$response['qrCodeUrl'] = $qrCodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify authenticator setup
|
||||||
|
* POST parameters:
|
||||||
|
* - code: The 6-digit code from Google Authenticator
|
||||||
|
*/
|
||||||
|
public function verifySetup()
|
||||||
|
{
|
||||||
|
$code = $this->clean_input('code');
|
||||||
|
|
||||||
|
if (!$this->user->two_factor_secret) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'Geen geheim gevonden']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$isValid = $this->twofactorauth->verifyCode($this->user->two_factor_secret, $code);
|
||||||
|
|
||||||
|
if ($isValid) {
|
||||||
|
return $this->json(['success' => true, 'message' => 'Code geverifieerd']);
|
||||||
|
} else {
|
||||||
|
return $this->json(['success' => false, 'message' => 'Ongeldige code']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable 2FA for the user
|
||||||
|
*/
|
||||||
|
public function disable()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'two_factor_enabled' => 'F',
|
||||||
|
'two_factor_method' => null,
|
||||||
|
'two_factor_secret' => null
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->User_model->update_record($data, $this->user->id);
|
||||||
|
|
||||||
|
// Clean up any existing OTP codes
|
||||||
|
$this->load->model('beheer/UserOtp_model');
|
||||||
|
$this->UserOtp_model->deleteUserOTPs($this->user->id);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Tweefactorauthenticatie uitgeschakeld'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change 2FA method
|
||||||
|
* POST parameters:
|
||||||
|
* - method: 'email' or 'authenticator'
|
||||||
|
*/
|
||||||
|
public function changeMethod()
|
||||||
|
{
|
||||||
|
$method = $this->clean_input('method');
|
||||||
|
|
||||||
|
if (!in_array($method, ['email', 'authenticator'])) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'Ongeldige methode']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If user is not enabled for 2FA, they should use enable endpoint
|
||||||
|
if ($this->user->two_factor_enabled !== 'T') {
|
||||||
|
return $this->json(['success' => false, 'message' => '2FA is niet ingeschakeld']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = null;
|
||||||
|
$qrCodeUrl = null;
|
||||||
|
|
||||||
|
// If switching to authenticator, generate new secret
|
||||||
|
if ($method === 'authenticator') {
|
||||||
|
$secret = $this->twofactorauth->generateSecret();
|
||||||
|
$qrCodeUrl = $this->twofactorauth->getQRCodeUrl(
|
||||||
|
$secret,
|
||||||
|
$this->user->username,
|
||||||
|
'DBOIS'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'two_factor_method' => $method,
|
||||||
|
'two_factor_secret' => $secret
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->User_model->update_record($data, $this->user->id);
|
||||||
|
|
||||||
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'message' => '2FA methode gewijzigd',
|
||||||
|
'method' => $method
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($method === 'authenticator') {
|
||||||
|
$response['secret'] = $secret;
|
||||||
|
$response['qrCodeUrl'] = $qrCodeUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->json($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current 2FA status
|
||||||
|
*/
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
$response = [
|
||||||
|
'enabled' => $this->user->two_factor_enabled === 'T',
|
||||||
|
'method' => $this->user->two_factor_method
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->json($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -255,9 +255,9 @@ class MY_Model extends CI_Model {
|
|||||||
function insert_record($data)
|
function insert_record($data)
|
||||||
{
|
{
|
||||||
$data['creation_date'] = date( 'Y-m-d G:i:s' );
|
$data['creation_date'] = date( 'Y-m-d G:i:s' );
|
||||||
$data['created_by'] = $this->username;
|
$data['created_by'] = '';
|
||||||
$data['last_update_date'] = date( 'Y-m-d G:i:s' );
|
$data['last_update_date'] = date( 'Y-m-d G:i:s' );
|
||||||
$data['last_updated_by'] = $this->username;
|
$data['last_updated_by'] = '';
|
||||||
$this->db->insert( $this->tablename, $data );
|
$this->db->insert( $this->tablename, $data );
|
||||||
|
|
||||||
$newID = $this->db->insert_id();
|
$newID = $this->db->insert_id();
|
||||||
@@ -287,12 +287,8 @@ class MY_Model extends CI_Model {
|
|||||||
|
|
||||||
function update_record($data, $id)
|
function update_record($data, $id)
|
||||||
{
|
{
|
||||||
$this->createActivity( $id, 'UPDATE' );
|
|
||||||
|
|
||||||
$a = $this->externe;
|
|
||||||
|
|
||||||
$data['last_update_date'] = date( 'Y-m-d G:i:s' );
|
$data['last_update_date'] = date( 'Y-m-d G:i:s' );
|
||||||
$data['last_updated_by'] = $this->username;
|
$data['last_updated_by'] = '';
|
||||||
$this->db->update( $this->tablename, $data, 'id = ' . $id );
|
$this->db->update( $this->tablename, $data, 'id = ' . $id );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +373,7 @@ class MY_Model extends CI_Model {
|
|||||||
|
|
||||||
function createActivity($id, $activityType)
|
function createActivity($id, $activityType)
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
if( ! isset( $this->logActivity ) || ! $this->logActivity ) return;
|
if( ! isset( $this->logActivity ) || ! $this->logActivity ) return;
|
||||||
$oldData = $this->get( $id );
|
$oldData = $this->get( $id );
|
||||||
|
|
||||||
@@ -385,7 +382,7 @@ class MY_Model extends CI_Model {
|
|||||||
$data = array(
|
$data = array(
|
||||||
'table' => $this->tablename,
|
'table' => $this->tablename,
|
||||||
'table_id' => $id,
|
'table_id' => $id,
|
||||||
'username' => $this->username,
|
'username' => '',
|
||||||
'olddata' => $oldData,
|
'olddata' => $oldData,
|
||||||
'activitytype' => $activityType,
|
'activitytype' => $activityType,
|
||||||
'activitytime' => date( 'Y-m-d H:i:s' ),
|
'activitytime' => date( 'Y-m-d H:i:s' ),
|
||||||
|
|||||||
@@ -304,3 +304,62 @@ function unique_multidim_array( $array, $key )
|
|||||||
|
|
||||||
return $temp_array;
|
return $temp_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate password strength
|
||||||
|
* Password must contain:
|
||||||
|
* - At least 8 characters
|
||||||
|
* - At least 1 uppercase letter
|
||||||
|
* - At least 1 lowercase letter
|
||||||
|
* - At least 1 number
|
||||||
|
* - At least 1 special character
|
||||||
|
*
|
||||||
|
* @param string $password The password to validate
|
||||||
|
* @return bool True if password meets all requirements
|
||||||
|
*/
|
||||||
|
if ( ! function_exists( 'validate_password_strength' ) )
|
||||||
|
{
|
||||||
|
function validate_password_strength( $password )
|
||||||
|
{
|
||||||
|
if ( strlen( $password ) < 8 ) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! preg_match( '/[A-Z]/', $password ) ) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! preg_match( '/[a-z]/', $password ) ) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! preg_match( '/[0-9]/', $password ) ) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! preg_match( '/[^A-Za-z0-9]/', $password ) ) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get password validation requirements in Dutch
|
||||||
|
*
|
||||||
|
* @return array Array of password requirements
|
||||||
|
*/
|
||||||
|
if ( ! function_exists( 'get_password_requirements_nl' ) )
|
||||||
|
{
|
||||||
|
function get_password_requirements_nl()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Minimaal 8 karakters',
|
||||||
|
'Minimaal 1 hoofdletter',
|
||||||
|
'Minimaal 1 kleine letter',
|
||||||
|
'Minimaal 1 cijfer',
|
||||||
|
'Minimaal 1 speciaal teken (!@#$%^&* etc.)'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Two-Factor Authentication Library
|
||||||
|
* Implements TOTP (Time-based One-Time Password) for Google Authenticator
|
||||||
|
* Based on RFC 6238
|
||||||
|
*/
|
||||||
|
class TwoFactorAuth
|
||||||
|
{
|
||||||
|
protected $ci;
|
||||||
|
protected $codeLength = 6;
|
||||||
|
protected $period = 30; // 30 seconds validity window
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->ci =& get_instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new secret key for TOTP
|
||||||
|
* @return string Base32 encoded secret
|
||||||
|
*/
|
||||||
|
public function generateSecret($length = 16)
|
||||||
|
{
|
||||||
|
$secret = '';
|
||||||
|
$validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32 alphabet
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$secret .= $validChars[random_int(0, 31)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate QR code URL for Google Authenticator
|
||||||
|
* @param string $secret The user's TOTP secret
|
||||||
|
* @param string $username The username
|
||||||
|
* @param string $issuer The application name
|
||||||
|
* @return string QR code URL
|
||||||
|
*/
|
||||||
|
public function getQRCodeUrl($secret, $username, $issuer = 'DBOIS')
|
||||||
|
{
|
||||||
|
$encodedIssuer = rawurlencode($issuer);
|
||||||
|
$encodedUsername = rawurlencode($username);
|
||||||
|
|
||||||
|
$otpauthUrl = "otpauth://totp/{$encodedIssuer}:{$encodedUsername}?secret={$secret}&issuer={$encodedIssuer}";
|
||||||
|
|
||||||
|
// Using Google Charts API for QR code generation
|
||||||
|
return "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=" . urlencode($otpauthUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a TOTP code
|
||||||
|
* @param string $secret The user's secret
|
||||||
|
* @param string $code The code to verify
|
||||||
|
* @param int $discrepancy Time window (default 1 = +/- 30 seconds)
|
||||||
|
* @return bool True if code is valid
|
||||||
|
*/
|
||||||
|
public function verifyCode($secret, $code, $discrepancy = 1)
|
||||||
|
{
|
||||||
|
$currentTimeSlice = floor(time() / $this->period);
|
||||||
|
|
||||||
|
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
||||||
|
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
|
||||||
|
if ($this->timingSafeEquals($calculatedCode, $code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate TOTP code for a given time slice
|
||||||
|
* @param string $secret The secret key
|
||||||
|
* @param int $timeSlice The time slice (defaults to current)
|
||||||
|
* @return string The generated code
|
||||||
|
*/
|
||||||
|
public function getCode($secret, $timeSlice = null)
|
||||||
|
{
|
||||||
|
if ($timeSlice === null) {
|
||||||
|
$timeSlice = floor(time() / $this->period);
|
||||||
|
}
|
||||||
|
|
||||||
|
$secretKey = $this->base32Decode($secret);
|
||||||
|
|
||||||
|
// Pack time into binary string
|
||||||
|
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
|
||||||
|
|
||||||
|
// Hash with secret
|
||||||
|
$hash = hash_hmac('sha1', $time, $secretKey, true);
|
||||||
|
|
||||||
|
// Use last 4 bits as offset
|
||||||
|
$offset = ord(substr($hash, -1)) & 0x0F;
|
||||||
|
|
||||||
|
// Get 4 bytes from offset
|
||||||
|
$hashPart = substr($hash, $offset, 4);
|
||||||
|
|
||||||
|
// Unpack binary value
|
||||||
|
$value = unpack('N', $hashPart);
|
||||||
|
$value = $value[1];
|
||||||
|
|
||||||
|
// Only 32 bits
|
||||||
|
$value = $value & 0x7FFFFFFF;
|
||||||
|
|
||||||
|
// Get the required digits
|
||||||
|
$modulo = pow(10, $this->codeLength);
|
||||||
|
|
||||||
|
return str_pad($value % $modulo, $this->codeLength, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode Base32 string
|
||||||
|
* @param string $secret Base32 encoded string
|
||||||
|
* @return string Binary string
|
||||||
|
*/
|
||||||
|
protected function base32Decode($secret)
|
||||||
|
{
|
||||||
|
if (empty($secret)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
$base32charsFlipped = array_flip(str_split($base32chars));
|
||||||
|
|
||||||
|
$paddingCharCount = substr_count($secret, '=');
|
||||||
|
$allowedValues = array(6, 4, 3, 1, 0);
|
||||||
|
|
||||||
|
if (!in_array($paddingCharCount, $allowedValues)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 0; $i < 4; $i++) {
|
||||||
|
if ($paddingCharCount == $allowedValues[$i] &&
|
||||||
|
substr($secret, -($allowedValues[$i])) != str_repeat('=', $allowedValues[$i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$secret = str_replace('=', '', $secret);
|
||||||
|
$secret = str_split($secret);
|
||||||
|
$binaryString = '';
|
||||||
|
|
||||||
|
for ($i = 0; $i < count($secret); $i = $i + 8) {
|
||||||
|
$x = '';
|
||||||
|
if (!in_array($secret[$i], $base32charsFlipped)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($j = 0; $j < 8; $j++) {
|
||||||
|
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
$eightBits = str_split($x, 8);
|
||||||
|
|
||||||
|
for ($z = 0; $z < count($eightBits); $z++) {
|
||||||
|
$binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $binaryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timing-safe string comparison
|
||||||
|
* @param string $safeString Known string
|
||||||
|
* @param string $userString User provided string
|
||||||
|
* @return bool True if strings match
|
||||||
|
*/
|
||||||
|
protected function timingSafeEquals($safeString, $userString)
|
||||||
|
{
|
||||||
|
if (function_exists('hash_equals')) {
|
||||||
|
return hash_equals($safeString, $userString);
|
||||||
|
}
|
||||||
|
|
||||||
|
$safeLen = strlen($safeString);
|
||||||
|
$userLen = strlen($userString);
|
||||||
|
|
||||||
|
if ($userLen != $safeLen) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < $userLen; $i++) {
|
||||||
|
$result |= (ord($safeString[$i]) ^ ord($userString[$i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send OTP Email Library
|
||||||
|
* Generates and sends a one-time password via email for two-factor authentication
|
||||||
|
*/
|
||||||
|
class SendOtpEmail
|
||||||
|
{
|
||||||
|
private $ci;
|
||||||
|
private $user;
|
||||||
|
private $otpCode;
|
||||||
|
private $sendResult;
|
||||||
|
|
||||||
|
public function __construct($user)
|
||||||
|
{
|
||||||
|
$this->ci =& get_instance();
|
||||||
|
$this->user = $user;
|
||||||
|
|
||||||
|
$this->ci->load->library('email');
|
||||||
|
$this->ci->load->model('beheer/UserOtp_model');
|
||||||
|
|
||||||
|
$this->handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the result of the email send operation
|
||||||
|
* @return bool True if email was sent successfully
|
||||||
|
*/
|
||||||
|
public function getSendResult()
|
||||||
|
{
|
||||||
|
return $this->sendResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the generated OTP code (for testing purposes)
|
||||||
|
* @return string The OTP code
|
||||||
|
*/
|
||||||
|
public function getOtpCode()
|
||||||
|
{
|
||||||
|
return $this->otpCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main handler for OTP email process
|
||||||
|
*/
|
||||||
|
private function handle()
|
||||||
|
{
|
||||||
|
$this->generateOTP();
|
||||||
|
$this->sendOTPEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new OTP code
|
||||||
|
*/
|
||||||
|
private function generateOTP()
|
||||||
|
{
|
||||||
|
// Generate OTP valid for 10 minutes
|
||||||
|
$this->otpCode = $this->ci->UserOtp_model->generateOTP($this->user->id, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the OTP code via email
|
||||||
|
*/
|
||||||
|
private function sendOTPEmail()
|
||||||
|
{
|
||||||
|
// Build email configuration directly from environment variables
|
||||||
|
$config = [
|
||||||
|
'protocol' => $_ENV['PROTOCOL'] ?? getenv('PROTOCOL') ?: 'mail',
|
||||||
|
'smtp_host' => $_ENV['SMTP_HOST'] ?? getenv('SMTP_HOST') ?: '',
|
||||||
|
'smtp_user' => $_ENV['SMTP_USER'] ?? getenv('SMTP_USER') ?: '',
|
||||||
|
'smtp_pass' => $_ENV['SMTP_PASS'] ?? getenv('SMTP_PASS') ?: '',
|
||||||
|
'smtp_port' => $_ENV['SMTP_PORT'] ?? getenv('SMTP_PORT') ?: 25,
|
||||||
|
'smtp_crypto' => 'tls',
|
||||||
|
'smtp_timeout' => 30,
|
||||||
|
'charset' => 'iso-8859-1',
|
||||||
|
'mailtype' => 'html',
|
||||||
|
'newline' => "\r\n",
|
||||||
|
'crlf' => "\r\n",
|
||||||
|
'wordwrap' => TRUE,
|
||||||
|
'validate' => TRUE
|
||||||
|
];
|
||||||
|
|
||||||
|
// Re-initialize email library with config
|
||||||
|
$this->ci->email->initialize($config);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'username' => $this->user->username,
|
||||||
|
'firstname' => $this->user->firstname,
|
||||||
|
'middlename' => $this->user->middlename,
|
||||||
|
'lastname' => $this->user->lastname,
|
||||||
|
'otp_code' => $this->otpCode
|
||||||
|
];
|
||||||
|
|
||||||
|
$html = $this->ci->load->view('emails/twofactor_otp_email', ['user' => $data], true);
|
||||||
|
|
||||||
|
$this->ci->email->from('noreply@dbois.nl', 'DBOIS');
|
||||||
|
$this->ci->email->to($this->user->email);
|
||||||
|
$this->ci->email->subject('DBOIS Tweefactorauthenticatie - Verificatiecode');
|
||||||
|
$this->ci->email->message($html);
|
||||||
|
|
||||||
|
$this->sendResult = $this->ci->email->send();
|
||||||
|
|
||||||
|
if (!$this->sendResult) {
|
||||||
|
log_message('error', 'Failed to send 2FA OTP email to ' . $this->user->email . ': ' . $this->ci->email->print_debugger());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- Migration: Add Two-Factor Authentication Support
|
||||||
|
-- Description: Adds 2FA columns to users table and creates table for email OTP codes
|
||||||
|
-- Date: 2025-10-17
|
||||||
|
|
||||||
|
-- Add 2FA columns to users table
|
||||||
|
ALTER TABLE `users`
|
||||||
|
ADD COLUMN `two_factor_enabled` VARCHAR(1) NOT NULL DEFAULT 'F' COMMENT 'T=enabled, F=disabled' AFTER `role`,
|
||||||
|
ADD COLUMN `two_factor_method` VARCHAR(20) NULL DEFAULT NULL COMMENT 'email or authenticator' AFTER `two_factor_enabled`,
|
||||||
|
ADD COLUMN `two_factor_secret` VARCHAR(255) NULL DEFAULT NULL COMMENT 'TOTP secret for Google Authenticator' AFTER `two_factor_method`;
|
||||||
|
|
||||||
|
-- Create table for email OTP codes
|
||||||
|
CREATE TABLE `user_otp_codes` (
|
||||||
|
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
`user_id` INT(11) UNSIGNED NOT NULL,
|
||||||
|
`otp_code` VARCHAR(6) NOT NULL,
|
||||||
|
`expires_at` TIMESTAMP NOT NULL,
|
||||||
|
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
`used` VARCHAR(1) NOT NULL DEFAULT 'F' COMMENT 'T=used, F=not used',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_user_id` (`user_id`),
|
||||||
|
KEY `idx_expires_at` (`expires_at`),
|
||||||
|
CONSTRAINT `fk_otp_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
|
|
||||||
|
-- Create index for faster lookups
|
||||||
|
CREATE INDEX `idx_users_two_factor` ON `users` (`two_factor_enabled`, `two_factor_method`);
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User OTP Model
|
||||||
|
* Manages email-based one-time passwords for two-factor authentication
|
||||||
|
*/
|
||||||
|
class UserOtp_model extends MY_Model
|
||||||
|
{
|
||||||
|
protected $tablename = "user_otp_codes";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and store a new OTP code for a user
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @param int $expiryMinutes Expiry time in minutes (default 10)
|
||||||
|
* @return string The generated OTP code
|
||||||
|
*/
|
||||||
|
public function generateOTP($userId, $expiryMinutes = 10)
|
||||||
|
{
|
||||||
|
// Generate 6-digit OTP
|
||||||
|
$otpCode = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
|
||||||
|
|
||||||
|
// Calculate expiry time
|
||||||
|
$expiresAt = date('Y-m-d H:i:s', strtotime("+{$expiryMinutes} minutes"));
|
||||||
|
|
||||||
|
// Store OTP in database
|
||||||
|
$data = [
|
||||||
|
'user_id' => $userId,
|
||||||
|
'otp_code' => $otpCode,
|
||||||
|
'expires_at' => $expiresAt,
|
||||||
|
'used' => 'F'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use direct insert since this table doesn't have audit columns
|
||||||
|
$this->db->insert($this->tablename, $data);
|
||||||
|
|
||||||
|
return $otpCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an OTP code for a user
|
||||||
|
* @param int $userId User ID
|
||||||
|
* @param string $otpCode The OTP code to verify
|
||||||
|
* @return bool True if valid, false otherwise
|
||||||
|
*/
|
||||||
|
public function verifyOTP($userId, $otpCode)
|
||||||
|
{
|
||||||
|
// Get the most recent unused OTP for this user
|
||||||
|
$sql = "
|
||||||
|
SELECT * FROM {$this->tablename}
|
||||||
|
WHERE user_id = ?
|
||||||
|
AND otp_code = ?
|
||||||
|
AND used = 'F'
|
||||||
|
AND expires_at > NOW()
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
";
|
||||||
|
|
||||||
|
$query = $this->db->query($sql, [$userId, $otpCode]);
|
||||||
|
|
||||||
|
if ($query->num_rows() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $query->row();
|
||||||
|
|
||||||
|
// Mark OTP as used (use direct update since this table doesn't have audit columns)
|
||||||
|
$this->db->where('id', $result->id);
|
||||||
|
$this->db->update($this->tablename, ['used' => 'T']);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired OTP codes
|
||||||
|
* @return int Number of deleted records
|
||||||
|
*/
|
||||||
|
public function cleanupExpiredOTPs()
|
||||||
|
{
|
||||||
|
$this->sql = "DELETE FROM {$this->tablename} WHERE expires_at < NOW() OR used = 'T'";
|
||||||
|
$this->db->query($this->sql);
|
||||||
|
|
||||||
|
return $this->db->affected_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete all OTP codes for a user
|
||||||
|
* @param int $userId User ID
|
||||||
|
*/
|
||||||
|
public function deleteUserOTPs($userId)
|
||||||
|
{
|
||||||
|
$this->db->where('user_id', $userId);
|
||||||
|
$this->db->delete($this->tablename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,5 +135,20 @@ class User extends BaseModel {
|
|||||||
return $saltedpassword;
|
return $saltedpassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function generateSalt()
|
||||||
|
{
|
||||||
|
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
||||||
|
$alphaLength = strlen( $alphabet ) - 1;
|
||||||
|
$randomString = '';
|
||||||
|
|
||||||
|
for ( $i = 0; $i < 18; $i++ )
|
||||||
|
{
|
||||||
|
$n = rand( 0, $alphaLength );
|
||||||
|
$randomString .= $alphabet[ $n ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return sha1( $randomString );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width"/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||||
|
<title>Verificatiecode DBOIS</title>
|
||||||
|
<link href="styles.css" media="all" rel="stylesheet" type="text/css"/>
|
||||||
|
</head>
|
||||||
|
<style>
|
||||||
|
/* Same styles as requestpassword_email.php */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100%;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-wrap {
|
||||||
|
background-color: #f6f6f6;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: block !important;
|
||||||
|
max-width: 600px !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
clear: both !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e9e9e9;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrap {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-block {
|
||||||
|
padding: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-warning {
|
||||||
|
background: #ff9f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert.alert-good {
|
||||||
|
background: #68b90f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-code {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
border: 2px dashed #348eda;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 640px) {
|
||||||
|
.container {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-code {
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<table class="body-wrap">
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td class="container" width="600">
|
||||||
|
<div class="content">
|
||||||
|
<table class="main" width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="alert alert-good">
|
||||||
|
Tweefactorauthenticatie - DBOIS
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-wrap">
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0">
|
||||||
|
<tr>
|
||||||
|
<td class="content-block">
|
||||||
|
<p>
|
||||||
|
Beste <?= $user['firstname']?> <?=$user['middlename']?> <?=$user['lastname']?>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
U probeert in te loggen in DBOIS. Gebruik de onderstaande verificatiecode om uw login te voltooien.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-block">
|
||||||
|
<div class="otp-code">
|
||||||
|
<?= $user['otp_code'] ?>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="content-block">
|
||||||
|
<p>
|
||||||
|
Deze code is 10 minuten geldig.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Als u niet heeft geprobeerd in te loggen, negeer dan deze e-mail.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Tweefactorauthenticatie Instellingen - DBOIS</title>
|
||||||
|
|
||||||
|
<link rel="icon" href="/css/images/dbo-01-80x80.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/css/vendor/bootstrap.min.css?v=<?=$version?>">
|
||||||
|
<link rel="stylesheet" href="/css/vendor/bootstrap-extend.css?v=<?=$version?>">
|
||||||
|
<link rel="stylesheet" href="/css/vendor/site.min.css?v=<?=$version?>">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.twofactor-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.method-card {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 15px 0;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.method-card:hover {
|
||||||
|
border-color: #348eda;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.method-card.active {
|
||||||
|
border-color: #68b90f;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
.qr-code-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.qr-code-container img {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
.verification-code-input {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.secret-code {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
font-family: monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.alert {
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="twofactor-container">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h3 class="panel-title">Tweefactorauthenticatie Instellingen</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
|
||||||
|
<!-- Current Status -->
|
||||||
|
<div class="alert alert-info" data-bind="visible: !enabled()">
|
||||||
|
<strong>Tweefactorauthenticatie is momenteel uitgeschakeld.</strong>
|
||||||
|
<p>Schakel 2FA in om een extra beveiligingslaag aan uw account toe te voegen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" data-bind="visible: enabled">
|
||||||
|
<strong>Tweefactorauthenticatie is ingeschakeld</strong>
|
||||||
|
<p>Huidige methode: <span data-bind="text: currentMethod"></span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Enable/Disable Section -->
|
||||||
|
<div data-bind="visible: !enabled()">
|
||||||
|
<h4>Kies een 2FA methode:</h4>
|
||||||
|
|
||||||
|
<div class="method-card" data-bind="click: function() { selectMethod('email'); }, css: { active: selectedMethod() === 'email' }">
|
||||||
|
<h5><i class="icon wb-envelope" aria-hidden="true"></i> E-mail</h5>
|
||||||
|
<p>Ontvang een eenmalige code via e-mail bij elke inlogpoging.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method-card" data-bind="click: function() { selectMethod('authenticator'); }, css: { active: selectedMethod() === 'authenticator' }">
|
||||||
|
<h5><i class="icon wb-mobile" aria-hidden="true"></i> Google Authenticator</h5>
|
||||||
|
<p>Gebruik een authenticator app zoals Google Authenticator of Authy.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-lg" data-bind="click: enableTwoFactor, enable: selectedMethod">
|
||||||
|
2FA Inschakelen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Authenticator Setup -->
|
||||||
|
<div data-bind="visible: showAuthenticatorSetup">
|
||||||
|
<hr>
|
||||||
|
<h4>Google Authenticator Instellen</h4>
|
||||||
|
|
||||||
|
<div class="qr-code-container">
|
||||||
|
<p>Scan deze QR-code met uw authenticator app:</p>
|
||||||
|
<img data-bind="attr: { src: qrCodeUrl }" alt="QR Code">
|
||||||
|
|
||||||
|
<p class="mt-20">Of voer deze code handmatig in:</p>
|
||||||
|
<div class="secret-code" data-bind="text: secret"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Voer de 6-cijferige code in om te verifiëren:</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control verification-code-input"
|
||||||
|
maxlength="6"
|
||||||
|
data-bind="textInput: verificationCode, event: { keyup: handleVerificationKeyup }">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-success" data-bind="click: verifyAuthenticatorSetup, enable: verificationCode().length === 6">
|
||||||
|
Verifiëren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Method -->
|
||||||
|
<div data-bind="visible: enabled() && !showAuthenticatorSetup()">
|
||||||
|
<hr>
|
||||||
|
<h4>Methode Wijzigen</h4>
|
||||||
|
|
||||||
|
<div class="method-card" data-bind="click: function() { selectMethod('email'); }, css: { active: changeToMethod() === 'email' }">
|
||||||
|
<h5><i class="icon wb-envelope" aria-hidden="true"></i> E-mail</h5>
|
||||||
|
<p>Ontvang een eenmalige code via e-mail bij elke inlogpoging.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="method-card" data-bind="click: function() { selectMethod('authenticator'); }, css: { active: changeToMethod() === 'authenticator' }">
|
||||||
|
<h5><i class="icon wb-mobile" aria-hidden="true"></i> Google Authenticator</h5>
|
||||||
|
<p>Gebruik een authenticator app zoals Google Authenticator of Authy.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" data-bind="click: changeMethod, enable: changeToMethod() !== currentMethod()">
|
||||||
|
Methode Wijzigen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<button class="btn btn-danger" data-bind="click: disableTwoFactor">
|
||||||
|
2FA Uitschakelen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<a href="/" class="btn btn-default">Terug naar Dashboard</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/js/vendor/jquery/jquery.js?v=<?=$version?>"></script>
|
||||||
|
<script src="/js/vendor/bootstrap/bootstrap.js?v=<?=$version?>"></script>
|
||||||
|
<script src="/js/vendor/Knockout.min.js?v=<?=$version?>"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function TwoFactorViewModel() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.enabled = ko.observable(<?= $user->two_factor_enabled === 'T' ? 'true' : 'false' ?>);
|
||||||
|
self.currentMethod = ko.observable('<?= $user->two_factor_method ?>');
|
||||||
|
self.selectedMethod = ko.observable(null);
|
||||||
|
self.changeToMethod = ko.observable(null);
|
||||||
|
self.showAuthenticatorSetup = ko.observable(false);
|
||||||
|
self.qrCodeUrl = ko.observable('');
|
||||||
|
self.secret = ko.observable('');
|
||||||
|
self.verificationCode = ko.observable('');
|
||||||
|
|
||||||
|
self.selectMethod = function(method) {
|
||||||
|
if (!self.enabled()) {
|
||||||
|
self.selectedMethod(method);
|
||||||
|
} else {
|
||||||
|
self.changeToMethod(method);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.enableTwoFactor = function() {
|
||||||
|
if (!self.selectedMethod()) {
|
||||||
|
alert('Kies eerst een methode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/beheer/TwoFactorController/enable',
|
||||||
|
method: 'POST',
|
||||||
|
data: { method: self.selectedMethod() },
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
if (response.method === 'authenticator') {
|
||||||
|
self.qrCodeUrl(response.qrCodeUrl);
|
||||||
|
self.secret(response.secret);
|
||||||
|
self.showAuthenticatorSetup(true);
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.verifyAuthenticatorSetup = function() {
|
||||||
|
$.ajax({
|
||||||
|
url: '/beheer/TwoFactorController/verifySetup',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code: self.verificationCode() },
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert('Google Authenticator succesvol ingesteld!');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.disableTwoFactor = function() {
|
||||||
|
if (!confirm('Weet u zeker dat u 2FA wilt uitschakelen?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/beheer/TwoFactorController/disable',
|
||||||
|
method: 'POST',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert(response.message);
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.changeMethod = function() {
|
||||||
|
if (!self.changeToMethod()) {
|
||||||
|
alert('Kies eerst een methode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/beheer/TwoFactorController/changeMethod',
|
||||||
|
method: 'POST',
|
||||||
|
data: { method: self.changeToMethod() },
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
if (response.method === 'authenticator') {
|
||||||
|
self.enabled(false);
|
||||||
|
self.selectedMethod('authenticator');
|
||||||
|
self.qrCodeUrl(response.qrCodeUrl);
|
||||||
|
self.secret(response.secret);
|
||||||
|
self.showAuthenticatorSetup(true);
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(response.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.handleVerificationKeyup = function(data, event) {
|
||||||
|
// Auto-verify when 6 digits entered
|
||||||
|
if (self.verificationCode().length === 6 && event.keyCode === 13) {
|
||||||
|
self.verifyAuthenticatorSetup();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
ko.applyBindings(new TwoFactorViewModel());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -33,6 +33,131 @@
|
|||||||
<body class="animsition page-login-v3 layout-full">
|
<body class="animsition page-login-v3 layout-full">
|
||||||
<!-- Page -->
|
<!-- Page -->
|
||||||
<?php $this->load->view('specials/tellme_modal'); ?>
|
<?php $this->load->view('specials/tellme_modal'); ?>
|
||||||
|
|
||||||
|
<!-- Password Change Modal -->
|
||||||
|
<div class="modal fade" id="password-change-modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header bg-warning">
|
||||||
|
<h4 class="modal-title">
|
||||||
|
<i class="icon wb-warning" aria-hidden="true"></i>
|
||||||
|
Wachtwoord wijzigen verplicht
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>Let op:</strong> Uw huidige wachtwoord voldoet niet aan de beveiligingseisen.
|
||||||
|
U moet uw wachtwoord wijzigen om door te gaan.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h5>Wachtwoordeisen:</h5>
|
||||||
|
<ul class="list-unstyled" data-bind="foreach: passwordRequirements">
|
||||||
|
<li style="padding: 5px 0;">
|
||||||
|
<i class="icon wb-check text-success" aria-hidden="true"></i>
|
||||||
|
<span data-bind="text: $data"></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- New Password Input -->
|
||||||
|
<div class="form-group form-material floating" data-plugin="formMaterial" style="margin-top: 30px;">
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="newPassword"
|
||||||
|
data-bind="textInput: newPassword"
|
||||||
|
autocomplete="new-password" />
|
||||||
|
<label class="floating-label">Nieuw wachtwoord</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Password Input -->
|
||||||
|
<div class="form-group form-material floating" data-plugin="formMaterial">
|
||||||
|
<input type="password"
|
||||||
|
class="form-control"
|
||||||
|
id="confirmPassword"
|
||||||
|
data-bind="textInput: confirmPassword, event: { keyup: handlePasswordChangeKeyup }"
|
||||||
|
autocomplete="new-password" />
|
||||||
|
<label class="floating-label">Bevestig wachtwoord</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div data-bind="visible: passwordChangeError()" class="alert alert-danger" style="margin-top: 20px;">
|
||||||
|
<span data-bind="text: passwordChangeError"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary btn-lg btn-block"
|
||||||
|
data-bind="click: changePassword, enable: newPassword().length >= 8 && confirmPassword().length >= 8 && !isChangingPassword()">
|
||||||
|
<span data-bind="visible: !isChangingPassword()">Wachtwoord wijzigen</span>
|
||||||
|
<span data-bind="visible: isChangingPassword()">Wijzigen...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2FA Verification Modal -->
|
||||||
|
<div class="modal fade" id="twofactor-modal" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="false">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">
|
||||||
|
<i class="icon wb-lock" aria-hidden="true"></i>
|
||||||
|
Tweefactorauthenticatie
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body text-center">
|
||||||
|
<!-- Email Method -->
|
||||||
|
<div data-bind="visible: twoFactorMethod() === 'email'">
|
||||||
|
<div style="font-size: 48px; color: #348eda; margin-bottom: 20px;">
|
||||||
|
<i class="icon wb-envelope" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<p>Voer de 6-cijferige code in die naar uw e-mail is verzonden.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Authenticator Method -->
|
||||||
|
<div data-bind="visible: twoFactorMethod() === 'authenticator'">
|
||||||
|
<div style="font-size: 48px; color: #348eda; margin-bottom: 20px;">
|
||||||
|
<i class="icon wb-mobile" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<p>Voer de 6-cijferige code in van uw authenticator app.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code Input -->
|
||||||
|
<div class="form-group" style="margin-top: 30px;">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control text-center"
|
||||||
|
id="twoFactorCode"
|
||||||
|
maxlength="6"
|
||||||
|
placeholder="000000"
|
||||||
|
style="font-size: 32px; letter-spacing: 8px; font-weight: bold;"
|
||||||
|
data-bind="textInput: twoFactorCode, event: { keyup: handleTwoFactorKeyup }"
|
||||||
|
autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div data-bind="visible: twoFactorError()" class="alert alert-danger" style="margin-top: 20px;">
|
||||||
|
<span data-bind="text: twoFactorError"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resend Button (Email only) -->
|
||||||
|
<div data-bind="visible: twoFactorMethod() === 'email'" style="margin-top: 20px;">
|
||||||
|
<a href="#" data-bind="click: resendOtp, enable: !isResending()">
|
||||||
|
<span data-bind="visible: !isResending()">Code opnieuw verzenden</span>
|
||||||
|
<span data-bind="visible: isResending()">Verzenden...</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-bind="click: cancelTwoFactor">Annuleren</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" data-bind="click: verifyTwoFactor, enable: twoFactorCode().length === 6 && !isVerifying()">
|
||||||
|
<span data-bind="visible: !isVerifying()">Verifiëren</span>
|
||||||
|
<span data-bind="visible: isVerifying()">Verifiëren...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="page vertical-align text-center" data-animsition-in="fade-in" data-animsition-out="fade-out">
|
<div class="page vertical-align text-center" data-animsition-in="fade-in" data-animsition-out="fade-out">
|
||||||
<div class="page-content vertical-align-middle animation-slide-top animation-duration-1">
|
<div class="page-content vertical-align-middle animation-slide-top animation-duration-1">
|
||||||
<div class="panel" id="login-form">
|
<div class="panel" id="login-form">
|
||||||
@@ -116,6 +241,12 @@
|
|||||||
<script src="js/lib/modules/login/loginModule.js?v=<? $this->version ?>"></script>
|
<script src="js/lib/modules/login/loginModule.js?v=<? $this->version ?>"></script>
|
||||||
<script src="js/lib/modules/specials/tellmeModule.js?v=<? $this->version ?>"></script>
|
<script src="js/lib/modules/specials/tellmeModule.js?v=<? $this->version ?>"></script>
|
||||||
<script src="js/lib/helpers.js?v=<? $this->version ?>"></script>
|
<script src="js/lib/helpers.js?v=<? $this->version ?>"></script>
|
||||||
|
<script>
|
||||||
|
// Apply bindings for password change modal
|
||||||
|
$(function() {
|
||||||
|
ko.applyBindings(dbo.loginModel, document.getElementById('password-change-modal'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html class="no-js css-menubar" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=0, minimal-ui">
|
||||||
|
<title>Tweefactorauthenticatie - DBOIS</title>
|
||||||
|
|
||||||
|
<link rel="icon" href="css/images/dbo-01-80x80.png" type="image/png">
|
||||||
|
|
||||||
|
<!-- Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="css/vendor/bootstrap.min.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/vendor/bootstrap-extend.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/vendor/site.min.css?v=<?=$this->version?>">
|
||||||
|
|
||||||
|
<!-- Plugins -->
|
||||||
|
<link rel="stylesheet" href="css/vendor/plugins/asscrollable/asScrollable.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/vendor/plugins/switchery/switchery.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/vendor/plugins/slidepanel/slidePanel.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/vendor/plugins/flag-icon-css/flag-icon.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="css/login-v3.css?v=<?=$this->version?>">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="fonts/web-icons/web-icons.min.css?v=<?=$this->version?>">
|
||||||
|
<link rel="stylesheet" href="fonts/brand-icons/brand-icons.min.css?v=<?=$this->version?>">
|
||||||
|
<link rel='stylesheet' href='http://fonts.googleapis.com/css?family=Roboto:300,400,500,300italic'>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.verification-code-input {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32px;
|
||||||
|
letter-spacing: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.resend-link {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.method-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #348eda;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="animsition page-login-v3 layout-full">
|
||||||
|
<!-- Page -->
|
||||||
|
<?php $this->load->view('specials/tellme_modal'); ?>
|
||||||
|
<div class="page vertical-align text-center" data-animsition-in="fade-in" data-animsition-out="fade-out">
|
||||||
|
<div class="page-content vertical-align-middle animation-slide-top animation-duration-1">
|
||||||
|
<div class="panel" id="verify2fa-form">
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="brand">
|
||||||
|
<img class="brand-img" src="css/images/logo_dbo.png" alt="DBOIS">
|
||||||
|
<h2 class="brand-text font-size-18">projectbeheer</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="method-icon" data-bind="visible: method() === 'email'">
|
||||||
|
<i class="icon wb-envelope" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
<div class="method-icon" data-bind="visible: method() === 'authenticator'">
|
||||||
|
<i class="icon wb-mobile" aria-hidden="true"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Tweefactorauthenticatie</h3>
|
||||||
|
|
||||||
|
<p data-bind="visible: method() === 'email'">
|
||||||
|
Voer de 6-cijferige code in die naar uw e-mail is verzonden.
|
||||||
|
</p>
|
||||||
|
<p data-bind="visible: method() === 'authenticator'">
|
||||||
|
Voer de 6-cijferige code in van uw authenticator app.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-material floating mt-40" data-plugin="formMaterial">
|
||||||
|
<input type="text"
|
||||||
|
class="form-control verification-code-input"
|
||||||
|
name="verification_code"
|
||||||
|
maxlength="6"
|
||||||
|
data-bind="textInput: verificationCode, event: { keyup: handleKeyup }"
|
||||||
|
autocomplete="off"
|
||||||
|
autofocus />
|
||||||
|
<label class="floating-label">Verificatiecode</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-bind="visible: errorMessage()" class="alert alert-danger">
|
||||||
|
<span data-bind="text: errorMessage"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary btn-block btn-lg mt-40"
|
||||||
|
data-bind="click: verify, enable: verificationCode().length === 6 && !isVerifying()">
|
||||||
|
<span data-bind="visible: !isVerifying()">Verifiëren</span>
|
||||||
|
<span data-bind="visible: isVerifying()">Verifiëren...</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div data-bind="visible: method() === 'email'">
|
||||||
|
<a href="#" class="resend-link" data-bind="click: resendOtp, enable: !isResending()">
|
||||||
|
<span data-bind="visible: !isResending()">Code opnieuw verzenden</span>
|
||||||
|
<span data-bind="visible: isResending()">Verzenden...</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-20">
|
||||||
|
<a href="/login/logout">Annuleren</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="page-copyright page-copyright-inverse">
|
||||||
|
<p>© 2018. De Beleidsonderzoekers</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Page -->
|
||||||
|
|
||||||
|
<!-- Core -->
|
||||||
|
<script src="js/vendor/jquery/jquery.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/vendor/popper-js/umd/popper.min.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/vendor/bootstrap/bootstrap.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/vendor/animsition/animsition.js?v=<?=$this->version?>"></script>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="js/vendor/Config.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/vendor/Knockout.min.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/lib/modules/specials/tellmeModule.js?v=<?=$this->version?>"></script>
|
||||||
|
<script src="js/lib/helpers.js?v=<?=$this->version?>"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function Verify2FAViewModel() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.method = ko.observable('<?= $method ?>');
|
||||||
|
self.verificationCode = ko.observable('');
|
||||||
|
self.errorMessage = ko.observable('');
|
||||||
|
self.isVerifying = ko.observable(false);
|
||||||
|
self.isResending = ko.observable(false);
|
||||||
|
|
||||||
|
self.verify = function() {
|
||||||
|
if (self.verificationCode().length !== 6) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.errorMessage('');
|
||||||
|
self.isVerifying(true);
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/login/verify2fa',
|
||||||
|
method: 'POST',
|
||||||
|
data: { code: self.verificationCode() },
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
// Redirect to main application
|
||||||
|
window.location.href = '/';
|
||||||
|
} else {
|
||||||
|
self.errorMessage(response.message || 'Ongeldige verificatiecode');
|
||||||
|
self.verificationCode('');
|
||||||
|
}
|
||||||
|
self.isVerifying(false);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
self.errorMessage('Er is een fout opgetreden. Probeer het opnieuw.');
|
||||||
|
self.isVerifying(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.resendOtp = function() {
|
||||||
|
self.isResending(true);
|
||||||
|
self.errorMessage('');
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/login/resendOtp',
|
||||||
|
method: 'POST',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
alert(response.message || 'Nieuwe code verzonden');
|
||||||
|
} else {
|
||||||
|
self.errorMessage(response.message || 'Kan code niet opnieuw verzenden');
|
||||||
|
}
|
||||||
|
self.isResending(false);
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
self.errorMessage('Er is een fout opgetreden. Probeer het opnieuw.');
|
||||||
|
self.isResending(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.handleKeyup = function(data, event) {
|
||||||
|
// Clear error message on input
|
||||||
|
self.errorMessage('');
|
||||||
|
|
||||||
|
// Auto-submit when 6 digits entered and Enter is pressed
|
||||||
|
if (self.verificationCode().length === 6 && event.keyCode === 13) {
|
||||||
|
self.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow digits
|
||||||
|
var sanitized = self.verificationCode().replace(/[^0-9]/g, '');
|
||||||
|
if (sanitized !== self.verificationCode()) {
|
||||||
|
self.verificationCode(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
ko.applyBindings(new Verify2FAViewModel());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -15,6 +15,20 @@ $( function () {
|
|||||||
email : ko.observable(),
|
email : ko.observable(),
|
||||||
showPasswordRequest: ko.observable( false ),
|
showPasswordRequest: ko.observable( false ),
|
||||||
|
|
||||||
|
// 2FA properties
|
||||||
|
twoFactorMethod : ko.observable( '' ),
|
||||||
|
twoFactorCode : ko.observable( '' ),
|
||||||
|
twoFactorError : ko.observable( '' ),
|
||||||
|
isVerifying : ko.observable( false ),
|
||||||
|
isResending : ko.observable( false ),
|
||||||
|
|
||||||
|
// Password change properties
|
||||||
|
newPassword : ko.observable( '' ),
|
||||||
|
confirmPassword : ko.observable( '' ),
|
||||||
|
passwordRequirements : ko.observableArray( [] ),
|
||||||
|
passwordChangeError : ko.observable( '' ),
|
||||||
|
isChangingPassword : ko.observable( false ),
|
||||||
|
|
||||||
login: function () {
|
login: function () {
|
||||||
dbo.loginService.login();
|
dbo.loginService.login();
|
||||||
},
|
},
|
||||||
@@ -26,10 +40,64 @@ $( function () {
|
|||||||
togglePasswordrequest: function () {
|
togglePasswordrequest: function () {
|
||||||
var vm = dbo.loginModel
|
var vm = dbo.loginModel
|
||||||
vm.showPasswordRequest( !vm.showPasswordRequest() );
|
vm.showPasswordRequest( !vm.showPasswordRequest() );
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyTwoFactor: function () {
|
||||||
|
dbo.loginService.verifyTwoFactor();
|
||||||
|
},
|
||||||
|
|
||||||
|
resendOtp: function () {
|
||||||
|
dbo.loginService.resendOtp();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelTwoFactor: function () {
|
||||||
|
$('#twofactor-modal').modal('hide');
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
vm.twoFactorCode( '' );
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
vm.twoFactorMethod( '' );
|
||||||
|
},
|
||||||
|
|
||||||
|
handleTwoFactorKeyup: function (data, event) {
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
// Clear error on input
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
|
||||||
|
// Only allow digits
|
||||||
|
var sanitized = vm.twoFactorCode().replace(/[^0-9]/g, '');
|
||||||
|
if (sanitized !== vm.twoFactorCode()) {
|
||||||
|
vm.twoFactorCode(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-submit on Enter key when code is 6 digits
|
||||||
|
if (vm.twoFactorCode().length === 6 && event.keyCode === 13) {
|
||||||
|
vm.verifyTwoFactor();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: function () {
|
||||||
|
dbo.loginService.changePassword();
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePasswordChangeKeyup: function (data, event) {
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
// Clear error on input
|
||||||
|
vm.passwordChangeError( '' );
|
||||||
|
|
||||||
|
// Auto-submit on Enter key
|
||||||
|
if (event.keyCode === 13 && vm.newPassword().length >= 8 && vm.confirmPassword().length >= 8) {
|
||||||
|
vm.changePassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ko.applyBindings( dbo.loginModel, document.getElementById( "login-form" ) );
|
ko.applyBindings( dbo.loginModel, document.getElementById( "login-form" ) );
|
||||||
|
ko.applyBindings( dbo.loginModel, document.getElementById( "twofactor-modal" ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
dbo.loginService = {
|
dbo.loginService = {
|
||||||
@@ -43,6 +111,35 @@ dbo.loginService = {
|
|||||||
if ( ! submitDATA ) return self.notifyWrongPassword();
|
if ( ! submitDATA ) return self.notifyWrongPassword();
|
||||||
|
|
||||||
ds.login( submitDATA, function ( data ) {
|
ds.login( submitDATA, function ( data ) {
|
||||||
|
// Check if password change is required
|
||||||
|
if ( data && typeof data === 'object' && data.result === 'PASSWORD-CHANGE-REQUIRED' ) {
|
||||||
|
// Show password change modal
|
||||||
|
vm.passwordRequirements( data.requirements || [] );
|
||||||
|
vm.newPassword( '' );
|
||||||
|
vm.confirmPassword( '' );
|
||||||
|
vm.passwordChangeError( '' );
|
||||||
|
$('#password-change-modal').modal('show');
|
||||||
|
// Focus on new password input after modal is shown
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#newPassword').focus();
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if 2FA is required
|
||||||
|
if ( data && typeof data === 'object' && data.result === '2FA-REQUIRED' ) {
|
||||||
|
// Show 2FA modal
|
||||||
|
vm.twoFactorMethod( data.method );
|
||||||
|
vm.twoFactorCode( '' );
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
$('#twofactor-modal').modal('show');
|
||||||
|
// Focus on code input after modal is shown
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#twoFactorCode').focus();
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ( data ) return location.reload();
|
if ( data ) return location.reload();
|
||||||
|
|
||||||
self.notifyWrongPassword();
|
self.notifyWrongPassword();
|
||||||
@@ -50,6 +147,102 @@ dbo.loginService = {
|
|||||||
} );
|
} );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
verifyTwoFactor: function () {
|
||||||
|
var ds = dbo.loginDataservice;
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
|
||||||
|
if ( vm.twoFactorCode().length !== 6 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.isVerifying( true );
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
|
||||||
|
ds.verifyTwoFactor( vm.twoFactorCode(), function ( response ) {
|
||||||
|
vm.isVerifying( false );
|
||||||
|
|
||||||
|
if ( response.success ) {
|
||||||
|
// Close modal and reload page
|
||||||
|
$('#twofactor-modal').modal('hide');
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
vm.twoFactorError( response.message || 'Ongeldige verificatiecode' );
|
||||||
|
vm.twoFactorCode( '' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
resendOtp: function () {
|
||||||
|
var ds = dbo.loginDataservice;
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
|
||||||
|
vm.isResending( true );
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
|
||||||
|
ds.resendOtp( function ( response ) {
|
||||||
|
vm.isResending( false );
|
||||||
|
|
||||||
|
if ( response.success ) {
|
||||||
|
tellMe( {
|
||||||
|
title : 'Code verzonden',
|
||||||
|
message : response.message || 'Nieuwe code verzonden',
|
||||||
|
modalClass: 'modal-success'
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
vm.twoFactorError( response.message || 'Kan code niet opnieuw verzenden' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: function () {
|
||||||
|
var ds = dbo.loginDataservice;
|
||||||
|
var vm = dbo.loginModel;
|
||||||
|
|
||||||
|
// Validate passwords match
|
||||||
|
if ( vm.newPassword() !== vm.confirmPassword() ) {
|
||||||
|
vm.passwordChangeError( 'Wachtwoorden komen niet overeen' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password length
|
||||||
|
if ( vm.newPassword().length < 8 ) {
|
||||||
|
vm.passwordChangeError( 'Wachtwoord moet minimaal 8 karakters bevatten' );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.isChangingPassword( true );
|
||||||
|
vm.passwordChangeError( '' );
|
||||||
|
|
||||||
|
var submitDATA = {
|
||||||
|
new_password: vm.newPassword(),
|
||||||
|
confirm_password: vm.confirmPassword()
|
||||||
|
};
|
||||||
|
|
||||||
|
ds.changePassword( submitDATA, function ( response ) {
|
||||||
|
vm.isChangingPassword( false );
|
||||||
|
|
||||||
|
if ( response.success ) {
|
||||||
|
// Close password change modal
|
||||||
|
$('#password-change-modal').modal('hide');
|
||||||
|
|
||||||
|
// Check if 2FA is required after password change
|
||||||
|
if ( response.result === '2FA-REQUIRED' ) {
|
||||||
|
vm.twoFactorMethod( response.method );
|
||||||
|
vm.twoFactorCode( '' );
|
||||||
|
vm.twoFactorError( '' );
|
||||||
|
$('#twofactor-modal').modal('show');
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#twoFactorCode').focus();
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vm.passwordChangeError( response.message || 'Wachtwoord wijzigen mislukt' );
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
requestPassword: function () {
|
requestPassword: function () {
|
||||||
var ds = dbo.loginDataservice;
|
var ds = dbo.loginDataservice;
|
||||||
var self = dbo.loginService;
|
var self = dbo.loginService;
|
||||||
@@ -147,6 +340,24 @@ dbo.loginDataservice = {
|
|||||||
$.post( 'index.php/login/requestPassword', submitDATA, function ( data ) {
|
$.post( 'index.php/login/requestPassword', submitDATA, function ( data ) {
|
||||||
callback( data );
|
callback( data );
|
||||||
} );
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyTwoFactor: function ( code, callback ) {
|
||||||
|
$.post( 'index.php/login/verify2fa', { code: code }, function ( data ) {
|
||||||
|
callback( data );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
resendOtp: function ( callback ) {
|
||||||
|
$.post( 'index.php/login/resendOtp', {}, function ( data ) {
|
||||||
|
callback( data );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
changePassword: function ( submitDATA, callback ) {
|
||||||
|
$.post( 'index.php/login/changeWeakPassword', submitDATA, function ( data ) {
|
||||||
|
callback( data );
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user