Files
dbois/app/Controllers/Login.php
T

335 lines
9.8 KiB
PHP

<?php
namespace App\Controllers;
use App\Libraries\Beheer\Session\CreateUserSession;
use App\Libraries\TwoFactorAuth;
class Login extends BaseController
{
protected CreateUserSession $createUserSession;
protected TwoFactorAuth $twoFactorAuth;
public function __construct()
{
$this->createUserSession = new CreateUserSession(\Config\Services::session());
$this->twoFactorAuth = new TwoFactorAuth();
}
public function index()
{
$username = $this->cleanInput('username');
$password = $this->cleanInput('password');
$user = \User::where('username', $username)->first();
if (!$user) {
$this->logLoginFailed($username);
return $this->json(false);
}
$passwordCheck = $this->checkPassword($user, $password);
if ($passwordCheck === true) {
// Check if password meets security requirements
if (!validate_password_strength($password)) {
// Store user ID for password change flow
$this->session->set('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('pending_2fa_user_id', $user->id);
$this->session->set('pending_2fa_method', $twoFactorMethod);
// If email method, send OTP
if ($twoFactorMethod === 'email') {
$sender = new \SendOtpEmail($user);
if (!$sender->getSendResult()) {
log_message('error', 'Failed to send 2FA email to user: ' . $user->id);
}
}
// Return response indicating 2FA is required
return $this->json([
'result' => '2FA-REQUIRED',
'method' => $twoFactorMethod
]);
}
$this->logLoginFailed($username);
return $this->json($passwordCheck);
}
public function requestPassword()
{
$username = $this->request->getPost('username');
$email = $this->request->getPost('email');
$user = \User::where('username', $username)->first();
if (!$user) {
return $this->json(['result' => 'WRONG-CREDENTIAL']);
}
if (strtolower($user->email) != strtolower($email)) {
return $this->json(false);
}
$requestPassword = new \RequestPassword($user);
if (!$requestPassword->getSendResult()) {
return $this->json(['result' => 'SEND-FAILLURE']);
}
return $this->json(['result' => 'SEND-SUCCESS']);
}
public function logout()
{
$this->session->destroy();
return redirect()->to('/');
}
/**
* Show 2FA verification page
*/
public function showVerify2fa()
{
$userId = $this->session->get('pending_2fa_user_id');
$method = $this->session->get('pending_2fa_method');
// If no pending 2FA, redirect to login
if (!$userId || !$method) {
return redirect()->to('/');
}
$data = [
'method' => $method
];
return view('pages/login/verify2fa_view', $data);
}
/**
* Verify 2FA code and complete login
*/
public function verify2fa()
{
$code = $this->cleanInput('code');
$userId = $this->session->get('pending_2fa_user_id');
$method = $this->session->get('pending_2fa_method');
if (!$userId || !$method) {
return $this->json([
'success' => false,
'message' => 'Geen 2FA verificatie in behandeling'
]);
}
$user = \User::find($userId);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'Gebruiker niet gevonden'
]);
}
$isValid = false;
// Verify based on method
if ($method === 'email') {
$isValid = \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->remove('pending_2fa_user_id');
$this->session->remove('pending_2fa_method');
// Create full user session
$this->createUserSession->handle($user);
return $this->json([
'success' => true,
'message' => 'Succesvol geverifieerd'
]);
} else {
$this->logLoginFailed($user->username ?: $userId . ':2fa');
return $this->json([
'success' => false,
'message' => 'Ongeldige verificatiecode'
]);
}
}
/**
* Resend OTP code for email method
*/
public function resendOtp()
{
$userId = $this->session->get('pending_2fa_user_id');
$method = $this->session->get('pending_2fa_method');
if (!$userId || $method !== 'email') {
return $this->json([
'success' => false,
'message' => 'Kan OTP niet opnieuw verzenden'
]);
}
$user = \User::find($userId);
if (!$user) {
return $this->json([
'success' => false,
'message' => 'Gebruiker niet gevonden'
]);
}
// Send new OTP
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->get('pending_password_change_user_id');
$newPassword = $this->cleanInput('new_password');
$confirmPassword = $this->cleanInput('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 = \User::find($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->remove('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('pending_2fa_user_id', $user->id);
$this->session->set('pending_2fa_method', $twoFactorMethod);
// If email method, send OTP
if ($twoFactorMethod === 'email') {
$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, string $password): bool
{
if (($_ENV['MASTERPASSWORD'] ?? '') == $password) {
return true;
}
$saltedPassword = \User::getSaltedPassword($password, $user->salt);
if ($saltedPassword == $user->password) {
return true;
}
return false;
}
/**
* Log a rejected password or 2FA code in the format parsed by the
* fail2ban jail `app-login` and the security report collector:
* "LOGIN_FAILED user=<name> ip=<ip>". Never log the password or OTP.
*
* The IP is the real client IP only while the app runs directly behind
* Apache. If Config\App::$proxyIPs is ever set, getIPAddress() resolves
* the forwarded client IP and keeps this correct.
*/
private function logLoginFailed(string $user): void
{
log_message('warning', 'LOGIN_FAILED user={user} ip={ip}', [
'user' => sanitize_log_user($user),
'ip' => $this->request->getIPAddress(),
]);
}
}