mirror of
https://github.com/getnamingo/registry.git
synced 2025-07-05 18:43:19 +02:00
Improvements of the 2FA validation code UI
This commit is contained in:
parent
b46ff2286c
commit
6515bcda80
5 changed files with 162 additions and 29 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
101
cp/resources/views/auth/verify2fa.twig
Normal file
101
cp/resources/views/auth/verify2fa.twig
Normal 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 %}
|
|
@ -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');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue