Improvements of the 2FA validation code UI

This commit is contained in:
Pinga 2024-02-26 14:51:36 +02:00
parent b46ff2286c
commit 6515bcda80
5 changed files with 162 additions and 29 deletions

View file

@ -142,25 +142,31 @@ class Auth
$auth->login($email, $password, $rememberDuration); $auth->login($email, $password, $rememberDuration);
// check if a valid code is provided
global $container; global $container;
$db = $container->get('db'); $db = $container->get('db');
$tfa = $db->selectRow('SELECT tfa_enabled, tfa_secret FROM users WHERE id = ?', [$auth->getUserId()]); $tfa_secret = $db->selectValue('SELECT tfa_secret FROM users WHERE id = ?', [$auth->getUserId()]);
if ($tfa) { if (!is_null($tfa_secret)) {
if ($tfa['tfa_enabled'] == 1) { if (!is_null($code) && $code !== "" && preg_match('/^\d{6}$/', $code)) {
$tfaService = new \RobThree\Auth\TwoFactorAuth('Namingo'); // If tfa_secret exists and is not empty, verify the 2FA code
if ($tfaService->verifyCode($tfa['tfa_secret'], $code) === true) { $tfaService = new \RobThree\Auth\TwoFactorAuth('Namingo');
return true; if ($tfaService->verifyCode($tfa_secret, $code) === true) {
} else { // 2FA verification successful
self::$auth->logOut(); return true;
redirect()->route('login')->with('error','Incorrect 2FA Code. Please check and enter the correct code. 2FA codes are time-sensitive. For continuous issues, contact support.'); } else {
} // 2FA verification failed
} elseif ($tfa['tfa_enabled'] == 0) { self::$auth->logOut();
return true; redirect()->route('login')->with('error','Incorrect 2FA Code. Please check and enter the correct code. 2FA codes are time-sensitive. For continuous issues, contact support.');
//return false; // Ensure to return false or handle accordingly
}
} else {
self::$auth->logOut();
redirect()->route('login')->with('error','2FA Code Required. Please enter your 6-digit 2FA code to proceed with the login.');
//return false;
} }
} else { } else {
self::$auth->logOut(); return true;
redirect()->route('login')->with('error','Temporary Database Issue. Please try again shortly. If this problem persists, kindly reach out to our support team for assistance.');
} }
} }
catch (InvalidEmailException $e) { catch (InvalidEmailException $e) {

View file

@ -33,6 +33,21 @@ class AuthController extends Controller
public function createLogin(Request $request, Response $response){ public function createLogin(Request $request, Response $response){
return view($response,'auth/login.twig'); return view($response,'auth/login.twig');
} }
/**
* Show 2FA verification form.
*
* @param Request $request
* @param Response $response
* @return mixed
*/
public function verify2FA(Request $request, Response $response){
if (isset($_SESSION['is2FAEnabled']) && $_SESSION['is2FAEnabled'] === true) {
return view($response, 'auth/verify2fa.twig');
} else {
return $response->withHeader('Location', '/login')->withStatus(302);
}
}
/** /**
* @param Request $request * @param Request $request
@ -42,20 +57,34 @@ class AuthController extends Controller
*/ */
public function login(Request $request, Response $response){ public function login(Request $request, Response $response){
global $container; global $container;
$data = $request->getParsedBody(); $data = $request->getParsedBody();
if(isset($data['remember'])){ $db = $container->get('db');
$remember = $data['remember']; $is2FAEnabled = $db->selectValue('SELECT tfa_enabled, tfa_secret FROM users WHERE email = ?', [$data['email']]);
}else{
$remember = null; // If 2FA is enabled and no code is provided, redirect to 2FA code entry
if($is2FAEnabled && !isset($data['code'])) {
$_SESSION['2fa_email'] = $data['email'];
$_SESSION['2fa_password'] = $data['password'];
$_SESSION['is2FAEnabled'] = true;
return $response->withHeader('Location', '/login/verify')->withStatus(302);
} else {
$email = $data['email'];
$password = $data['password'];
$_SESSION['is2FAEnabled'] = false;
} }
if(isset($data['code'])){
$code = $data['code']; // If the 2FA code is present, this might be a 2FA verification attempt
}else{ if (isset($data['code']) && isset($_SESSION['2fa_email']) && isset($_SESSION['2fa_password'])) {
$code = null; $email = $_SESSION['2fa_email'];
$password = $_SESSION['2fa_password'];
// Clear the session variables immediately after use
unset($_SESSION['2fa_email'], $_SESSION['2fa_password'], $_SESSION['is2FAEnabled']);
} }
$login = Auth::login($data['email'], $data['password'], $remember, $code);
if($login===true) { $login = Auth::login($email, $password, $data['remember'] ?? null, $data['code'] ?? null);
unset($_SESSION['2fa_email'], $_SESSION['2fa_password'], $_SESSION['is2FAEnabled']);
if ($login===true) {
$db = $container->get('db'); $db = $container->get('db');
$currentDateTime = new \DateTime(); $currentDateTime = new \DateTime();
$currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp $currentDate = $currentDateTime->format('Y-m-d H:i:s.v'); // Current timestamp

View file

@ -33,10 +33,6 @@
</span> </span>
</div> </div>
</div> </div>
<div class="mb-2">
<label class="form-label">2FA Code</label>
<input name="code" type="text" class="form-control" autocomplete="off" placeholder="Enter 6-digit code" pattern="\d{6}" maxlength="6" minlength="6" inputmode="numeric">
</div>
<div class="mb-2"> <div class="mb-2">
<label class="form-check"> <label class="form-check">
<input name="remember" value="remember" id="remember" type="checkbox" class="form-check-input"/> <input name="remember" value="remember" id="remember" type="checkbox" class="form-check-input"/>

View file

@ -0,0 +1,101 @@
{% extends "layouts/auth.twig" %}
{% block title %}Two-Factor Authentication{% endblock %}
{% block content %}
<div class="page page-center">
<div class="container container-tight py-4">
<div class="text-center mb-4">
<a href="." class="navbar-brand navbar-brand-autodark"><img src="./static/logo-bw.svg" height="36" alt=""></a>
{% include 'partials/flash.twig' %}
</div>
<form class="card card-md" action="{{route('login')}}" id="login" method="post" autocomplete="off" novalidate>
{{ csrf.field | raw }}
<div class="card-body">
<h2 class="card-title card-title-lg text-center mb-4">Two-Factor Authentication</h2>
<p class="my-4 text-center">Please enter the <strong>6-digit</strong> verification code from your authenticator app to continue.</p>
<div class="my-5">
<div class="row g-4">
<div class="col">
<div class="row g-2">
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
</div>
</div>
<div class="col">
<div class="row g-2">
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
<div class="col">
<input type="text" class="form-control form-control-lg text-center py-3" maxlength="1" inputmode="numeric" pattern="[0-9]*" data-code-input />
</div>
</div>
</div>
</div>
</div>
<div class="form-footer">
<div class="btn-list flex-nowrap">
<a href="{{route('login')}}" class="btn w-100">
Cancel
</a>
<input type="hidden" name="code" id="fullCode">
<button type="submit" class="btn btn-primary w-100">
Verify
</button>
</div>
</div>
</div>
</form>
</div>
</div>
<script>
document.querySelectorAll('[data-code-input]').forEach(function(input, idx, inputs) {
// Handle input to move forward
input.addEventListener('input', function(e) {
// Move to the next box if the current one is filled and it's not the last box
if (input.value && idx < inputs.length - 1) {
inputs[idx + 1].focus();
}
});
// Handle backspace to move backward
input.addEventListener('keydown', function(e) {
// If backspace is pressed and the input is empty or becomes empty, focus the previous box
if (e.key === 'Backspace' && (input.value === '' || input.value.length === 1)) {
if (idx > 0) {
// Delay moving focus to handle case where backspace is hit and the input is not yet empty
setTimeout(() => inputs[idx - 1].focus(), 0);
}
}
});
});
document.getElementById('login').addEventListener('submit', function(e) {
e.preventDefault(); // Prevent the form from submitting immediately
// Concatenate the values from each input
var inputs = document.querySelectorAll('[data-code-input]');
var code = '';
inputs.forEach(function(input) {
code += input.value;
});
// Set the concatenated value to the hidden input
document.getElementById('fullCode').value = code;
console.log(code);
// Now submit the form
this.submit();
});
</script>
{% endblock %}

View file

@ -26,6 +26,7 @@ $app->get('/', HomeController::class .':index')->setName('index');
$app->group('', function ($route) { $app->group('', function ($route) {
$route->get('/login', AuthController::class . ':createLogin')->setName('login'); $route->get('/login', AuthController::class . ':createLogin')->setName('login');
$route->map(['GET', 'POST'], '/login/verify', AuthController::class . ':verify2FA')->setName('verify2FA');
$route->post('/login', AuthController::class . ':login'); $route->post('/login', AuthController::class . ':login');
$route->post('/webauthn/login/challenge', AuthController::class . ':getLoginChallenge')->setName('webauthn.login.challenge'); $route->post('/webauthn/login/challenge', AuthController::class . ':getLoginChallenge')->setName('webauthn.login.challenge');
$route->post('/webauthn/login/verify', AuthController::class . ':verifyLogin')->setName('webauthn.login.verify'); $route->post('/webauthn/login/verify', AuthController::class . ':verifyLogin')->setName('webauthn.login.verify');