Add two-factor authentication support for users and subusers
- Extend `users_o` and `subusers_o` with `two_factor_enabled` and `two_factor_secret` properties. - Implement methods for managing 2FA (`isTwoFactorEnabled`, `setTwoFactorSecret`, `verify_2fa_code`) in authentication logic. - Add 2FA handling in login flows for both users and subusers, including token generation and validation. - Introduce `totp` class for TOTP-based authentication, including QR code generation and code verification. - Add test cases for 2FA functionality (`TwoFactorAuthTest.php`) and coverage for login scenarios with 2FA. - Update OpenAPI specifications to include 2FA flows (`auth/2fa/setup`, `auth/2fa/enable`, `auth/2fa/verify`, `auth/2fa/disable`).
This commit is contained in:
@@ -6,12 +6,14 @@ use classes\authentication;
|
||||
use classes\economic;
|
||||
use classes\email;
|
||||
use classes\recaptcha;
|
||||
use classes\totp;
|
||||
use classes\virkdata;
|
||||
use Exception;
|
||||
use objects\customer_password_reset_keys_o;
|
||||
use objects\logs_o;
|
||||
use objects\tokens_o;
|
||||
use objects\users_o;
|
||||
use objects\subusers_o;
|
||||
use traits\route_t;
|
||||
|
||||
class authRoute
|
||||
@@ -52,6 +54,12 @@ class authRoute
|
||||
$response->error('Invalid credentials', 401);
|
||||
}
|
||||
// If the credentials are valid, create a token
|
||||
$user = (new users_o())->getUserByCustomerNumber($data['customer_number']);
|
||||
if ($user->isTwoFactorEnabled()) {
|
||||
$token = (new authentication())->create_2fa_token($user->id, '2FA_VERIFICATION_USER');
|
||||
$response->success(['2fa_required' => true, '2fa_token' => $token]);
|
||||
}
|
||||
|
||||
$token = (new authentication())->create_token($data['customer_number']);
|
||||
// Return the token
|
||||
$response->success(['token' => $token]);
|
||||
@@ -89,10 +97,111 @@ class authRoute
|
||||
if (!$user) {
|
||||
$response->error('User not found', 400);
|
||||
}
|
||||
|
||||
$user_data = $user->includeIncludes(['economicCustomer', 'permissions'])->asArray();
|
||||
$user_data['two_factor_enabled'] = $user->isTwoFactorEnabled();
|
||||
|
||||
// Return the (session) user object
|
||||
$response->success(
|
||||
($user->includeIncludes(['economicCustomer', 'permissions'])->asArray())
|
||||
);
|
||||
$response->success($user_data);
|
||||
});
|
||||
|
||||
$this->post('/auth/2fa/setup', function () {
|
||||
global $response;
|
||||
$auth = new authentication();
|
||||
$user = $auth->get_user();
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($user === false && $subuser === false) {
|
||||
$response->error('Unauthorized', 401);
|
||||
}
|
||||
|
||||
$principal = $user ?: $subuser;
|
||||
$totp = new totp();
|
||||
$secret = $totp->generateSecret();
|
||||
$principal->setTwoFactorSecret($secret);
|
||||
|
||||
$name = $user ? $principal->customer_number->value() : $principal->username->value();
|
||||
$qrCodeUrl = $totp->getQrCodeUrl($secret, $name, 'Truck Wash');
|
||||
|
||||
$response->success([
|
||||
'secret' => $secret,
|
||||
'qr_code_url' => $qrCodeUrl
|
||||
]);
|
||||
});
|
||||
|
||||
$this->post('/auth/2fa/enable', function () {
|
||||
global $response;
|
||||
$auth = new authentication();
|
||||
$user = $auth->get_user();
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($user === false && $subuser === false) {
|
||||
$response->error('Unauthorized', 401);
|
||||
}
|
||||
|
||||
self::requireParameters(['code']);
|
||||
$code = (string)self::getParameter('code');
|
||||
|
||||
$principal = $user ?: $subuser;
|
||||
if ($auth->verify_2fa_code($principal, $code)) {
|
||||
$principal->setTwoFactorEnabled(true);
|
||||
$response->success(['message' => '2FA enabled successfully']);
|
||||
} else {
|
||||
$response->error('Invalid 2FA code', 400);
|
||||
}
|
||||
});
|
||||
|
||||
$this->post('/auth/2fa/disable', function () {
|
||||
global $response;
|
||||
$auth = new authentication();
|
||||
$user = $auth->get_user();
|
||||
$subuser = $auth->get_subuser();
|
||||
if ($user === false && $subuser === false) {
|
||||
$response->error('Unauthorized', 401);
|
||||
}
|
||||
|
||||
self::requireParameters(['code']);
|
||||
$code = (string)self::getParameter('code');
|
||||
|
||||
$principal = $user ?: $subuser;
|
||||
if ($auth->verify_2fa_code($principal, $code)) {
|
||||
$principal->setTwoFactorEnabled(false);
|
||||
$principal->setTwoFactorSecret(null);
|
||||
$response->success(['message' => '2FA disabled successfully']);
|
||||
} else {
|
||||
$response->error('Invalid 2FA code', 400);
|
||||
}
|
||||
});
|
||||
|
||||
$this->post('/auth/2fa/verify', function () {
|
||||
global $response;
|
||||
self::requireParameters(['2fa_token', 'code']);
|
||||
$token_str = (string)self::getParameter('2fa_token');
|
||||
$code = (string)self::getParameter('code');
|
||||
|
||||
$token_o = new tokens_o();
|
||||
try {
|
||||
$token = $token_o->getToken($token_str);
|
||||
} catch (Exception $e) {
|
||||
$response->error('Invalid or expired 2FA token', 401);
|
||||
}
|
||||
|
||||
$auth = new authentication();
|
||||
if ($token->type->value() === '2FA_VERIFICATION_USER') {
|
||||
$user = (new users_o())->getUserById($token->user_id->value());
|
||||
if ($auth->verify_2fa_code($user, $code)) {
|
||||
$token_o->delete($token_str);
|
||||
$new_token = $auth->create_employee_token($user->id); // Works for both users and employees
|
||||
$response->success(['token' => $new_token]);
|
||||
}
|
||||
} elseif ($token->type->value() === '2FA_VERIFICATION_SUBUSER') {
|
||||
$subuser = (new subusers_o())->select($token->user_id->value());
|
||||
if ($auth->verify_2fa_code($subuser, $code)) {
|
||||
$token_o->delete($token_str);
|
||||
$new_token = $subuser->generateSession();
|
||||
$response->success(['session' => $new_token]);
|
||||
}
|
||||
}
|
||||
|
||||
$response->error('Invalid 2FA code', 400);
|
||||
});
|
||||
|
||||
$this->post('/auth/employee/login', function () {
|
||||
@@ -119,6 +228,13 @@ class authRoute
|
||||
(new logs_o())->add('auth', 'global', 1, 0, 'AUTH_FAILURE', 'Employee number: ' . $data['user_id']);
|
||||
$response->error('Invalid credentials', 401);
|
||||
}
|
||||
|
||||
$user = (new users_o())->getUserById($data['user_id']);
|
||||
if ($user->isTwoFactorEnabled()) {
|
||||
$token = (new authentication())->create_2fa_token($user->id, '2FA_VERIFICATION_USER');
|
||||
$response->success(['2fa_required' => true, '2fa_token' => $token]);
|
||||
}
|
||||
|
||||
// If the credentials are valid, create a token
|
||||
$token = (new authentication())->create_employee_token($data['user_id']);
|
||||
// Return the token
|
||||
|
||||
Reference in New Issue
Block a user