Added an basic form to add Google Authenticator.

This commit is contained in:
Jan Böhmer 2019-12-23 17:20:28 +01:00
parent 24672a30b9
commit 35b5640627
10 changed files with 277 additions and 3 deletions

View file

@ -90,6 +90,8 @@ require('./jquery.tristate.js');
require('darkmode-js'); require('darkmode-js');
window.QRCode = require('qrcode');
require('../ts_src/ajax_ui'); require('../ts_src/ajax_ui');
import {ajaxUI} from "../ts_src/ajax_ui"; import {ajaxUI} from "../ts_src/ajax_ui";

View file

@ -23,6 +23,7 @@ import {ajaxUI} from "./ajax_ui";
import "bootbox"; import "bootbox";
import "marked"; import "marked";
import * as marked from "marked"; import * as marked from "marked";
import "qrcode";
import {parse} from "marked"; 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); $('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 //Need for proper body padding, with every navbar height
$(window).resize(function () { $(window).resize(function () {
let height : number = $('#navbar').height() + 10; let height : number = $('#navbar').height() + 10;

View file

@ -43,6 +43,7 @@
"marked": "^0.7.0", "marked": "^0.7.0",
"patternfly-bootstrap-treeview": "^2.1.8", "patternfly-bootstrap-treeview": "^2.1.8",
"pdfmake": "^0.1.53", "pdfmake": "^0.1.53",
"qrcode": "^1.4.4",
"ts-loader": "^5.3.3", "ts-loader": "^5.3.3",
"typescript": "^3.3.4000" "typescript": "^3.3.4000"
} }

View file

@ -25,12 +25,14 @@ use App\Entity\Attachments\AttachmentType;
use App\Entity\Attachments\UserAttachment; use App\Entity\Attachments\UserAttachment;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Form\Permissions\PermissionsType; use App\Form\Permissions\PermissionsType;
use App\Form\TFASettingsType;
use App\Form\UserAdminForm; use App\Form\UserAdminForm;
use App\Form\UserSettingsType; use App\Form\UserSettingsType;
use App\Services\EntityExporter; use App\Services\EntityExporter;
use App\Services\EntityImporter; use App\Services\EntityImporter;
use App\Services\StructuralElementRecursionHelper; use App\Services\StructuralElementRecursionHelper;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
use Symfony\Component\Asset\Packages; use Symfony\Component\Asset\Packages;
use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
@ -150,7 +152,7 @@ class UserController extends AdminPages\BaseAdminController
/** /**
* @Route("/settings", name="user_settings") * @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 * @var User
@ -224,6 +226,22 @@ class UserController extends AdminPages\BaseAdminController
$this->addFlash('success', 'user.settings.pw_changed_flash'); $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 * Output both forms
*****************************/ *****************************/
@ -232,6 +250,13 @@ class UserController extends AdminPages\BaseAdminController
'settings_form' => $form->createView(), 'settings_form' => $form->createView(),
'pw_form' => $pw_form->createView(), 'pw_form' => $pw_form->createView(),
'page_need_reload' => $page_need_reload, 'page_need_reload' => $page_need_reload,
'tfa_form' => $tfa_form->createView(),
'tfa_google' => [
'qrContent' => $googleAuthenticator->getQRContent($user),
'secret' => $user->getGoogleAuthenticatorSecret(),
'username' => $user->getGoogleAuthenticatorUsername()
]
]); ]);
} }

View file

@ -0,0 +1,42 @@
<?php
namespace App\Form;
use App\Entity\UserSystem\User;
use App\Validator\Constraints\ValidGoogleAuthCode;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\ResetType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TFASettingsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->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,
]);
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace App\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class ValidGoogleAuthCode extends Constraint
{
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Validator\Constraints;
use App\Entity\UserSystem\User;
use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator;
use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
use Symfony\Component\Validator\Exception\UnexpectedValueException;
class ValidGoogleAuthCodeValidator extends ConstraintValidator
{
protected $googleAuthenticator;
public function __construct(GoogleAuthenticator $googleAuthenticator)
{
$this->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');
}
}
}
}

View file

@ -0,0 +1,44 @@
<div class="card mt-4">
<div class="card-header">
<i class="fa fa-shield-alt fa-fw" aria-hidden="true"></i>
{% trans %}user.settings.2fa_settings{% endtrans %}
</div>
<div class="card-body">
{{ form_start(tfa_form) }}
<ul class="nav nav-tabs" id="tfa-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="google-tab" data-toggle="tab" href="#tfa-google" role="tab"
aria-controls="home" aria-selected="true">{% trans %}tfa.settings.google.tab{% endtrans %}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="backup-tab" data-toggle="tab" href="#tfa-backup" role="tab"
aria-controls="profile" aria-selected="false">{% trans %}tfa.settings.bakup.tab{% endtrans %}</a>
</li>
</ul>
<div class="tab-content mt-3 mb-3" id="tfa-tabs-content">
<div class="tab-pane fade show active" id="tfa-google" role="tabpanel" aria-labelledby="google-tab">
<div class="offset-3 row">
<div class="col-3">
<canvas class="qrcode" data-content="{{ tfa_google.qrContent }}"></canvas>
</div>
<div class="col-9 my-auto">
<ol class="">
<li>{% trans %}tfa_google.step.download{% endtrans %}</li>
<li>{% trans %}tfa_google.step.scan{% endtrans %}</li>
<li>{% trans %}tfa_google.step.input_code{% endtrans %}</li>
</ol>
</div>
</div>
{{ form_row(tfa_form.google_confirmation) }}
</div>
<div class="tab-pane fade" id="tfa-backup" role="tabpanel" aria-labelledby="backup-tab">
</div>
</div>
{{ form_row(tfa_form.submit) }}
{{ form_row(tfa_form.cancel) }}
{{ form_end(tfa_form) }}
</div>
</div>

View file

@ -47,6 +47,8 @@
{% block content %} {% block content %}
{{ parent() }} {{ parent() }}
{% include "Users/_2fa_settings.html.twig" %}
<div class="card mt-4"> <div class="card mt-4">
<div class="card-header"> <div class="card-header">
<i class="fa fa-key fa-fw" aria-hidden="true"></i> <i class="fa fa-key fa-fw" aria-hidden="true"></i>

View file

@ -1490,12 +1490,30 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.8.2:
electron-to-chromium "^1.3.322" electron-to-chromium "^1.3.322"
node-releases "^1.1.42" 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: buffer-equal@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b"
integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= 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" version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
@ -1519,6 +1537,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4" ieee754 "^1.1.4"
isarray "^1.0.0" 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: builtin-status-codes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" 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" miller-rabin "^4.0.0"
randombytes "^2.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: dir-glob@^2.0.0:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" 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" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 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: isexe@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 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" resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d"
integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== 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: popper.js@>=1.12.9, popper.js@^1.14.1, popper.js@^1.14.7:
version "1.16.0" version "1.16.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" 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" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= 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: qs@6.7.0:
version "6.7.0" version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" 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" camelcase "^5.0.0"
decamelize "^1.2.0" decamelize "^1.2.0"
yargs-parser@^13.1.0: yargs-parser@^13.1.0, yargs-parser@^13.1.1:
version "13.1.1" version "13.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0"
integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==
@ -7128,3 +7182,19 @@ yargs@13.2.4:
which-module "^2.0.0" which-module "^2.0.0"
y18n "^4.0.0" y18n "^4.0.0"
yargs-parser "^13.1.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"