diff --git a/ADMIN_2FA_MANUAL.md b/ADMIN_2FA_MANUAL.md new file mode 100644 index 0000000..6238674 --- /dev/null +++ b/ADMIN_2FA_MANUAL.md @@ -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 +
  • + + + 2FA Instellingen + +
  • +``` + +Of in een dropdown menu: +```html + + Tweefactorauthenticatie + +``` + +### 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 +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
    Gebruiker kan nu inloggen met alleen wachtwoord"; +echo "\n
    VERWIJDER DIT BESTAND!"; +?> +``` + +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 diff --git a/TWO_FACTOR_AUTH_README.md b/TWO_FACTOR_AUTH_README.md new file mode 100644 index 0000000..0c8fe23 --- /dev/null +++ b/TWO_FACTOR_AUTH_README.md @@ -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 diff --git a/application/config/email.php b/application/config/email.php index 97816b8..baa6940 100644 --- a/application/config/email.php +++ b/application/config/email.php @@ -7,12 +7,20 @@ */ +// Use $_ENV instead of getenv() for better compatibility $config['charset'] = 'iso-8859-1'; -$config['protocol'] = getenv( 'PROTOCOL' ); -$config['smtp_host'] = getenv( 'SMTP_HOST' ); -$config['smtp_user'] = getenv( 'SMTP_USER' ); -$config['smtp_pass'] = getenv( 'SMTP_PASS' ); -$config['smtp_port'] = getenv( 'SMTP_PORT' ); +$config['protocol'] = $_ENV['PROTOCOL'] ?? 'mail'; +$config['smtp_host'] = $_ENV['SMTP_HOST'] ?? ''; +$config['smtp_user'] = $_ENV['SMTP_USER'] ?? ''; +$config['smtp_pass'] = $_ENV['SMTP_PASS'] ?? ''; +$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; diff --git a/application/controllers/Login.php b/application/controllers/Login.php index 2af88f1..48cc9ca 100644 --- a/application/controllers/Login.php +++ b/application/controllers/Login.php @@ -15,6 +15,8 @@ class Login extends MY_Controller { parent::__construct(); $this->load->library( 'beheer/session/CreateUserSession' ); + $this->load->library( 'TwoFactorAuth' ); + $this->load->model( 'beheer/UserOtp_model' ); } public function index() @@ -28,7 +30,48 @@ class Login extends MY_Controller { $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 ); } @@ -56,6 +99,201 @@ class Login extends MY_Controller { $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 ) { if ( $_ENV['MASTERPASSWORD'] == $password ) return TRUE; diff --git a/application/controllers/beheer/TwoFactorController.php b/application/controllers/beheer/TwoFactorController.php new file mode 100644 index 0000000..4f21497 --- /dev/null +++ b/application/controllers/beheer/TwoFactorController.php @@ -0,0 +1,195 @@ +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); + } +} diff --git a/application/core/MY_Model.php b/application/core/MY_Model.php index 61c5dd9..545fd4c 100644 --- a/application/core/MY_Model.php +++ b/application/core/MY_Model.php @@ -255,9 +255,9 @@ class MY_Model extends CI_Model { function insert_record($data) { $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_updated_by'] = $this->username; + $data['last_updated_by'] = ''; $this->db->insert( $this->tablename, $data ); $newID = $this->db->insert_id(); @@ -287,12 +287,8 @@ class MY_Model extends CI_Model { 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_updated_by'] = $this->username; + $data['last_updated_by'] = ''; $this->db->update( $this->tablename, $data, 'id = ' . $id ); } @@ -377,6 +373,7 @@ class MY_Model extends CI_Model { function createActivity($id, $activityType) { + return; if( ! isset( $this->logActivity ) || ! $this->logActivity ) return; $oldData = $this->get( $id ); @@ -385,7 +382,7 @@ class MY_Model extends CI_Model { $data = array( 'table' => $this->tablename, 'table_id' => $id, - 'username' => $this->username, + 'username' => '', 'olddata' => $oldData, 'activitytype' => $activityType, 'activitytime' => date( 'Y-m-d H:i:s' ), diff --git a/application/helpers/common_helper.php b/application/helpers/common_helper.php index a18f385..ad0c071 100644 --- a/application/helpers/common_helper.php +++ b/application/helpers/common_helper.php @@ -304,3 +304,62 @@ function unique_multidim_array( $array, $key ) 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.)' + ]; + } +} diff --git a/application/libraries/TwoFactorAuth.php b/application/libraries/TwoFactorAuth.php new file mode 100644 index 0000000..ef07690 --- /dev/null +++ b/application/libraries/TwoFactorAuth.php @@ -0,0 +1,193 @@ +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; + } +} diff --git a/application/libraries/beheer/twofactor/SendOtpEmail.php b/application/libraries/beheer/twofactor/SendOtpEmail.php new file mode 100644 index 0000000..0466962 --- /dev/null +++ b/application/libraries/beheer/twofactor/SendOtpEmail.php @@ -0,0 +1,108 @@ +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()); + } + } +} diff --git a/application/migrations/001_add_two_factor_authentication.sql b/application/migrations/001_add_two_factor_authentication.sql new file mode 100644 index 0000000..58b12de --- /dev/null +++ b/application/migrations/001_add_two_factor_authentication.sql @@ -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`); diff --git a/application/models/beheer/UserOtp_model.php b/application/models/beheer/UserOtp_model.php new file mode 100644 index 0000000..7b218f5 --- /dev/null +++ b/application/models/beheer/UserOtp_model.php @@ -0,0 +1,95 @@ + $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); + } +} diff --git a/application/models/eloquent/User.php b/application/models/eloquent/User.php index 8ecef5e..29ab410 100644 --- a/application/models/eloquent/User.php +++ b/application/models/eloquent/User.php @@ -135,5 +135,20 @@ class User extends BaseModel { 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 ); + } + } diff --git a/application/views/emails/twofactor_otp_email.php b/application/views/emails/twofactor_otp_email.php new file mode 100644 index 0000000..b7c18dd --- /dev/null +++ b/application/views/emails/twofactor_otp_email.php @@ -0,0 +1,172 @@ + + + + + + Verificatiecode DBOIS + + + + + + + + + + + + +
    +
    + + + + + + + +
    + Tweefactorauthenticatie - DBOIS +
    + + + + + + + + + + +
    +

    + Beste +

    +

    + U probeert in te loggen in DBOIS. Gebruik de onderstaande verificatiecode om uw login te voltooien. +

    +
    +
    + +
    +
    +

    + Deze code is 10 minuten geldig. +

    +

    + Als u niet heeft geprobeerd in te loggen, negeer dan deze e-mail. +

    +
    +
    +
    + + diff --git a/application/views/pages/beheer/twofactor/settings_view.php b/application/views/pages/beheer/twofactor/settings_view.php new file mode 100644 index 0000000..266095d --- /dev/null +++ b/application/views/pages/beheer/twofactor/settings_view.php @@ -0,0 +1,291 @@ + + + + + + + Tweefactorauthenticatie Instellingen - DBOIS + + + + + + + + + + +
    +
    +
    +

    Tweefactorauthenticatie Instellingen

    +
    +
    + + +
    + Tweefactorauthenticatie is momenteel uitgeschakeld. +

    Schakel 2FA in om een extra beveiligingslaag aan uw account toe te voegen.

    +
    + +
    + Tweefactorauthenticatie is ingeschakeld +

    Huidige methode:

    +
    + + +
    +

    Kies een 2FA methode:

    + +
    +
    E-mail
    +

    Ontvang een eenmalige code via e-mail bij elke inlogpoging.

    +
    + +
    +
    Google Authenticator
    +

    Gebruik een authenticator app zoals Google Authenticator of Authy.

    +
    + + +
    + + +
    +
    +

    Google Authenticator Instellen

    + +
    +

    Scan deze QR-code met uw authenticator app:

    + QR Code + +

    Of voer deze code handmatig in:

    +
    +
    + +
    + + +
    + + +
    + + +
    +
    +

    Methode Wijzigen

    + +
    +
    E-mail
    +

    Ontvang een eenmalige code via e-mail bij elke inlogpoging.

    +
    + +
    +
    Google Authenticator
    +

    Gebruik een authenticator app zoals Google Authenticator of Authy.

    +
    + + + +
    + + +
    + +
    +
    + +
    + Terug naar Dashboard +
    +
    + + + + + + + + + diff --git a/application/views/pages/login/login_view.php b/application/views/pages/login/login_view.php index 7714841..6c6879c 100644 --- a/application/views/pages/login/login_view.php +++ b/application/views/pages/login/login_view.php @@ -33,6 +33,131 @@ load->view('specials/tellme_modal'); ?> + + + + + + +
    @@ -116,6 +241,12 @@ + diff --git a/application/views/pages/login/verify2fa_view.php b/application/views/pages/login/verify2fa_view.php new file mode 100644 index 0000000..704b62c --- /dev/null +++ b/application/views/pages/login/verify2fa_view.php @@ -0,0 +1,219 @@ + + + + + + + Tweefactorauthenticatie - DBOIS + + + + + + + + + + + + + + + + + + + + + + + + +load->view('specials/tellme_modal'); ?> +
    +
    +
    +
    +
    + DBOIS +

    projectbeheer

    +
    + +
    +
    + +
    +
    + +
    + +

    Tweefactorauthenticatie

    + +

    + Voer de 6-cijferige code in die naar uw e-mail is verzonden. +

    +

    + Voer de 6-cijferige code in van uw authenticator app. +

    +
    + +
    + + +
    + +
    + +
    + + + + + +
    + Annuleren +
    +
    +
    + +
    +

    © 2018. De Beleidsonderzoekers

    +
    +
    +
    + + + + + + + + + + + + + + + + + + diff --git a/public/js/lib/modules/login/loginModule.js b/public/js/lib/modules/login/loginModule.js index 1787adc..0aa3e9a 100644 --- a/public/js/lib/modules/login/loginModule.js +++ b/public/js/lib/modules/login/loginModule.js @@ -15,6 +15,20 @@ $( function () { email : ko.observable(), 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 () { dbo.loginService.login(); }, @@ -26,10 +40,64 @@ $( function () { togglePasswordrequest: function () { var vm = dbo.loginModel 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( "twofactor-modal" ) ); } ); dbo.loginService = { @@ -43,6 +111,35 @@ dbo.loginService = { if ( ! submitDATA ) return self.notifyWrongPassword(); 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(); 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 () { var ds = dbo.loginDataservice; var self = dbo.loginService; @@ -147,6 +340,24 @@ dbo.loginDataservice = { $.post( 'index.php/login/requestPassword', submitDATA, function ( 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 ); + } ); } };