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 @@ +
+
+ + {% trans %}user.settings.2fa_settings{% endtrans %} +
+
+ {{ form_start(tfa_form) }} + +
+
+
+
+ +
+
+
    +
  1. {% trans %}tfa_google.step.download{% endtrans %}
  2. +
  3. {% trans %}tfa_google.step.scan{% endtrans %}
  4. +
  5. {% trans %}tfa_google.step.input_code{% endtrans %}
  6. +
+
+
+ + {{ form_row(tfa_form.google_confirmation) }} +
+
+ +
+
+ + {{ form_row(tfa_form.submit) }} + {{ form_row(tfa_form.cancel) }} + {{ form_end(tfa_form) }} +
+
\ No newline at end of file diff --git a/templates/Users/user_settings.html.twig b/templates/Users/user_settings.html.twig index df0c93fa..8319c204 100644 --- a/templates/Users/user_settings.html.twig +++ b/templates/Users/user_settings.html.twig @@ -47,6 +47,8 @@ {% block content %} {{ parent() }} + {% include "Users/_2fa_settings.html.twig" %} +
diff --git a/yarn.lock b/yarn.lock index 75261cf5..28e056a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1490,12 +1490,30 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.8.2: electron-to-chromium "^1.3.322" node-releases "^1.1.42" +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= -buffer-from@^1.0.0: +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + +buffer-from@^1.0.0, buffer-from@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -1519,6 +1537,14 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2431,6 +2457,11 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dijkstrajs@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b" + integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs= + dir-glob@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -3934,6 +3965,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5060,6 +5096,11 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== +pngjs@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + popper.js@>=1.12.9, popper.js@^1.14.1, popper.js@^1.14.7: version "1.16.0" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" @@ -5512,6 +5553,19 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qrcode@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" + integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q== + dependencies: + buffer "^5.4.3" + buffer-alloc "^1.2.0" + buffer-from "^1.1.1" + dijkstrajs "^1.0.1" + isarray "^2.0.1" + pngjs "^3.3.0" + yargs "^13.2.4" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -7086,7 +7140,7 @@ yargs-parser@^12.0.0: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.0: +yargs-parser@^13.1.0, yargs-parser@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== @@ -7128,3 +7182,19 @@ yargs@13.2.4: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^13.1.0" + +yargs@^13.2.4: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1"