mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Added possibility to show backup codes in user settings.
This commit is contained in:
parent
fba5f9794f
commit
604ebe420d
13 changed files with 288 additions and 6 deletions
|
@ -90,6 +90,8 @@ require('./jquery.tristate.js');
|
|||
|
||||
require('darkmode-js');
|
||||
|
||||
window.ClipboardJS = require('clipboard');
|
||||
|
||||
window.QRCode = require('qrcode');
|
||||
|
||||
require('../ts_src/ajax_ui');
|
||||
|
|
|
@ -469,6 +469,32 @@ $(document).on("ajaxUI:start ajaxUI:reload", function() {
|
|||
});
|
||||
});
|
||||
|
||||
$(document).on("ajaxUI:start ajaxUI:reload", function() {
|
||||
function setTooltip(btn, message) {
|
||||
$(btn).tooltip('hide')
|
||||
.attr('data-original-title', message)
|
||||
.tooltip('show');
|
||||
}
|
||||
|
||||
function hideTooltip(btn) {
|
||||
setTimeout(function() {
|
||||
$(btn).tooltip('hide');
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
var clipboard = new ClipboardJS('.btn');
|
||||
clipboard.on('success', function(e) {
|
||||
setTooltip(e.trigger, 'Copied!');
|
||||
hideTooltip(e.trigger);
|
||||
});
|
||||
|
||||
clipboard.on('error', function(e) {
|
||||
setTooltip(e.trigger, 'Failed!');
|
||||
hideTooltip(e.trigger);
|
||||
});
|
||||
});
|
||||
|
||||
//Need for proper body padding, with every navbar height
|
||||
$(window).resize(function () {
|
||||
let height : number = $('#navbar').height() + 10;
|
||||
|
|
|
@ -123,7 +123,7 @@ services:
|
|||
App\Services\TFA\BackupCodeGenerator:
|
||||
arguments:
|
||||
$code_length: 8
|
||||
$code_count: 10
|
||||
$code_count: 15
|
||||
|
||||
App\Services\TranslationExtractor\PermissionExtractor:
|
||||
tags:
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"bootstrap-fileinput": "^5.0.1",
|
||||
"bootstrap-select": "^1.13.8",
|
||||
"bootswatch": "^4.3.1",
|
||||
"clipboard": "^2.0.4",
|
||||
"copy-webpack-plugin": "^5.0.4",
|
||||
"corejs-typeahead": "^1.2.1",
|
||||
"darkmode-js": "^1.5.3",
|
||||
|
|
|
@ -31,7 +31,9 @@ use App\Form\UserSettingsType;
|
|||
use App\Services\EntityExporter;
|
||||
use App\Services\EntityImporter;
|
||||
use App\Services\StructuralElementRecursionHelper;
|
||||
use App\Services\TFA\BackupCodeManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Util\Exception;
|
||||
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
|
||||
use Symfony\Component\Asset\Packages;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
|
@ -151,10 +153,33 @@ class UserController extends AdminPages\BaseAdminController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/2fa_backup_codes", name="show_backup_codes")
|
||||
*/
|
||||
public function showBackupCodes()
|
||||
{
|
||||
$user = $this->getUser();
|
||||
if (!$user instanceof User) {
|
||||
return new \RuntimeException('This controller only works only for Part-DB User objects!');
|
||||
}
|
||||
|
||||
//When user change its settings, he should be logged in fully.
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
|
||||
if (empty($user->getBackupCodes())) {
|
||||
$this->addFlash('error', 'You do not have any backup codes enabled, therefore you can not view them!');
|
||||
throw new Exception('You do not have any backup codes enabled, therefore you can not view them!');
|
||||
}
|
||||
|
||||
return $this->render('Users/backup_codes.html.twig', [
|
||||
'user' => $user
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/settings", name="user_settings")
|
||||
*/
|
||||
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator)
|
||||
public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager)
|
||||
{
|
||||
/**
|
||||
* @var User
|
||||
|
@ -252,12 +277,14 @@ class UserController extends AdminPages\BaseAdminController
|
|||
if (!$google_enabled) {
|
||||
//Save 2FA settings (save secrets)
|
||||
$user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData());
|
||||
$backupCodeManager->enableBackupCodes($user);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'user.settings.2fa.google.activated');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
} elseif ($google_enabled) {
|
||||
//Remove secret to disable google authenticator
|
||||
$user->setGoogleAuthenticatorSecret(null);
|
||||
$backupCodeManager->disableBackupCodesIfUnused($user);
|
||||
$em->flush();
|
||||
$this->addFlash('success', 'user.settings.2fa.google.disabled');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
|
@ -270,6 +297,7 @@ class UserController extends AdminPages\BaseAdminController
|
|||
*****************************/
|
||||
|
||||
return $this->render('Users/user_settings.html.twig', [
|
||||
'user' => $user,
|
||||
'settings_form' => $form->createView(),
|
||||
'pw_form' => $pw_form->createView(),
|
||||
'page_need_reload' => $page_need_reload,
|
||||
|
|
|
@ -764,7 +764,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
*/
|
||||
public function getBackupCodes() : array
|
||||
{
|
||||
return $this->backupCodes;
|
||||
return $this->backupCodes ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -46,7 +46,6 @@ class BackupCodeGenerator
|
|||
/**
|
||||
* Returns a full backup code set. The code count can be configured in the constructor
|
||||
* @return string[] An array containing different backup codes.
|
||||
* @throws \Exception If no entropy source is available
|
||||
*/
|
||||
public function generateCodeSet() : array
|
||||
{
|
||||
|
|
57
src/Services/TFA/BackupCodeManager.php
Normal file
57
src/Services/TFA/BackupCodeManager.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Services\TFA;
|
||||
|
||||
|
||||
use App\Entity\UserSystem\User;
|
||||
|
||||
/**
|
||||
* This services offers methods to manage backup codes for two factor authentication
|
||||
* @package App\Services\TFA
|
||||
*/
|
||||
class BackupCodeManager
|
||||
{
|
||||
protected $backupCodeGenerator;
|
||||
|
||||
public function __construct(BackupCodeGenerator $backupCodeGenerator)
|
||||
{
|
||||
$this->backupCodeGenerator = $backupCodeGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable backup codes for the given user, by generating a set of backup codes.
|
||||
* If the backup codes were already enabled before, they a
|
||||
* @param User $user
|
||||
*/
|
||||
public function enableBackupCodes(User $user)
|
||||
{
|
||||
if(empty($user->getBackupCodes())) {
|
||||
$this->regenerateBackupCodes($user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable (remove) the backup codes when no other 2 factor authentication methods are enabled.
|
||||
* @param User $user
|
||||
*/
|
||||
public function disableBackupCodesIfUnused(User $user)
|
||||
{
|
||||
if($user->isU2FAuthEnabled() || $user->isGoogleAuthenticatorEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBackupCodes([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new set of backup codes for the user. If no backup codes were available before, new ones are
|
||||
* generated.
|
||||
* @param User $user The user for which the backup codes should be regenerated
|
||||
*/
|
||||
public function regenerateBackupCodes(User $user)
|
||||
{
|
||||
$codes = $this->backupCodeGenerator->generateCodeSet();
|
||||
$user->setBackupCodes($codes);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
{# @var user \App\Entity\UserSystem\User #}
|
||||
|
||||
<div class="card mt-4">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-shield-alt fa-fw" aria-hidden="true"></i>
|
||||
|
@ -19,6 +21,10 @@
|
|||
<div class="tab-pane fade show active" id="tfa-google" role="tabpanel" aria-labelledby="google-tab">
|
||||
{{ form_start(google_form) }}
|
||||
{% if not tfa_google.enabled %}
|
||||
<div class="offset-3">
|
||||
<h6>{% trans %}tfa_google.disabled_message{% endtrans %}</h6>
|
||||
</div>
|
||||
|
||||
<div class="offset-3 row">
|
||||
<div class="col-3">
|
||||
<canvas class="qrcode" data-content="{{ tfa_google.qrContent }}"></canvas>
|
||||
|
@ -50,13 +56,33 @@
|
|||
|
||||
{{ form_row(google_form.google_confirmation) }}
|
||||
{% else %}
|
||||
Google Authenticator is enabled! TODO
|
||||
<div class="offset-3">
|
||||
<h6>{% trans %}tfa_google.enabled_message{% endtrans %}</h6>
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ form_row(google_form.submit) }}
|
||||
{{ form_end(google_form) }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tfa-backup" role="tabpanel" aria-labelledby="backup-tab">
|
||||
{% if user.backupCodes is empty %}
|
||||
<div class="offset-3">
|
||||
<h6>{% trans %}tfa_backup.disabled{% endtrans %}</h6>
|
||||
<span>{% trans %}tfa_backup.explanation{% endtrans %}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="offset-3">
|
||||
<h6>{% trans %}tfa_backup.enabled{% endtrans %}</h6>
|
||||
<span>{% trans %}tfa_backup.explanation{% endtrans %}</span>
|
||||
</div>
|
||||
<div class="offset-3 mt-2">
|
||||
<p class="mb-0"><b>{% trans %}tfa_backup.remaining_tokens{% endtrans %}:</b> {{ user.backupCodes | length }}</p>
|
||||
<p><b>{% trans %}tfa_backup.generation_date{% endtrans %}:</b> {{ user.backupCodesGenerationDate | format_datetime }}</p>
|
||||
</div>
|
||||
<div class="offset-3">
|
||||
<a href="{{ url('show_backup_codes') }}" target="_blank" data-no-ajax class="btn btn-primary">{% trans %}tfa_backup.show_codes{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
40
templates/Users/backup_codes.html.twig
Normal file
40
templates/Users/backup_codes.html.twig
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html.twig" %}
|
||||
|
||||
{% block title %}{{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{% trans %}tfa_backup.codes.explanation{% endtrans %}</h5>
|
||||
<p class="card-text">{% trans %}tfa_backup.codes.help{% endtrans %}</p>
|
||||
|
||||
<p class="mb-0"><b>{% trans %}tfa_backup.remaining_tokens{% endtrans %}:</b> {{ user.backupCodes | length }}</p>
|
||||
<p class="mb-0"><b>{% trans %}tfa_backup.username{% endtrans %}:</b> {{ user.name }}</p>
|
||||
<p><b>{% trans %}tfa_backup.generation_date{% endtrans %}:</b> {{ user.backupCodesGenerationDate | format_datetime }}</p>
|
||||
|
||||
<div class="alert border-dark">
|
||||
<div class="card-body">
|
||||
<ul class="row list-unstyled" id="backup_codes_list">
|
||||
{% for code in user.backupCodes %}
|
||||
<h4 class="col-6"><li><i class="far fa-square fa-fw"></i><span class="text-monospace text-dark ml-1">{{ code }}</span></li></h4>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><small>{% trans with {'%date%': "now" | format_datetime} %}tfa_backup.codes.page_generated_on{% endtrans %}</small></p>
|
||||
|
||||
<a href="javascript:window.print()" class="btn btn-outline-dark d-print-none">
|
||||
<i class="fas fa-print fa-fw"></i> {% trans %}tfa_backup.codes.print{% endtrans %}
|
||||
</a>
|
||||
<button class="btn btn-outline-dark d-print-none" data-clipboard-text="{{ user.backupCodes | join('\n') }}">
|
||||
<i class="fas fa-copy"></i> {% trans %}tfa_backup.codes.copy_clipboard{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -57,13 +57,16 @@ class UserTest extends TestCase
|
|||
$this->assertSame($expected ,$user->isGoogleAuthenticatorEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHPUnit 8
|
||||
*/
|
||||
public function testSetBackupCodes()
|
||||
{
|
||||
$user = new User();
|
||||
$codes = ["test", "invalid", "test"];
|
||||
$user->setBackupCodes($codes);
|
||||
// Backup Codes generation date must be changed!
|
||||
$this->assertEquals(new \DateTime(), $user->getBackupCodesGenerationDate(), '', 0.1);
|
||||
$this->assertEqualsWithDelta(new \DateTime(), $user->getBackupCodesGenerationDate(), 0.1);
|
||||
$this->assertEquals($codes, $user->getBackupCodes());
|
||||
|
||||
//Test what happens if we delete the backup keys
|
||||
|
|
69
tests/Services/TFA/BackupCodeManagerTest.php
Normal file
69
tests/Services/TFA/BackupCodeManagerTest.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace App\Tests\Services\TFA;
|
||||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Services\TFA\BackupCodeManager;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class BackupCodeManagerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @var BackupCodeManager $service
|
||||
*/
|
||||
protected $service;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->service = self::$container->get(BackupCodeManager::class);
|
||||
}
|
||||
|
||||
public function testRegenerateBackupCodes()
|
||||
{
|
||||
$user = new User();
|
||||
$old_codes = ['aaaa', 'bbbb'];
|
||||
$user->setBackupCodes($old_codes);
|
||||
$this->service->regenerateBackupCodes($user);
|
||||
$this->assertNotEquals($old_codes, $user->getBackupCodes());
|
||||
}
|
||||
|
||||
public function testEnableBackupCodes()
|
||||
{
|
||||
$user = new User();
|
||||
//Check that nothing is changed, if there are already backup codes
|
||||
|
||||
$old_codes = ['aaaa', 'bbbb'];
|
||||
$user->setBackupCodes($old_codes);
|
||||
$this->service->enableBackupCodes($user);
|
||||
$this->assertEquals($old_codes, $user->getBackupCodes());
|
||||
|
||||
//When no old codes are existing, it should generate a set
|
||||
$user->setBackupCodes([]);
|
||||
$this->service->enableBackupCodes($user);
|
||||
$this->assertNotEmpty($user->getBackupCodes());
|
||||
}
|
||||
|
||||
public function testDisableBackupCodesIfUnused()
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
//By default nothing other 2FA is activated, so the backup codes should be disabled
|
||||
$codes = ['aaaa', 'bbbb'];
|
||||
$user->setBackupCodes($codes);
|
||||
$this->service->disableBackupCodesIfUnused($user);
|
||||
$this->assertEmpty($user->getBackupCodes());
|
||||
|
||||
$user->setBackupCodes($codes);
|
||||
|
||||
$user->setGoogleAuthenticatorSecret('jskf');
|
||||
$this->service->disableBackupCodesIfUnused($user);
|
||||
$this->assertEquals($codes, $user->getBackupCodes());
|
||||
|
||||
$user->setGoogleAuthenticatorSecret('');
|
||||
$user->addU2FKey(new U2FKey());
|
||||
$this->service->disableBackupCodesIfUnused($user);
|
||||
$this->assertEquals($codes, $user->getBackupCodes());
|
||||
}
|
||||
}
|
31
yarn.lock
31
yarn.lock
|
@ -1705,6 +1705,15 @@ clean-webpack-plugin@^0.1.19:
|
|||
dependencies:
|
||||
rimraf "^2.6.1"
|
||||
|
||||
clipboard@^2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d"
|
||||
integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ==
|
||||
dependencies:
|
||||
good-listener "^1.2.2"
|
||||
select "^1.1.2"
|
||||
tiny-emitter "^2.0.0"
|
||||
|
||||
cliui@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
|
||||
|
@ -2405,6 +2414,11 @@ del@^4.1.1:
|
|||
pify "^4.0.1"
|
||||
rimraf "^2.6.3"
|
||||
|
||||
delegate@^3.1.2:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
|
||||
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
|
||||
|
||||
delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
|
@ -3353,6 +3367,13 @@ globby@^7.1.1:
|
|||
pify "^3.0.0"
|
||||
slash "^1.0.0"
|
||||
|
||||
good-listener@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
|
||||
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
|
||||
dependencies:
|
||||
delegate "^3.1.2"
|
||||
|
||||
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423"
|
||||
|
@ -5956,6 +5977,11 @@ select-hose@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||
integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=
|
||||
|
||||
select@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
|
||||
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
|
||||
|
||||
selfsigned@^1.10.7:
|
||||
version "1.10.7"
|
||||
resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.7.tgz#da5819fd049d5574f28e88a9bcc6dbc6e6f3906b"
|
||||
|
@ -6563,6 +6589,11 @@ timsort@^0.3.0:
|
|||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||
|
||||
tiny-emitter@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||
|
||||
tiny-inflate@^1.0.0, tiny-inflate@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue