Twofactor authentication and strong password

This commit is contained in:
2025-10-17 14:52:45 +02:00
parent 71a688f97f
commit dbd8c8aed2
17 changed files with 3055 additions and 14 deletions
+741
View File
@@ -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
+342
View File
@@ -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
+13 -5
View File
@@ -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;
+239 -1
View File
@@ -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);
}
}
+5 -8
View File
@@ -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' ),
+59
View File
@@ -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.)'
];
}
}
+193
View File
@@ -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);
}
}
+15
View File
@@ -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>
+211
View File
@@ -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 );
} );
} }
}; };