diff --git a/assets/js/app.js b/assets/js/app.js index ccad4ba3..9d23993e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -90,6 +90,8 @@ require('./jquery.tristate.js'); require('darkmode-js'); +window.ClipboardJS = require('clipboard'); + window.QRCode = require('qrcode'); require('../ts_src/ajax_ui'); diff --git a/assets/ts_src/event_listeners.ts b/assets/ts_src/event_listeners.ts index 8a3ed399..c415839d 100644 --- a/assets/ts_src/event_listeners.ts +++ b/assets/ts_src/event_listeners.ts @@ -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; diff --git a/config/services.yaml b/config/services.yaml index 7fa8ebf3..90c5a93f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -123,7 +123,7 @@ services: App\Services\TFA\BackupCodeGenerator: arguments: $code_length: 8 - $code_count: 10 + $code_count: 15 App\Services\TranslationExtractor\PermissionExtractor: tags: diff --git a/package.json b/package.json index 99980aad..ad964654 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index c31f990d..697fe56e 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -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, diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index a03aa85a..21c20e45 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -764,7 +764,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe */ public function getBackupCodes() : array { - return $this->backupCodes; + return $this->backupCodes ?? []; } /** diff --git a/src/Services/TFA/BackupCodeGenerator.php b/src/Services/TFA/BackupCodeGenerator.php index a180dae0..053b741e 100644 --- a/src/Services/TFA/BackupCodeGenerator.php +++ b/src/Services/TFA/BackupCodeGenerator.php @@ -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 { diff --git a/src/Services/TFA/BackupCodeManager.php b/src/Services/TFA/BackupCodeManager.php new file mode 100644 index 00000000..ec73e367 --- /dev/null +++ b/src/Services/TFA/BackupCodeManager.php @@ -0,0 +1,57 @@ +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); + } +} \ No newline at end of file diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig index 0ac1c6cd..fe21b45f 100644 --- a/templates/Users/_2fa_settings.html.twig +++ b/templates/Users/_2fa_settings.html.twig @@ -1,3 +1,5 @@ +{# @var user \App\Entity\UserSystem\User #} +
{% trans %}tfa_backup.remaining_tokens{% endtrans %}: {{ user.backupCodes | length }}
+{% trans %}tfa_backup.generation_date{% endtrans %}: {{ user.backupCodesGenerationDate | format_datetime }}
+{% trans %}tfa_backup.codes.help{% endtrans %}
+ +{% trans %}tfa_backup.remaining_tokens{% endtrans %}: {{ user.backupCodes | length }}
+{% trans %}tfa_backup.username{% endtrans %}: {{ user.name }}
+{% trans %}tfa_backup.generation_date{% endtrans %}: {{ user.backupCodesGenerationDate | format_datetime }}
+ +{% trans with {'%date%': "now" | format_datetime} %}tfa_backup.codes.page_generated_on{% endtrans %}
+ + + {% trans %}tfa_backup.codes.print{% endtrans %} + + +