Added possibility to show backup codes in user settings.

This commit is contained in:
Jan Böhmer 2019-12-27 18:21:12 +01:00
parent fba5f9794f
commit 604ebe420d
13 changed files with 288 additions and 6 deletions

View file

@ -90,6 +90,8 @@ require('./jquery.tristate.js');
require('darkmode-js');
window.ClipboardJS = require('clipboard');
window.QRCode = require('qrcode');
require('../ts_src/ajax_ui');

View file

@ -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;

View file

@ -123,7 +123,7 @@ services:
App\Services\TFA\BackupCodeGenerator:
arguments:
$code_length: 8
$code_count: 10
$code_count: 15
App\Services\TranslationExtractor\PermissionExtractor:
tags:

View file

@ -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",

View file

@ -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,

View file

@ -764,7 +764,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
*/
public function getBackupCodes() : array
{
return $this->backupCodes;
return $this->backupCodes ?? [];
}
/**

View file

@ -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
{

View 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);
}
}

View file

@ -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>

View 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 %}

View file

@ -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

View 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());
}
}

View file

@ -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"