diff --git a/assets/js/app.js b/assets/js/app.js index 9287e85d..ccad4ba3 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -90,6 +90,8 @@ require('./jquery.tristate.js'); require('darkmode-js'); +window.QRCode = require('qrcode'); + require('../ts_src/ajax_ui'); import {ajaxUI} from "../ts_src/ajax_ui"; diff --git a/assets/ts_src/event_listeners.ts b/assets/ts_src/event_listeners.ts index 1a4f203a..8a3ed399 100644 --- a/assets/ts_src/event_listeners.ts +++ b/assets/ts_src/event_listeners.ts @@ -23,6 +23,7 @@ import {ajaxUI} from "./ajax_ui"; import "bootbox"; import "marked"; import * as marked from "marked"; +import "qrcode"; import {parse} from "marked"; /************************************ @@ -458,6 +459,16 @@ $(document).on("ajaxUI:start ajaxUI:reload attachment:create", function() { $('select.attachment_type_selector').change(updater).each(updater); }); +$(document).on("ajaxUI:start ajaxUI:reload", function() { + $('.qrcode').each(function() { + let canvas = $(this); + //@ts-ignore + QRCode.toCanvas(canvas[0], canvas.data('content'), function(error) { + if(error) console.error(error); + }) + }); +}); + //Need for proper body padding, with every navbar height $(window).resize(function () { let height : number = $('#navbar').height() + 10; diff --git a/package.json b/package.json index 9d9f378a..99980aad 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "marked": "^0.7.0", "patternfly-bootstrap-treeview": "^2.1.8", "pdfmake": "^0.1.53", + "qrcode": "^1.4.4", "ts-loader": "^5.3.3", "typescript": "^3.3.4000" } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a93c8b3e..62f6dddd 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -25,12 +25,14 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\UserAttachment; use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; +use App\Form\TFASettingsType; use App\Form\UserAdminForm; use App\Form\UserSettingsType; use App\Services\EntityExporter; use App\Services\EntityImporter; use App\Services\StructuralElementRecursionHelper; use Doctrine\ORM\EntityManagerInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; use Symfony\Component\Asset\Packages; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; @@ -150,7 +152,7 @@ class UserController extends AdminPages\BaseAdminController /** * @Route("/settings", name="user_settings") */ - public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder) + public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator) { /** * @var User @@ -224,6 +226,22 @@ class UserController extends AdminPages\BaseAdminController $this->addFlash('success', 'user.settings.pw_changed_flash'); } + //Handle 2FA things + $tfa_form = $this->createForm(TFASettingsType::class, $user); + $tfa_form->handleRequest($request); + if (!$user->getGoogleAuthenticatorSecret()) { + $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); + $tfa_form->setData($user); + } + + if ($tfa_form->isSubmitted() && $tfa_form->isValid()) { + //Save 2FA settings (save secrets) + $user->setGoogleAuthenticatorSecret($tfa_form->get('googleAuthenticatorSecret')->getData()); + $em->flush(); + $this->addFlash('success', 'user.settings.2fa.google.activated'); + } + + /****************************** * Output both forms *****************************/ @@ -232,6 +250,13 @@ class UserController extends AdminPages\BaseAdminController 'settings_form' => $form->createView(), 'pw_form' => $pw_form->createView(), 'page_need_reload' => $page_need_reload, + + 'tfa_form' => $tfa_form->createView(), + 'tfa_google' => [ + 'qrContent' => $googleAuthenticator->getQRContent($user), + 'secret' => $user->getGoogleAuthenticatorSecret(), + 'username' => $user->getGoogleAuthenticatorUsername() + ] ]); } diff --git a/src/Form/TFASettingsType.php b/src/Form/TFASettingsType.php new file mode 100644 index 00000000..ae1d9326 --- /dev/null +++ b/src/Form/TFASettingsType.php @@ -0,0 +1,42 @@ +add('google_confirmation', TextType::class, [ + 'mapped' => false, + 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*'], + 'constraints' => [new ValidGoogleAuthCode()] + ]); + + $builder->add('googleAuthenticatorSecret', HiddenType::class,[ + 'disabled' => false, + ]); + + + $builder->add('submit', SubmitType::class); + $builder->add('cancel', ResetType::class); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/ValidGoogleAuthCode.php b/src/Validator/Constraints/ValidGoogleAuthCode.php new file mode 100644 index 00000000..ef89a896 --- /dev/null +++ b/src/Validator/Constraints/ValidGoogleAuthCode.php @@ -0,0 +1,12 @@ +googleAuthenticator = $googleAuthenticator; + } + + /** + * @inheritDoc + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof ValidGoogleAuthCode) { + throw new UnexpectedTypeException($constraint, ValidGoogleAuthCode::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + if(!ctype_digit($value)) { + $this->context->addViolation('validator.google_code.only_digits_allowed'); + } + + //Number must have 6 digits + if(strlen($value) !== 6) { + $this->context->addViolation('validator.google_code.wrong_digit_count'); + } + + //Try to retrieve the user we want to check + if($this->context->getObject() instanceof FormInterface && + $this->context->getObject()->getParent() instanceof FormInterface + && $this->context->getObject()->getParent()->getData() instanceof User) { + $user = $this->context->getObject()->getParent()->getData(); + + //Check if the given code is valid + if(!$this->googleAuthenticator->checkCode($user, $value)) { + $this->context->addViolation('validator.google_code.wrong_code'); + } + + } + } +} \ No newline at end of file diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig new file mode 100644 index 00000000..19b9d23b --- /dev/null +++ b/templates/Users/_2fa_settings.html.twig @@ -0,0 +1,44 @@ +