diff --git a/assets/js/app.js b/assets/js/app.js index 9287e85d..9d23993e 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -90,6 +90,10 @@ require('./jquery.tristate.js'); require('darkmode-js'); +window.ClipboardJS = require('clipboard'); + +window.QRCode = require('qrcode'); + require('../ts_src/ajax_ui'); import {ajaxUI} from "../ts_src/ajax_ui"; diff --git a/assets/js/u2f_auth.js b/assets/js/u2f_auth.js new file mode 100644 index 00000000..9ab9a256 --- /dev/null +++ b/assets/js/u2f_auth.js @@ -0,0 +1,134 @@ +import u2fApi from 'u2f-api' + +'use strict' + +window.u2fauth = window.u2fauth || {} + +u2fauth.formId = 'u2fForm' +u2fauth.authCodeId = '_auth_code' +u2fauth.keynameId = 'u2fkeyname' +u2fauth.pressButtonId = 'u2fpressbutton' +u2fauth.errorId = 'u2fError' +u2fauth.timeout = 30 +u2fauth.errorTranslation = { + 1: 'Unknown Error', + 2: 'Bad Request', + 3: 'Client configuration not supported', + 4: 'Device already registered or ineligible', + 5: 'Timeout. Click to retry' +} + +u2fauth.ready = function (fn) { + if (document.readyState !== 'loading') { + fn() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fn) + } else { + document.attachEvent('onreadystatechange', function () { + if (document.readyState !== 'loading') { fn() } + }) + } +} + +u2fauth.authenticate = function () { + u2fauth.clearError() + u2fauth.showPressButton() + + var form = document.getElementById(u2fauth.formId) + var request = JSON.parse(form.dataset.request) + + u2fApi.isSupported() + .then(function (supported) { + if (supported) { + return u2fApi.sign(request, u2fauth.timeout) + .then(response => { + u2fauth.hidePressButton() + u2fauth.submit(form, response) + }) + } else { + alert('Browser not supported') + } + }) + .catch(data => { + u2fauth.hidePressButton() + u2fauth.showError(data.metaData.code, u2fauth.authenticate) + }) +} + +u2fauth.register = function () { + u2fauth.clearError() + u2fauth.hideKeyname() + u2fauth.showPressButton() + + var form = document.getElementById(u2fauth.formId) + var request = JSON.parse(form.dataset.request) + + u2fApi.isSupported() + .then(function (supported) { + if (supported) { + return u2fApi.register(request[0], request[1], u2fauth.timeout) + .then(response => { + u2fauth.hidePressButton() + u2fauth.submit(form, response) + }) + } else { + alert('Browser not supported') + } + }) + .catch(data => { + console.info(data) + u2fauth.hidePressButton() + u2fauth.showError(data.metaData.code, u2fauth.register) + }) +} + +u2fauth.submit = function (form, data) { + var codeField = document.getElementById(u2fauth.authCodeId) + codeField.value = JSON.stringify(data) + form.submit() +} + +u2fauth.hideKeyname = function () { + var keyname = document.getElementById(u2fauth.keynameId) + keyname.style.display = 'none' +} + +u2fauth.hidePressButton = function () { + var pressButton = document.getElementById(u2fauth.pressButtonId) + pressButton.style.display = 'none' +} + +u2fauth.showPressButton = function () { + var pressButton = document.getElementById(u2fauth.pressButtonId) + pressButton.style.display = 'block' +} + +u2fauth.clearError = function () { + var errorDisplay = document.getElementById(u2fauth.errorId) + errorDisplay.style.display = 'none' + errorDisplay.innerText = '' +} + +u2fauth.showError = function (error, callback) { + var errorDisplay = document.getElementById(u2fauth.errorId) + errorDisplay.style.display = 'block' + errorDisplay.innerText = u2fauth.errorTranslation[error] + errorDisplay.onclick = callback +} + +u2fauth.ready(function () { + const form = document.getElementById('u2fForm') + if (!form) { + return + } + const type = form.dataset.action + + if (type === 'auth') { + u2fauth.authenticate() + } else if (type === 'reg' && form.addEventListener) { + form.addEventListener('submit', function (event) { + event.preventDefault() + u2fauth.register() + }, false) + } +}) diff --git a/assets/ts_src/ajax_ui.ts b/assets/ts_src/ajax_ui.ts index 98da4eaf..ef29879a 100644 --- a/assets/ts_src/ajax_ui.ts +++ b/assets/ts_src/ajax_ui.ts @@ -297,11 +297,17 @@ class AjaxUI { /** * Submits the given form via ajax. * @param form The form that will be submmitted. + * @param btn The btn via which the form is submitted */ - public submitForm(form) + public submitForm(form, btn = null) { let options = ajaxUI.getFormOptions(); + if(btn) { + options.data = {}; + options.data[$(btn).attr('name')] = $(btn).attr('value'); + } + $(form).ajaxSubmit(options); } @@ -363,6 +369,11 @@ class AjaxUI { return; } + //Ignore ajax errors with 200 code (like the ones during 2FA authentication) + if(request.status == 200) { + return; + } + console.error("Error getting the ajax data from server!"); console.log(event); console.log(request); diff --git a/assets/ts_src/event_listeners.ts b/assets/ts_src/event_listeners.ts index 1a4f203a..7305d168 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"; /************************************ @@ -184,26 +185,55 @@ $(document).on("ajaxUI:start ajaxUI:reload", function() { }); $(document).on("ajaxUI:start ajaxUI:reload", function() { - $("[data-delete-form]").unbind('submit').submit(function(event) { + $("[data-delete-form]").unbind('submit').submit(function (event) { event.preventDefault(); let form = this; + //Get the submit button + let btn = document.activeElement; + let title = $(this).data("title"); let message = $(this).data("message"); bootbox.confirm({ - message: message, - title: title, - callback: function(result) { + message: message, title: title, callback: function (result) { //If the dialog was confirmed, then submit the form. - if(result) { - ajaxUI.submitForm(form); + if (result) { + ajaxUI.submitForm(form, btn); } - }}); + } + }); return false; }); + + //Register for forms with delete-buttons + $("[data-delete-btn]").parents('form').unbind('submit').submit(function (event) { + event.preventDefault(); + let form = this; + //Get the submit button + let btn = document.activeElement; + + let title = $(btn).data("title"); + let message = $(btn).data("message"); + + //If not the button with the message was pressed, then simply submit the form. + if(!btn.hasAttribute('data-delete-btn')) { + ajaxUI.submitForm(form, btn); + } + + bootbox.confirm({ + message: message, title: title, callback: function (result) { + //If the dialog was confirmed, then submit the form. + if (result) { + ajaxUI.submitForm(form, btn); + } + } + }); + + }); + }); $(document).on("ajaxUI:start ajaxUI:reload", function() { @@ -458,6 +488,65 @@ $(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); + }) + }); +}); + +$(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); + }); +}); + +//Register U2F on page reload too... +$(document).on("ajaxUI:reload", function() { + //@ts-ignore + window.u2fauth.ready(function () { + const form = document.getElementById('u2fForm') + if (!form) { + return + } + const type = form.dataset.action + + if (type === 'auth') { + //@ts-ignore + u2fauth.authenticate() + } else if (type === 'reg' && form.addEventListener) { + form.addEventListener('submit', function (event) { + event.preventDefault() + //@ts-ignore + u2fauth.register() + }, false) + } + }) +}); + //Need for proper body padding, with every navbar height $(window).resize(function () { let height : number = $('#navbar').height() + 10; diff --git a/composer.json b/composer.json index da9f0437..03bfb23c 100644 --- a/composer.json +++ b/composer.json @@ -20,8 +20,10 @@ "nyholm/psr7": "^1.1", "ocramius/proxy-manager": "2.1.*", "omines/datatables-bundle": "^0.3.1", + "r/u2f-two-factor-bundle": "dev-u2f-api", "php-translation/symfony-bundle": "^0.9.1", "s9e/text-formatter": "^2.1", + "scheb/two-factor-bundle": "^4.11", "sensio/framework-extra-bundle": "^5.1", "sensiolabs/security-checker": "^6.0", "shivas/versioning-bundle": "^3.1", @@ -113,5 +115,11 @@ "allow-contrib": false, "require": "4.4.*" } - } + }, + "repositories": [ + { + "type": "vcs", + "url": "git@github.com:stephanvierkant/u2f-two-factor-bundle.git" + } + ] } diff --git a/composer.lock b/composer.lock index d8842a20..1a70e7cc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,70 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a59dc06a43af8bf7a925d52f820f8774", + "content-hash": "8aa86f40d5466e5d6f91f15664f3b633", "packages": [ + { + "name": "beberlei/assert", + "version": "v3.2.7", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/d63a6943fc4fd1a2aedb65994e3548715105abcf", + "reference": "d63a6943fc4fd1a2aedb65994e3548715105abcf", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan-shim": "*", + "phpunit/phpunit": ">=6.0.0 <8" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "psr-4": { + "Assert\\": "lib/Assert" + }, + "files": [ + "lib/Assert/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "time": "2019-12-19T17:51:41+00:00" + }, { "name": "doctrine/annotations", "version": "v1.8.0", @@ -1227,27 +1289,26 @@ }, { "name": "egulias/email-validator", - "version": "2.1.12", + "version": "2.1.13", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "a6255605af39f2db7f5cb62e672bd8a7bad8d208" + "reference": "834593d5900615639208417760ba6a17299e2497" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/a6255605af39f2db7f5cb62e672bd8a7bad8d208", - "reference": "a6255605af39f2db7f5cb62e672bd8a7bad8d208", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/834593d5900615639208417760ba6a17299e2497", + "reference": "834593d5900615639208417760ba6a17299e2497", "shasum": "" }, "require": { "doctrine/lexer": "^1.0.1", - "php": ">= 5.5" + "php": ">=5.5" }, "require-dev": { - "dominicsayers/isemail": "dev-master", - "phpunit/phpunit": "^4.8.35||^5.7||^6.0", - "satooshi/php-coveralls": "^1.0.1", - "symfony/phpunit-bridge": "^4.4@dev" + "dominicsayers/isemail": "^3.0.7", + "phpunit/phpunit": "^4.8.36|^7.5.15", + "satooshi/php-coveralls": "^1.0.1" }, "suggest": { "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" @@ -1281,7 +1342,7 @@ "validation", "validator" ], - "time": "2019-12-20T12:49:39+00:00" + "time": "2019-12-30T08:14:25+00:00" }, { "name": "florianv/exchanger", @@ -1406,35 +1467,35 @@ }, { "name": "friendsofsymfony/ckeditor-bundle", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSCKEditorBundle.git", - "reference": "7ba78735ea75b9a6d4de403d7aced6c993a0557a" + "reference": "7e1cfe2a83faba0be02661d44289d35e940bb5ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/7ba78735ea75b9a6d4de403d7aced6c993a0557a", - "reference": "7ba78735ea75b9a6d4de403d7aced6c993a0557a", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSCKEditorBundle/zipball/7e1cfe2a83faba0be02661d44289d35e940bb5ea", + "reference": "7e1cfe2a83faba0be02661d44289d35e940bb5ea", "shasum": "" }, "require": { "ext-json": "*", "ext-zip": "*", "php": "^7.1", - "symfony/asset": "^3.4 || ^4.0", - "symfony/config": "^3.4 || ^4.0", - "symfony/dependency-injection": "^3.4 || ^4.0", - "symfony/expression-language": "^3.4 || ^4.0", - "symfony/form": "^3.4 || ^4.0", - "symfony/framework-bundle": "^3.4 || ^4.0", - "symfony/http-foundation": "^3.4 || ^4.0", - "symfony/http-kernel": "^3.4 || ^4.0", - "symfony/options-resolver": "^3.4 || ^4.0", - "symfony/property-access": "^3.4 || ^4.0", - "symfony/routing": "^3.4 || ^4.0", - "symfony/twig-bundle": "^3.4 || ^4.0", - "twig/twig": "^2.0" + "symfony/asset": "^3.4 || ^4.0 || ^5.0", + "symfony/config": "^3.4 || ^4.0 || ^5.0", + "symfony/dependency-injection": "^3.4 || ^4.0 || ^5.0", + "symfony/expression-language": "^3.4 || ^4.0 || ^5.0", + "symfony/form": "^3.4 || ^4.0 || ^5.0", + "symfony/framework-bundle": "^3.4 || ^4.0 || ^5.0", + "symfony/http-foundation": "^3.4 || ^4.0 || ^5.0", + "symfony/http-kernel": "^3.4 || ^4.0 || ^5.0", + "symfony/options-resolver": "^3.4 || ^4.0 || ^5.0", + "symfony/property-access": "^3.4 || ^4.0 || ^5.0", + "symfony/routing": "^3.4 || ^4.0 || ^5.0", + "symfony/twig-bundle": "^3.4 || ^4.0 || ^5.0", + "twig/twig": "^2.4 || ^3.0" }, "conflict": { "sebastian/environment": "<1.3.4", @@ -1444,9 +1505,9 @@ "friendsofphp/php-cs-fixer": "^2.0", "matthiasnoback/symfony-dependency-injection-test": "^1.0 || ^2.0", "phpunit/phpunit": "^6.0", - "symfony/console": "^3.4 || ^4.0", - "symfony/phpunit-bridge": "^4.1", - "symfony/yaml": "^3.4 || ^4.0" + "symfony/console": "^3.4 || ^4.0 || ^5.0", + "symfony/phpunit-bridge": "^4.1 || ^5.0", + "symfony/yaml": "^3.4 || ^4.0 || ^5.0" }, "suggest": { "egeloen/form-extra-bundle": "Allows to load CKEditor asynchronously" @@ -1480,7 +1541,7 @@ "keywords": [ "CKEditor" ], - "time": "2019-04-15T16:29:43+00:00" + "time": "2019-12-23T15:31:36+00:00" }, { "name": "gregwar/captcha", @@ -1700,17 +1761,72 @@ "time": "2014-01-12T16:20:24+00:00" }, { - "name": "league/html-to-markdown", - "version": "4.9.0", + "name": "lcobucci/jwt", + "version": "3.3.1", "source": { "type": "git", - "url": "https://github.com/thephpleague/html-to-markdown.git", - "reference": "71319108e3db506250b8987721b13568fd9fa446" + "url": "https://github.com/lcobucci/jwt.git", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/71319108e3db506250b8987721b13568fd9fa446", - "reference": "71319108e3db506250b8987721b13568fd9fa446", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-openssl": "*", + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "mikey179/vfsstream": "~1.5", + "phpmd/phpmd": "~2.2", + "phpunit/php-invoker": "~1.1", + "phpunit/phpunit": "^5.7 || ^7.3", + "squizlabs/php_codesniffer": "~2.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Otávio Cobucci Oblonczyk", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "time": "2019-05-24T18:30:49+00:00" + }, + { + "name": "league/html-to-markdown", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/1dcd0f85de786f46a7f224a27cc3d709ddd2a68c", + "reference": "1dcd0f85de786f46a7f224a27cc3d709ddd2a68c", "shasum": "" }, "require": { @@ -1761,7 +1877,7 @@ "html", "markdown" ], - "time": "2019-11-02T14:54:14+00:00" + "time": "2019-12-28T01:32:28+00:00" }, { "name": "liip/imagine-bundle", @@ -2409,17 +2525,79 @@ "time": "2019-08-09T12:19:19+00:00" }, { - "name": "php-http/discovery", - "version": "1.7.2", + "name": "paragonie/constant_time_encoding", + "version": "v2.3.0", "source": { "type": "git", - "url": "https://github.com/php-http/discovery.git", - "reference": "16a3327861ae291006a2df8fc22e991806f720d7" + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/discovery/zipball/16a3327861ae291006a2df8fc22e991806f720d7", - "reference": "16a3327861ae291006a2df8fc22e991806f720d7", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2", + "reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7", + "vimeo/psalm": "^1|^2|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "time": "2019-11-06T19:20:29+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "2e0ad94833ec8473280454590a9011a0cd2dfc56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/2e0ad94833ec8473280454590a9011a0cd2dfc56", + "reference": "2e0ad94833ec8473280454590a9011a0cd2dfc56", "shasum": "" }, "require": { @@ -2471,7 +2649,7 @@ "message", "psr7" ], - "time": "2019-12-27T09:22:53+00:00" + "time": "2019-12-27T19:51:10+00:00" }, { "name": "php-http/httplug", @@ -2896,16 +3074,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.3", + "version": "4.3.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62" + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62", - "reference": "2ecaa9fef01634c83bfa8dc1fe35fb5cef223a62", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", "shasum": "" }, "require": { @@ -2917,6 +3095,7 @@ "require-dev": { "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", "phpunit/phpunit": "^6.4" }, "type": "library", @@ -2943,7 +3122,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2019-12-20T13:40:23+00:00" + "time": "2019-12-28T18:55:12+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -3382,6 +3561,71 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "r/u2f-two-factor-bundle", + "version": "dev-u2f-api", + "source": { + "type": "git", + "url": "https://github.com/stephanvierkant/u2f-two-factor-bundle.git", + "reference": "81212afd7897911eb6bbf3f8b315ae336cb3e45b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stephanvierkant/u2f-two-factor-bundle/zipball/81212afd7897911eb6bbf3f8b315ae336cb3e45b", + "reference": "81212afd7897911eb6bbf3f8b315ae336cb3e45b", + "shasum": "" + }, + "require": { + "doctrine/collections": "^1.6", + "doctrine/common": "*", + "ext-json": "*", + "php": "^7.1.3", + "scheb/two-factor-bundle": "^3.2.0|^4.0.0", + "symfony/framework-bundle": "^3.4|^4.0", + "symfony/templating": "^3.4|^4.0", + "yubico/u2flib-server": "^1.0.0" + }, + "conflict": { + "godzillante/u2f-two-factor-bundle": "*", + "tubssz/u2f-two-factor-bundle": "*" + }, + "require-dev": { + "phpstan/phpstan": "^0.11.6" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "R\\U2FTwoFactorBundle\\": "" + } + }, + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Uliczka", + "email": "nils.uliczka@darookee.net" + }, + { + "name": "Francesco De Francesco", + "email": "francesco.defrancesco@gmail.com" + } + ], + "description": "Use U2F-Keys as 2FA for Symfony2, using scheb/two-factor-bundle", + "homepage": "https://github.com/darookee/u2f-two-factor-bundle", + "keywords": [ + "authentication", + "fido", + "symfony2", + "two-factor", + "two-step", + "yubikey" + ], + "support": { + "source": "https://github.com/stephanvierkant/u2f-two-factor-bundle/tree/u2f-api" + }, + "time": "2019-07-17T10:00:04+00:00" + }, { "name": "s9e/regexp-builder", "version": "1.4.3", @@ -3489,17 +3733,85 @@ "time": "2019-12-26T19:14:01+00:00" }, { - "name": "sensio/framework-extra-bundle", - "version": "v5.5.2", + "name": "scheb/two-factor-bundle", + "version": "v4.11.1", "source": { "type": "git", - "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", - "reference": "92acfcc610e2180c52790ec3ff2e893f67e76b32" + "url": "https://github.com/scheb/two-factor-bundle.git", + "reference": "f9198cfcd5b2a92691926fd10406e8817232ac16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/92acfcc610e2180c52790ec3ff2e893f67e76b32", - "reference": "92acfcc610e2180c52790ec3ff2e893f67e76b32", + "url": "https://api.github.com/repos/scheb/two-factor-bundle/zipball/f9198cfcd5b2a92691926fd10406e8817232ac16", + "reference": "f9198cfcd5b2a92691926fd10406e8817232ac16", + "shasum": "" + }, + "require": { + "lcobucci/jwt": "^3.2", + "paragonie/constant_time_encoding": "^2.2", + "php": "^7.1.3", + "spomky-labs/otphp": "^9.1|^10.0", + "symfony/config": "^3.4|^4.0|^5.0", + "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/event-dispatcher": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^3.4|^4.0|^5.0", + "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^3.4|^4.0|^5.0", + "symfony/property-access": "^3.4|^4.0|^5.0", + "symfony/security-bundle": "^3.4|^4.0|^5.0", + "symfony/twig-bundle": "^3.4|^4.0|^5.0" + }, + "require-dev": { + "doctrine/lexer": "^1.0.1", + "doctrine/orm": "^2.6", + "escapestudios/symfony2-coding-standard": "^3.9", + "phpunit/phpunit": "^7.0|^8.0", + "squizlabs/php_codesniffer": "^3.5", + "swiftmailer/swiftmailer": "^6.0", + "symfony/yaml": "^3.4|^4.0|^5.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Scheb\\TwoFactorBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Scheb", + "email": "me@christianscheb.de" + } + ], + "description": "Provides two-factor authentication for Symfony applications", + "homepage": "https://github.com/scheb/two-factor-bundle", + "keywords": [ + "Authentication", + "security", + "symfony", + "two-factor", + "two-step" + ], + "time": "2019-12-19T12:07:42+00:00" + }, + { + "name": "sensio/framework-extra-bundle", + "version": "v5.5.3", + "source": { + "type": "git", + "url": "https://github.com/sensiolabs/SensioFrameworkExtraBundle.git", + "reference": "98f0807137b13d0acfdf3c255a731516e97015de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sensiolabs/SensioFrameworkExtraBundle/zipball/98f0807137b13d0acfdf3c255a731516e97015de", + "reference": "98f0807137b13d0acfdf3c255a731516e97015de", "shasum": "" }, "require": { @@ -3564,7 +3876,7 @@ "annotations", "controllers" ], - "time": "2019-12-12T16:21:49+00:00" + "time": "2019-12-27T08:57:19+00:00" }, { "name": "sensiolabs/security-checker", @@ -3667,6 +3979,67 @@ ], "time": "2019-12-08T15:52:26+00:00" }, + { + "name": "spomky-labs/otphp", + "version": "v9.1.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/otphp.git", + "reference": "48d463cf909320399fe08eab2e1cd18d899d5068" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/otphp/zipball/48d463cf909320399fe08eab2e1cd18d899d5068", + "reference": "48d463cf909320399fe08eab2e1cd18d899d5068", + "shasum": "" + }, + "require": { + "beberlei/assert": "^2.4|^3.0", + "paragonie/constant_time_encoding": "^2.0", + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^6.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "OTPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/otphp/contributors" + } + ], + "description": "A PHP library for generating one time passwords according to RFC 4226 (HOTP Algorithm) and the RFC 6238 (TOTP Algorithm) and compatible with Google Authenticator", + "homepage": "https://github.com/Spomky-Labs/otphp", + "keywords": [ + "FreeOTP", + "RFC 4226", + "RFC 6238", + "google authenticator", + "hotp", + "otp", + "totp" + ], + "time": "2019-03-18T10:08:51+00:00" + }, { "name": "symfony/apache-pack", "version": "v1.0.1", @@ -7646,16 +8019,16 @@ }, { "name": "twig/cssinliner-extra", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/twigphp/cssinliner-extra.git", - "reference": "431402520025e077268de4978a4206e5fb8c0103" + "reference": "d7c951e935d65c0cfd9a63bb08541a297f230f3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/431402520025e077268de4978a4206e5fb8c0103", - "reference": "431402520025e077268de4978a4206e5fb8c0103", + "url": "https://api.github.com/repos/twigphp/cssinliner-extra/zipball/d7c951e935d65c0cfd9a63bb08541a297f230f3a", + "reference": "d7c951e935d65c0cfd9a63bb08541a297f230f3a", "shasum": "" }, "require": { @@ -7664,7 +8037,7 @@ "twig/twig": "^2.4|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4@dev" + "symfony/phpunit-bridge": "^4.4|^5.0" }, "type": "library", "extra": { @@ -7696,20 +8069,20 @@ "inlining", "twig" ], - "time": "2019-10-17T07:27:07+00:00" + "time": "2019-12-27T07:33:44+00:00" }, { "name": "twig/extra-bundle", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/twigphp/twig-extra-bundle.git", - "reference": "c56821429490e351003a09b7ed0c917feec2355f" + "reference": "ce5c97dd566d9acd5d1fbd5eb76b6d264614725a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/c56821429490e351003a09b7ed0c917feec2355f", - "reference": "c56821429490e351003a09b7ed0c917feec2355f", + "url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/ce5c97dd566d9acd5d1fbd5eb76b6d264614725a", + "reference": "ce5c97dd566d9acd5d1fbd5eb76b6d264614725a", "shasum": "" }, "require": { @@ -7719,11 +8092,11 @@ "twig/twig": "^2.4|^3.0" }, "require-dev": { - "twig/cssinliner-extra": "^2.12|^3.0@dev", - "twig/html-extra": "^2.12@dev|^3.0@dev", - "twig/inky-extra": "^2.12@dev|^3.0@dev", - "twig/intl-extra": "^2.12@dev|^3.0@dev", - "twig/markdown-extra": "^2.12@dev|^3.0@dev" + "twig/cssinliner-extra": "^2.12|^3.0", + "twig/html-extra": "^2.12|^3.0", + "twig/inky-extra": "^2.12|^3.0", + "twig/intl-extra": "^2.12|^3.0", + "twig/markdown-extra": "^2.12|^3.0" }, "type": "symfony-bundle", "extra": { @@ -7755,20 +8128,20 @@ "extra", "twig" ], - "time": "2019-10-17T07:30:08+00:00" + "time": "2019-12-28T07:09:27+00:00" }, { "name": "twig/inky-extra", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/twigphp/inky-extra.git", - "reference": "7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b" + "reference": "68d7e3a00cb66dab07093c0c88059f4e02d71b39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b", - "reference": "7e33cb931f29e8cbc1f68eafa30e0ca7f7c6ad3b", + "url": "https://api.github.com/repos/twigphp/inky-extra/zipball/68d7e3a00cb66dab07093c0c88059f4e02d71b39", + "reference": "68d7e3a00cb66dab07093c0c88059f4e02d71b39", "shasum": "" }, "require": { @@ -7777,7 +8150,7 @@ "twig/twig": "^2.4|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4@dev" + "symfony/phpunit-bridge": "^4.4|^5.0" }, "type": "library", "extra": { @@ -7810,20 +8183,20 @@ "inky", "twig" ], - "time": "2019-10-17T07:27:12+00:00" + "time": "2019-12-27T07:33:44+00:00" }, { "name": "twig/intl-extra", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/twigphp/intl-extra.git", - "reference": "642552fa2834d9f56a727645f63e73d59672fb52" + "reference": "291d79ef98891da3efe14f0771fbe03a25fe6bec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/642552fa2834d9f56a727645f63e73d59672fb52", - "reference": "642552fa2834d9f56a727645f63e73d59672fb52", + "url": "https://api.github.com/repos/twigphp/intl-extra/zipball/291d79ef98891da3efe14f0771fbe03a25fe6bec", + "reference": "291d79ef98891da3efe14f0771fbe03a25fe6bec", "shasum": "" }, "require": { @@ -7832,7 +8205,7 @@ "twig/twig": "^2.4|^3.0" }, "require-dev": { - "symfony/phpunit-bridge": "^4.4@dev" + "symfony/phpunit-bridge": "^4.4|^5.0" }, "type": "library", "extra": { @@ -7863,20 +8236,20 @@ "intl", "twig" ], - "time": "2019-11-15T20:33:33+00:00" + "time": "2019-12-28T07:09:27+00:00" }, { "name": "twig/markdown-extra", - "version": "v3.0.0", + "version": "v3.0.1", "source": { "type": "git", "url": "https://github.com/twigphp/markdown-extra.git", - "reference": "609989e5bdc8cdb282e37fdc54e3591c244b5501" + "reference": "e3f6eb3f65eb2c165451c8417d918fb96fbd6d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/609989e5bdc8cdb282e37fdc54e3591c244b5501", - "reference": "609989e5bdc8cdb282e37fdc54e3591c244b5501", + "url": "https://api.github.com/repos/twigphp/markdown-extra/zipball/e3f6eb3f65eb2c165451c8417d918fb96fbd6d4d", + "reference": "e3f6eb3f65eb2c165451c8417d918fb96fbd6d4d", "shasum": "" }, "require": { @@ -7888,7 +8261,7 @@ "league/commonmark": "^1.0", "league/html-to-markdown": "^4.8", "michelf/php-markdown": "^1.8", - "symfony/phpunit-bridge": "^4.4@dev" + "symfony/phpunit-bridge": "^4.4|^5.0" }, "type": "library", "extra": { @@ -7920,20 +8293,20 @@ "markdown", "twig" ], - "time": "2019-10-17T07:30:08+00:00" + "time": "2019-12-28T07:09:27+00:00" }, { "name": "twig/twig", - "version": "v2.12.2", + "version": "v2.12.3", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "d761fd1f1c6b867ae09a7d8119a6d95d06dc44ed" + "reference": "97b6311585cae66a26833b14b33785f5797f7d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/d761fd1f1c6b867ae09a7d8119a6d95d06dc44ed", - "reference": "d761fd1f1c6b867ae09a7d8119a6d95d06dc44ed", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/97b6311585cae66a26833b14b33785f5797f7d39", + "reference": "97b6311585cae66a26833b14b33785f5797f7d39", "shasum": "" }, "require": { @@ -7943,8 +8316,7 @@ }, "require-dev": { "psr/container": "^1.0", - "symfony/debug": "^3.4|^4.2", - "symfony/phpunit-bridge": "^4.4@dev|^5.0" + "symfony/phpunit-bridge": "^4.4|^5.0" }, "type": "library", "extra": { @@ -7973,7 +8345,6 @@ }, { "name": "Twig Team", - "homepage": "https://twig.symfony.com/contributors", "role": "Contributors" }, { @@ -7987,7 +8358,7 @@ "keywords": [ "templating" ], - "time": "2019-11-11T16:52:09+00:00" + "time": "2019-12-28T07:12:03+00:00" }, { "name": "webmozart/assert", @@ -8037,6 +8408,43 @@ ], "time": "2019-11-24T13:36:37+00:00" }, + { + "name": "yubico/u2flib-server", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/Yubico/php-u2flib-server.git", + "reference": "55d813acf68212ad2cadecde07551600d6971939" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/55d813acf68212ad2cadecde07551600d6971939", + "reference": "55d813acf68212ad2cadecde07551600d6971939", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 1", + "php": ">=5.6" + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "vimeo/psalm": "^0|^1|^2" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Library for U2F implementation", + "homepage": "https://developers.yubico.com/php-u2flib-server", + "time": "2018-09-07T08:16:44+00:00" + }, { "name": "zendframework/zend-code", "version": "3.4.1", @@ -8092,6 +8500,7 @@ "code", "zf" ], + "abandoned": "laminas/laminas-code", "time": "2019-12-10T19:21:15+00:00" }, { @@ -8146,6 +8555,7 @@ "events", "zf2" ], + "abandoned": "laminas/laminas-eventmanager", "time": "2018-04-25T15:33:34+00:00" } ], @@ -9495,22 +9905,23 @@ }, { "name": "vimeo/psalm", - "version": "3.7.2", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "d9cae720c1af31db9ba27c2bc1fcf9b0dd092fb0" + "reference": "8e54e3aa060fc490d86d0e2abbf62750516d40fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/d9cae720c1af31db9ba27c2bc1fcf9b0dd092fb0", - "reference": "d9cae720c1af31db9ba27c2bc1fcf9b0dd092fb0", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/8e54e3aa060fc490d86d0e2abbf62750516d40fd", + "reference": "8e54e3aa060fc490d86d0e2abbf62750516d40fd", "shasum": "" }, "require": { "amphp/amp": "^2.1", "amphp/byte-stream": "^1.5", "composer/xdebug-handler": "^1.1", + "ext-dom": "*", "felixfbecker/advanced-json-rpc": "^3.0.3", "felixfbecker/language-server-protocol": "^1.4", "netresearch/jsonmapper": "^1.0", @@ -9531,6 +9942,7 @@ "ext-curl": "*", "friendsofphp/php-cs-fixer": "^2.15", "phpmyadmin/sql-parser": "^5.0", + "phpspec/prophecy": ">=1.9.0", "phpunit/phpunit": "^7.5 || ^8.0", "psalm/plugin-phpunit": "^0.6", "slevomat/coding-standard": "^5.0", @@ -9561,7 +9973,8 @@ "Psalm\\": "src/Psalm" }, "files": [ - "src/functions.php" + "src/functions.php", + "src/spl_object_id.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -9579,7 +9992,7 @@ "inspection", "php" ], - "time": "2019-12-03T13:33:31+00:00" + "time": "2019-12-29T16:11:07+00:00" }, { "name": "webmozart/glob", @@ -9679,6 +10092,7 @@ "minimum-stability": "stable", "stability-flags": { "gregwar/captcha-bundle": 20, + "r/u2f-two-factor-bundle": 20, "roave/security-advisories": 20 }, "prefer-stable": false, diff --git a/config/bundles.php b/config/bundles.php index 84926cba..e9d60242 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -21,5 +21,7 @@ return [ Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], Gregwar\CaptchaBundle\GregwarCaptchaBundle::class => ['all' => true], Doctrine\Bundle\DoctrineCacheBundle\DoctrineCacheBundle::class => ['all' => true], + Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], + R\U2FTwoFactorBundle\RU2FTwoFactorBundle::class => ['all' => true], Translation\Bundle\TranslationBundle::class => ['all' => true], ]; diff --git a/config/packages/r_u2f_two_factor.yaml b/config/packages/r_u2f_two_factor.yaml new file mode 100644 index 00000000..d3b962b2 --- /dev/null +++ b/config/packages/r_u2f_two_factor.yaml @@ -0,0 +1,4 @@ +ru2_f_two_factor: + formTemplate: "/security/U2F/u2f_login.html.twig" + registerTemplate: "/security/U2F/u2f_register.html.twig" + authCodeParameter: _auth_code \ No newline at end of file diff --git a/config/packages/scheb_two_factor.yaml b/config/packages/scheb_two_factor.yaml new file mode 100644 index 00000000..41feb3d2 --- /dev/null +++ b/config/packages/scheb_two_factor.yaml @@ -0,0 +1,25 @@ +# See the configuration reference at https://github.com/scheb/two-factor-bundle/blob/master/Resources/doc/configuration.md +scheb_two_factor: + security_tokens: + - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken + # If you're using guard-based authentication, you have to use this one: + # - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken + + google: + enabled: true # If Google Authenticator should be enabled, default false + server_name: '%partdb_title%' # Server name used in QR code + issuer: 'Part-DB' # Issuer name used in QR code + digits: 6 # Number of digits in authentication code + window: 1 # How many codes before/after the current one would be accepted as valid + template: security/2fa_form.html.twig + + backup_codes: + enabled: true # If the backup code feature should be enabled + + trusted_device: + enabled: true # If the trusted device feature should be enabled + lifetime: 5184000 # Lifetime of the trusted device token + extend_lifetime: false # Automatically extend lifetime of the trusted cookie on re-login + cookie_name: trusted_device # Name of the trusted device cookie + cookie_secure: false # Set the 'Secure' (HTTPS Only) flag on the trusted device cookie + cookie_same_site: "lax" # The same-site option of the cookie, can be "lax" or "strict" \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 5fd51805..b423c3ae 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -18,6 +18,11 @@ security: anonymous: true user_checker: App\Security\UserChecker + two_factor: + auth_form_path: 2fa_login + check_path: 2fa_login_check + csrf_token_generator: security.csrf.token_manager + # activate different ways to authenticate #http_basic: true @@ -42,5 +47,10 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # This makes the logout route available during two-factor authentication, allows the user to cancel + - { path: ^/logout, role: IS_AUTHENTICATED_ANONYMOUSLY } + # This ensures that the form can only be accessed when two-factor authentication is in progress + - { path: "^/\\w{2}/2fa", role: IS_AUTHENTICATED_2FA_IN_PROGRESS } + # We get into trouble with the U2F authentication, if the calls to the trees trigger an 2FA login + # This settings should not do much harm, because a read only access to show available data structures is not really critical + - { path: "^/\\w{2}/tree", role: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/config/routes/scheb_two_factor.yaml b/config/routes/scheb_two_factor.yaml new file mode 100644 index 00000000..18482a5b --- /dev/null +++ b/config/routes/scheb_two_factor.yaml @@ -0,0 +1,11 @@ +2fa_login: + path: /{_locale}/2fa + defaults: + _controller: "scheb_two_factor.form_controller:form" + +2fa_login_check: + path: /{_locale}/2fa_check + +r_u2f_register: + resource: "@RU2FTwoFactorBundle/Resources/config/routing.yml" + prefix: /{_locale}/user \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index 3a4e69cd..db0a8adb 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -26,6 +26,8 @@ services: _defaults: autowire: true # Automatically injects dependencies in your services. autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + bind: + bool $demo_mode: '%demo_mode%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name @@ -120,6 +122,11 @@ services: arguments: $mimeTypes: '@mime_types' + App\Services\TFA\BackupCodeGenerator: + arguments: + $code_length: 8 + $code_count: 15 + App\Services\TranslationExtractor\PermissionExtractor: tags: - { name: 'translation.extractor', alias: 'permissionExtractor'} \ No newline at end of file diff --git a/package.json b/package.json index 9d9f378a..241843ab 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", @@ -43,7 +44,9 @@ "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" + "typescript": "^3.3.4000", + "u2f-api": "^1.1.1" } } diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 6cba6a67..127605c6 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -21,7 +21,11 @@ namespace App\Controller; +use App\Entity\Parts\Part; +use App\Entity\UserSystem\U2FKey; +use App\Entity\UserSystem\User; use App\Services\PasswordResetManager; +use App\Services\TFA\BackupCodeManager; use Doctrine\ORM\EntityManagerInterface; use Gregwar\CaptchaBundle\Type\CaptchaType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -29,6 +33,7 @@ use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Mailer\MailerInterface; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a93c8b3e..775493c9 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -25,16 +25,22 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\UserAttachment; use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; +use App\Form\TFAGoogleSettingsType; use App\Form\UserAdminForm; 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 \Exception; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; use Symfony\Component\Asset\Packages; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -61,6 +67,29 @@ class UserController extends AdminPages\BaseAdminController */ public function edit(User $entity, Request $request, EntityManagerInterface $em) { + //Handle 2FA disabling + + if($request->request->has('reset_2fa')) { + //Check if the admin has the needed permissions + $this->denyAccessUnlessGranted('set_password', $entity); + if ($this->isCsrfTokenValid('reset_2fa'.$entity->getId(), $request->request->get('_token'))) { + //Disable Google authenticator + $entity->setGoogleAuthenticatorSecret(null); + $entity->setBackupCodes([]); + //Remove all U2F keys + foreach($entity->getU2FKeys() as $key) { + $em->remove($key); + } + //Invalidate trusted devices + $entity->invalidateTrustedDeviceTokens(); + $em->flush(); + + $this->addFlash('success', 'user.edit.reset_success'); + } else { + $this->addFlash('danger', 'csfr_invalid'); + } + } + return $this->_edit($entity, $request, $em); } @@ -76,7 +105,7 @@ class UserController extends AdminPages\BaseAdminController } /** - * @Route("/{id}", name="user_delete", methods={"DELETE"}) + * @Route("/{id}", name="user_delete", methods={"DELETE"}, requirements={"id"="\d+"}) */ public function delete(Request $request, User $entity, StructuralElementRecursionHelper $recursionHelper) { @@ -147,94 +176,6 @@ class UserController extends AdminPages\BaseAdminController ]); } - /** - * @Route("/settings", name="user_settings") - */ - public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder) - { - /** - * @var User - */ - $user = $this->getUser(); - - $page_need_reload = false; - - 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'); - - /*************************** - * User settings form - ***************************/ - - $form = $this->createForm(UserSettingsType::class, $user); - - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - //Check if user theme setting has changed - if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) { - $page_need_reload = true; - } - - $em->flush(); - $this->addFlash('success', 'user.settings.saved_flash'); - } - - /***************************** - * Password change form - ****************************/ - - $demo_mode = $this->getParameter('demo_mode'); - - $pw_form = $this->createFormBuilder() - ->add('old_password', PasswordType::class, [ - 'label' => 'user.settings.pw_old.label', - 'disabled' => $demo_mode, - 'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted. - ->add('new_password', RepeatedType::class, [ - 'disabled' => $demo_mode, - 'type' => PasswordType::class, - 'first_options' => ['label' => 'user.settings.pw_new.label'], - 'second_options' => ['label' => 'user.settings.pw_confirm.label'], - 'invalid_message' => 'password_must_match', - 'constraints' => [new Length([ - 'min' => 6, - 'max' => 128, - ])], - ]) - ->add('submit', SubmitType::class, ['label' => 'save']) - ->getForm(); - - $pw_form->handleRequest($request); - - //Check if password if everything was correct, then save it to User and DB - if ($pw_form->isSubmitted() && $pw_form->isValid()) { - $password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData()); - $user->setPassword($password); - - //After the change reset the password change needed setting - $user->setNeedPwChange(false); - - $em->persist($user); - $em->flush(); - $this->addFlash('success', 'user.settings.pw_changed_flash'); - } - - /****************************** - * Output both forms - *****************************/ - - return $this->render('Users/user_settings.html.twig', [ - 'settings_form' => $form->createView(), - 'pw_form' => $pw_form->createView(), - 'page_need_reload' => $page_need_reload, - ]); - } - /** * Get either a Gravatar URL or complete image tag for a specified email address. * diff --git a/src/Controller/UserSettingsController.php b/src/Controller/UserSettingsController.php new file mode 100644 index 00000000..afa62297 --- /dev/null +++ b/src/Controller/UserSettingsController.php @@ -0,0 +1,308 @@ +demo_mode = $demo_mode; + } + + /** + * @Route("/2fa_backup_codes", name="show_backup_codes") + */ + public function showBackupCodes() + { + $user = $this->getUser(); + + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + if (!$user instanceof User) { + return new \RuntimeException('This controller only works only for Part-DB User objects!'); + } + + if (empty($user->getBackupCodes())) { + $this->addFlash('error', 'tfa_backup.no_codes_enabled'); + 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("/u2f_delete", name="u2f_delete", methods={"DELETE"}) + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function removeU2FToken(Request $request, EntityManagerInterface $entityManager, BackupCodeManager $backupCodeManager) + { + if($this->demo_mode) { + throw new \RuntimeException('You can not do 2FA things in demo mode'); + } + + $user = $this->getUser(); + + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + if (!$user instanceof User) { + throw new \RuntimeException('This controller only works only for Part-DB User objects!'); + } + + + if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) { + if($request->request->has('key_id')) { + $key_id = $request->request->get('key_id'); + $key_repo = $entityManager->getRepository(U2FKey::class); + /** @var U2FKey|null $u2f */ + $u2f = $key_repo->find($key_id); + if($u2f === null) { + $this->addFlash('danger','tfa_u2f.u2f_delete.not_existing'); + throw new \RuntimeException('Key not existing!'); + } + + //User can only delete its own U2F keys + if ($u2f->getUser() !== $user) { + $this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied'); + throw new \RuntimeException('You can only delete your own U2F keys!'); + } + + $backupCodeManager->disableBackupCodesIfUnused($user); + $entityManager->remove($u2f); + $entityManager->flush(); + $this->addFlash('success', 'tfa.u2f.u2f_delete.success'); + } + } else { + $this->addFlash('error','csfr_invalid'); + } + + return $this->redirectToRoute('user_settings'); + } + + /** + * @Route("/invalidate_trustedDevices", name="tfa_trustedDevices_invalidate", methods={"DELETE"}) + */ + public function resetTrustedDevices(Request $request, EntityManagerInterface $entityManager) + { + if($this->demo_mode) { + throw new \RuntimeException('You can not do 2FA things in demo mode'); + } + + $user = $this->getUser(); + + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + if (!$user instanceof User) { + return new \RuntimeException('This controller only works only for Part-DB User objects!'); + } + + + if ($this->isCsrfTokenValid('devices_reset'.$user->getId(), $request->request->get('_token'))) { + $user->invalidateTrustedDeviceTokens(); + $entityManager->flush(); + $this->addFlash('success', 'tfa_trustedDevice.invalidate.success'); + } else { + $this->addFlash('error','csfr_invalid'); + } + + return $this->redirectToRoute('user_settings'); + } + + /** + * @Route("/settings", name="user_settings") + */ + public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator, BackupCodeManager $backupCodeManager) + { + /** + * @var User + */ + $user = $this->getUser(); + + $page_need_reload = false; + + //When user change its settings, he should be logged in fully. + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + if (!$user instanceof User) { + throw new \RuntimeException('This controller only works only for Part-DB User objects!'); + } + + /*************************** + * User settings form + ***************************/ + + $form = $this->createForm(UserSettingsType::class, $user); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid() && !$this->demo_mode) { + //Check if user theme setting has changed + if ($user->getTheme() !== $em->getUnitOfWork()->getOriginalEntityData($user)['theme']) { + $page_need_reload = true; + } + + $em->flush(); + $this->addFlash('success', 'user.settings.saved_flash'); + } + + /***************************** + * Password change form + ****************************/ + + $pw_form = $this->createFormBuilder() + //Username field for autocomplete + ->add('username', TextType::class, [ + 'data' => $user->getName(), + 'attr' => ['autocomplete' => 'username'], + 'disabled' => true, + 'row_attr' => ['class' => 'd-none'] + ]) + ->add('old_password', PasswordType::class, [ + 'label' => 'user.settings.pw_old.label', + 'disabled' => $this->demo_mode, + 'attr' => ['autocomplete' => 'current-password'], + 'constraints' => [new UserPassword()], ]) //This constraint checks, if the current user pw was inputted. + ->add('new_password', RepeatedType::class, [ + 'disabled' => $this->demo_mode, + 'type' => PasswordType::class, + 'first_options' => ['label' => 'user.settings.pw_new.label'], + 'second_options' => ['label' => 'user.settings.pw_confirm.label'], + 'invalid_message' => 'password_must_match', + 'options' => [ + 'attr' => ['autocomplete' => 'new-password'] + ], + 'constraints' => [new Length([ + 'min' => 6, + 'max' => 128, + ])], + ]) + ->add('submit', SubmitType::class, ['label' => 'save']) + ->getForm(); + + $pw_form->handleRequest($request); + + //Check if password if everything was correct, then save it to User and DB + if ($pw_form->isSubmitted() && $pw_form->isValid() && !$this->demo_mode) { + $password = $passwordEncoder->encodePassword($user, $pw_form['new_password']->getData()); + $user->setPassword($password); + + //After the change reset the password change needed setting + $user->setNeedPwChange(false); + + $em->persist($user); + $em->flush(); + $this->addFlash('success', 'user.settings.pw_changed_flash'); + } + + //Handle 2FA things + $google_form = $this->createForm(TFAGoogleSettingsType::class, $user); + $google_enabled = $user->isGoogleAuthenticatorEnabled(); + if (!$form->isSubmitted() && !$google_enabled) { + $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); + $google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret()); + } + $google_form->handleRequest($request); + + if($google_form->isSubmitted() && $google_form->isValid() && !$this->demo_mode) { + 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'); + } + } + + $backup_form = $this->get('form.factory')->createNamedBuilder('backup_codes')->add('reset_codes', SubmitType::class,[ + 'label' => 'tfa_backup.regenerate_codes', + 'attr' => ['class' => 'btn-danger'], + 'disabled' => empty($user->getBackupCodes()) + ])->getForm(); + + $backup_form->handleRequest($request); + if ($backup_form->isSubmitted() && $backup_form->isValid() && !$this->demo_mode) { + $backupCodeManager->regenerateBackupCodes($user); + $em->flush(); + $this->addFlash('success', 'user.settings.2fa.backup_codes.regenerated'); + } + + + /****************************** + * Output both forms + *****************************/ + + 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, + + 'google_form' => $google_form->createView(), + 'backup_form' => $backup_form->createView(), + 'tfa_google' => [ + 'enabled' => $google_enabled, + 'qrContent' => $googleAuthenticator->getQRContent($user), + 'secret' => $user->getGoogleAuthenticatorSecret(), + 'username' => $user->getGoogleAuthenticatorUsername() + ] + ]); + } +} \ No newline at end of file diff --git a/src/Entity/Base/MasterAttachmentTrait.php b/src/Entity/Base/MasterAttachmentTrait.php index 92fa8e27..bdef230d 100644 --- a/src/Entity/Base/MasterAttachmentTrait.php +++ b/src/Entity/Base/MasterAttachmentTrait.php @@ -53,9 +53,9 @@ trait MasterAttachmentTrait * Sets the new master picture for this part. * * @param Attachment|null $new_master_attachment - * @return Part + * @return $this */ - public function setMasterPictureAttachment(?Attachment $new_master_attachment): self + public function setMasterPictureAttachment(?Attachment $new_master_attachment) { $this->master_picture_attachment = $new_master_attachment; diff --git a/src/Entity/Parts/Storelocation.php b/src/Entity/Parts/Storelocation.php index ad087f07..916a154c 100644 --- a/src/Entity/Parts/Storelocation.php +++ b/src/Entity/Parts/Storelocation.php @@ -107,11 +107,11 @@ class Storelocation extends PartsContainingDBElement protected $storage_type; /** - * @ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY") - * @ORM\JoinTable(name="part_lots", - * joinColumns={@ORM\JoinColumn(name="id_store_location", referencedColumnName="id")}, - * inverseJoinColumns={@ORM\JoinColumn(name="id_part", referencedColumnName="id")} - * ) + * //@ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY") + * //@ORM\JoinTable(name="part_lots", + * // joinColumns={@ORM\JoinColumn(name="id_store_location", referencedColumnName="id")}, + * // inverseJoinColumns={@ORM\JoinColumn(name="id_part", referencedColumnName="id")} + * //) */ protected $parts; diff --git a/src/Entity/Parts/Supplier.php b/src/Entity/Parts/Supplier.php index 707d217b..ad41d7a2 100644 --- a/src/Entity/Parts/Supplier.php +++ b/src/Entity/Parts/Supplier.php @@ -106,11 +106,11 @@ class Supplier extends Company protected $shipping_costs; /** - * @ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY") - * @ORM\JoinTable(name="orderdetails", - * joinColumns={@ORM\JoinColumn(name="id_supplier", referencedColumnName="id")}, - * inverseJoinColumns={@ORM\JoinColumn(name="part_id", referencedColumnName="id")} - * ) + * //@ORM\ManyToMany(targetEntity="Part", fetch="EXTRA_LAZY") + * //@ORM\JoinTable(name="orderdetails", + * // joinColumns={@ORM\JoinColumn(name="id_supplier", referencedColumnName="id")}, + * // inverseJoinColumns={@ORM\JoinColumn(name="part_id", referencedColumnName="id")} + * //) */ protected $parts; diff --git a/src/Entity/UserSystem/Group.php b/src/Entity/UserSystem/Group.php index 903fd359..259623ef 100644 --- a/src/Entity/UserSystem/Group.php +++ b/src/Entity/UserSystem/Group.php @@ -64,12 +64,40 @@ class Group extends StructuralDBElement implements HasPermissionsInterface */ protected $permissions; + /** + * @var bool If true all users associated with this group must have enabled some kind of 2 factor authentication + * @ORM\Column(type="boolean", name="enforce_2fa") + */ + protected $enforce2FA = false; + public function __construct() { parent::__construct(); $this->permissions = new PermissionsEmbed(); } + /** + * Check if the users of this group are enforced to have two factor authentification (2FA) enabled. + * @return bool + */ + public function isEnforce2FA(): bool + { + return $this->enforce2FA; + } + + /** + * Sets if the user of this group are enforced to have two factor authentification enabled. + * @param bool $enforce2FA True, if the users of this group are enforced to have 2FA enabled. + * @return $this + */ + public function setEnforce2FA(bool $enforce2FA): Group + { + $this->enforce2FA = $enforce2FA; + return $this; + } + + + /** * Returns the ID as an string, defined by the element class. * This should have a form like P000014, for a part with ID 14. diff --git a/src/Entity/UserSystem/U2FKey.php b/src/Entity/UserSystem/U2FKey.php new file mode 100644 index 00000000..f9d96606 --- /dev/null +++ b/src/Entity/UserSystem/U2FKey.php @@ -0,0 +1,183 @@ +keyHandle = $data->keyHandle; + $this->publicKey = $data->publicKey; + $this->certificate = $data->certificate; + $this->counter = $data->counter; + } + + /** @inheritDoc */ + public function getKeyHandle() + { + return $this->keyHandle; + } + + /** @inheritDoc */ + public function setKeyHandle($keyHandle) + { + $this->keyHandle = $keyHandle; + } + + /** @inheritDoc */ + public function getPublicKey() + { + return $this->publicKey; + } + + /** @inheritDoc */ + public function setPublicKey($publicKey) + { + $this->publicKey = $publicKey; + } + + /** @inheritDoc */ + public function getCertificate() + { + return $this->certificate; + } + + + /** @inheritDoc */ + public function setCertificate($certificate) + { + $this->certificate = $certificate; + } + + /** @inheritDoc */ + public function getCounter() + { + return $this->counter; + } + + /** @inheritDoc */ + public function setCounter($counter) + { + $this->counter = $counter; + } + + /** @inheritDoc */ + public function getName() + { + return $this->name; + } + + /** @inheritDoc */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Gets the user, this U2F key belongs to. + * @return User + */ + public function getUser() : User + { + return $this->user; + } + + /** + * The primary key ID of this key + * @return int + */ + public function getID() : int + { + return $this->id; + } + + /** + * Sets the user this U2F key belongs to. + * @param TwoFactorInterface $new_user + * @return $this + */ + public function setUser(TwoFactorInterface $new_user) : self + { + $this->user = $new_user; + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 745a060d..e933b583 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -53,28 +53,38 @@ namespace App\Entity\UserSystem; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\UserAttachment; +use App\Entity\Base\MasterAttachmentTrait; use App\Entity\Base\NamedDBElement; use App\Entity\PriceInformations\Currency; use App\Security\Interfaces\HasPermissionsInterface; use App\Validator\Constraints\Selectable; use App\Validator\Constraints\ValidPermission; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface; +use Scheb\TwoFactorBundle\Model\BackupCodeInterface; +use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface; +use Scheb\TwoFactorBundle\Model\PreferredProviderInterface; +use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Validator\Constraints as Assert; +use R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface as U2FTwoFactorInterface; /** * This entity represents a user, which can log in and have permissions. * Also this entity is able to save some informations about the user, like the names, email-address and other info. - * Also this entity is able to save some informations about the user, like the names, email-address and other info. * * @ORM\Entity(repositoryClass="App\Repository\UserRepository") * @ORM\Table("`users`") * @UniqueEntity("name", message="validator.user.username_already_used") */ -class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface +class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, + TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, U2FTwoFactorInterface, PreferredProviderInterface { + use MasterAttachmentTrait; + /** The User id of the anonymous user */ public const ID_ANONYMOUS = 1; @@ -172,6 +182,33 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe */ protected $group; + /** + * @var string|null The secret used for google authenticator + * @ORM\Column(name="google_authenticator_secret", type="string", nullable=true) + */ + protected $googleAuthenticatorSecret; + + /** + * @var string[]|null A list of backup codes that can be used, if the user has no access to its Google Authenticator device + * @ORM\Column(type="json") + */ + protected $backupCodes = []; + + /** @var \DateTime The time when the backup codes were generated + * @ORM\Column(type="datetime", nullable=true) + */ + protected $backupCodesGenerationDate; + + /** @var int The version of the trusted device cookie. Used to invalidate all trusted device cookies at once. + * @ORM\Column(type="integer") + */ + protected $trustedDeviceCookieVersion = 0; + + /** @var Collection + * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) + */ + protected $u2fKeys; + /** * @var array * @ORM\Column(type="json") @@ -227,6 +264,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe { parent::__construct(); $this->permissions = new PermissionsEmbed(); + $this->u2fKeys = new ArrayCollection(); } /** @@ -457,6 +495,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return sprintf('%s %s', $this->getFirstName(), $this->getLastName()); } + /** + * Change the username of this user + * @param string $new_name The new username. + * @return $this + */ public function setName(string $new_name): NamedDBElement { // Anonymous user is not allowed to change its username @@ -468,7 +511,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @return string + * Get the first name of the user. + * @return string|null */ public function getFirstName(): ?string { @@ -476,9 +520,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $first_name + * Change the first name of the user + * @param string $first_name The new first name * - * @return User + * @return $this */ public function setFirstName(?string $first_name): self { @@ -488,7 +533,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @return string + * Get the last name of the user + * @return string|null */ public function getLastName(): ?string { @@ -496,9 +542,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $last_name + * Change the last name of the user + * @param string $last_name The new last name * - * @return User + * @return $this */ public function setLastName(?string $last_name): self { @@ -508,6 +555,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** + * Gets the department of this user * @return string */ public function getDepartment(): ?string @@ -516,8 +564,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $department - * + * Change the department of the user + * @param string $department The new department * @return User */ public function setDepartment(?string $department): self @@ -528,6 +576,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** + * Get the email of the user. * @return string */ public function getEmail(): ?string @@ -536,9 +585,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $email - * - * @return User + * Change the email of the user + * @param string $email The new email adress + * @return $this */ public function setEmail(?string $email): self { @@ -548,7 +597,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @return string + * Gets the language the user prefers (as 2 letter ISO code). + * @return string|null The 2 letter ISO code of the preferred language (e.g. 'en' or 'de'). + * If null is returned, the user has not specified a language and the server wide language should be used. */ public function getLanguage(): ?string { @@ -556,19 +607,21 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $language - * + * Change the language the user prefers. + * @param string|null $language The new language as 2 letter ISO code (e.g. 'en' or 'de'). + * Set to null, to use the system wide language. * @return User */ public function setLanguage(?string $language): self { $this->language = $language; - return $this; } /** - * @return string + * Gets the timezone of the user + * @return string|null The timezone of the user (e.g. 'Europe/Berlin') or null if the user has not specified + * a timezone (then the global one should be used) */ public function getTimezone(): ?string { @@ -576,9 +629,9 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $timezone - * - * @return User + * Change the timezone of this user. + * @param string $timezone|null The new timezone (e.g. 'Europe/Berlin') or null to use the system wide one. + * @return $this */ public function setTimezone(?string $timezone): self { @@ -588,7 +641,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @return string + * Gets the theme the users wants to see. See self::AVAILABLE_THEMES for valid values. + * @return string|null The name of the theme the user wants to see, or null if the system wide should be used. */ public function getTheme(): ?string { @@ -596,9 +650,10 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** - * @param string $theme - * - * @return User + * Change the theme the user wants to see. + * @param string|null $theme The name of the theme (See See self::AVAILABLE_THEMES for valid values). Set to null + * if the system wide theme should be used. + * @return $this */ public function setTheme(?string $theme): self { @@ -607,11 +662,20 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } + /** + * Gets the group to which this user belongs to. + * @return Group|null The group of this user. Null if this user does not have a group. + */ public function getGroup(): ?Group { return $this->group; } + /** + * Sets the group of this user. + * @param Group|null $group The new group of this user. Set to null if this user should not have a group. + * @return $this + */ public function setGroup(?Group $group): self { $this->group = $group; @@ -619,10 +683,181 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe return $this; } + /** + * Returns a string representation of this user (the full name). + * E.g. 'Jane Doe (j.doe) [DISABLED] + * @return string + */ public function __toString() { $tmp = $this->isDisabled() ? ' [DISABLED]' : ''; - return $this->getFullName(true).$tmp; } + + /** + * Return true if the user should do two-factor authentication. + * + * @return bool + */ + public function isGoogleAuthenticatorEnabled(): bool + { + return $this->googleAuthenticatorSecret ? true : false; + } + + /** + * Return the user name that should be shown in Google Authenticator. + * @return string + */ + public function getGoogleAuthenticatorUsername(): string + { + return $this->getUsername(); + } + + /** + * Return the Google Authenticator secret + * When an empty string is returned, the Google authentication is disabled. + * + * @return string|null + */ + public function getGoogleAuthenticatorSecret(): ?string + { + return $this->googleAuthenticatorSecret; + } + + /** + * Sets the secret used for Google Authenticator. Set to null to disable Google Authenticator. + * @param string|null $googleAuthenticatorSecret + * @return $this + */ + public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): self + { + $this->googleAuthenticatorSecret = $googleAuthenticatorSecret; + return $this; + } + + /** + * Check if the given code is a valid backup code. + * + * @param string $code The code that should be checked. + * @return bool True if the backup code is valid. + */ + public function isBackupCode(string $code): bool + { + return in_array($code, $this->backupCodes); + } + + /** + * Invalidate a backup code. + * + * @param string $code The code that should be invalidated + */ + public function invalidateBackupCode(string $code): void + { + $key = array_search($code, $this->backupCodes); + if ($key !== false){ + unset($this->backupCodes[$key]); + } + } + + /** + * Returns the list of all valid backup codes + * @return string[] An array with all backup codes + */ + public function getBackupCodes() : array + { + return $this->backupCodes ?? []; + } + + /** + * Set the backup codes for this user. Existing backup codes are overridden. + * @param string[] $codes A + * @return $this + */ + public function setBackupCodes(array $codes) : self + { + $this->backupCodes = $codes; + if(empty($codes)) { + $this->backupCodesGenerationDate = null; + } else { + $this->backupCodesGenerationDate = new \DateTime(); + } + return $this; + } + + /** + * Return the date when the backup codes were generated. + * @return \DateTime|null + */ + public function getBackupCodesGenerationDate() : ?\DateTime + { + return $this->backupCodesGenerationDate; + } + + /** + * Return version for the trusted device token. Increase version to invalidate all trusted token of the user. + * @return int The version of trusted device token + */ + public function getTrustedTokenVersion(): int + { + return $this->trustedDeviceCookieVersion; + } + + /** + * Invalidate all trusted device tokens at once, by incrementing the token version. + * You have to flush the changes to database afterwards. + */ + public function invalidateTrustedDeviceTokens() : void + { + $this->trustedDeviceCookieVersion++; + } + + /** + * Check if U2F is enabled + * @return bool + */ + public function isU2FAuthEnabled(): bool + { + return count($this->u2fKeys) > 0; + } + + /** + * Get all U2F Keys that are associated with this user + * @return Collection + */ + public function getU2FKeys(): Collection + { + return $this->u2fKeys; + } + + /** + * Add a U2F key to this user. + * @param TwoFactorKeyInterface $key + */ + public function addU2FKey(TwoFactorKeyInterface $key): void + { + $this->u2fKeys->add($key); + } + + /** + * Remove a U2F key from this user. + * @param TwoFactorKeyInterface $key + */ + public function removeU2FKey(TwoFactorKeyInterface $key): void + { + $this->u2fKeys->removeElement($key); + } + + /** + * @inheritDoc + */ + public function getPreferredTwoFactorProvider(): ?string + { + //If U2F is available then prefer it + if($this->isU2FAuthEnabled()) { + return 'u2f_two_factor'; + } + + //Otherwise use other methods + return null; + } } diff --git a/src/EventSubscriber/PasswordChangeNeededSubscriber.php b/src/EventSubscriber/PasswordChangeNeededSubscriber.php new file mode 100644 index 00000000..67198245 --- /dev/null +++ b/src/EventSubscriber/PasswordChangeNeededSubscriber.php @@ -0,0 +1,145 @@ +security = $security; + $this->flashBag = $flashBag; + $this->httpUtils = $httpUtils; + } + + /** + * This function is called when the kernel encounters a request. + * It checks if the user must change its password or add an 2FA mehtod and redirect it to the user settings page, + * if needed. + * @param RequestEvent $event + */ + public function redirectToSettingsIfNeeded(RequestEvent $event) : void + { + $user = $this->security->getUser(); + $request = $event->getRequest(); + + if(!$event->isMasterRequest()) { + return; + } + if(!$user instanceof User) { + return; + } + + //Abort if we dont need to redirect the user. + if (!$user->isNeedPwChange() && !static::TFARedirectNeeded($user)) { + return; + } + + //Check for a whitelisted URL + foreach (static::ALLOWED_ROUTES as $route) { + //Dont do anything if we encounter an allowed route + if ($this->httpUtils->checkRequestPath($request, $route)) { + return; + } + } + + /* Dont redirect tree endpoints, as this would cause trouble and creates multiple flash + warnigs for one page reload */ + if(strpos($request->getUri(), '/tree/') !== false) { + return; + } + + //Show appropriate message to user about the reason he was redirected + if($user->isNeedPwChange()) { + $this->flashBag->add('warning', 'user.pw_change_needed.flash'); + } + + if(static::TFARedirectNeeded($user)) { + $this->flashBag->add('warning', 'user.2fa_needed.flash'); + } + + $event->setResponse($this->httpUtils->createRedirectResponse($request, static::REDIRECT_TARGET)); + + } + + /** + * Check if a redirect because of a missing 2FA method is needed. + * That is the case if the group of the user enforces 2FA, but the user has neither Google Authenticator nor an + * U2F key setup. + * @param User $user The user for which should be checked if it needs to be redirected. + * @return bool True if the user needs to be redirected. + */ + public static function TFARedirectNeeded(User $user) : bool + { + $tfa_enabled = $user->isU2FAuthEnabled() || $user->isGoogleAuthenticatorEnabled(); + + if ($user->getGroup() !== null && $user->getGroup()->isEnforce2FA() && !$tfa_enabled) { + return true; + } + + return false; + } + + /** + * @inheritDoc + */ + public static function getSubscribedEvents() + { + return [ + KernelEvents::REQUEST => 'redirectToSettingsIfNeeded', + ]; + } +} \ No newline at end of file diff --git a/src/EventSubscriber/U2FRegistrationSubscriber.php b/src/EventSubscriber/U2FRegistrationSubscriber.php new file mode 100644 index 00000000..04969fd6 --- /dev/null +++ b/src/EventSubscriber/U2FRegistrationSubscriber.php @@ -0,0 +1,81 @@ +router = $router; + $this->em = $entityManager; + $this->demo_mode = $demo_mode; + $this->flashBag = $flashBag; + } + + /** @return string[] **/ + public static function getSubscribedEvents(): array + { + return array( + 'r_u2f_two_factor.register' => 'onRegister', + ); + } + + public function onRegister(RegisterEvent $event): void + { + //Skip adding of U2F key on demo mode + if (!$this->demo_mode) { + $user = $event->getUser(); + $registration = $event->getRegistration(); + $newKey = new U2FKey(); + $newKey->fromRegistrationData($registration); + $newKey->setUser($user); + $newKey->setName($event->getKeyName()); + + // persist the new key + $this->em->persist($newKey); + $this->em->flush(); + $this->flashBag->add('success', 'tfa_u2f.key_added_successful'); + } + + // generate new response, here we redirect the user to the fos user + // profile + $response = new RedirectResponse($this->router->generate('user_settings')); + $event->setResponse($response); + } +} \ No newline at end of file diff --git a/src/Form/AdminPages/GroupAdminForm.php b/src/Form/AdminPages/GroupAdminForm.php index 095c2383..a3b070e8 100644 --- a/src/Form/AdminPages/GroupAdminForm.php +++ b/src/Form/AdminPages/GroupAdminForm.php @@ -23,12 +23,22 @@ namespace App\Form\AdminPages; use App\Entity\Base\NamedDBElement; use App\Form\Permissions\PermissionsType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\FormBuilderInterface; class GroupAdminForm extends BaseEntityAdminForm { protected function additionalFormElements(FormBuilderInterface $builder, array $options, NamedDBElement $entity) { + $is_new = null === $entity->getID(); + + $builder->add('enforce2FA', CheckboxType::class, ['required' => false, + 'label' => 'group.edit.enforce_2fa', + 'help' => 'entity.edit.enforce_2fa.help', + 'label_attr' => ['class' => 'checkbox-custom'], + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity) + ]); + $builder->add('permissions', PermissionsType::class, [ 'mapped' => false, 'data' => $builder->getData(), diff --git a/src/Form/TFAGoogleSettingsType.php b/src/Form/TFAGoogleSettingsType.php new file mode 100644 index 00000000..495e27c0 --- /dev/null +++ b/src/Form/TFAGoogleSettingsType.php @@ -0,0 +1,94 @@ +addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) { + $form = $event->getForm(); + /** @var User $user */ + $user = $event->getData(); + + //Only show setup fields, when google authenticator is not enabled + if(!$user->isGoogleAuthenticatorEnabled()) { + $form->add( + 'google_confirmation', + TextType::class, + [ + 'mapped' => false, + 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*', 'autocomplete' => 'off'], + 'constraints' => [new ValidGoogleAuthCode()] + ] + ); + + $form->add( + 'googleAuthenticatorSecret', + HiddenType::class, + [ + 'disabled' => false, + ] + ); + + $form->add('submit', SubmitType::class, [ + 'label' => 'tfa_google.enable' + ]); + } else { + $form->add('submit', SubmitType::class, [ + 'label' =>'tfa_google.disable', + 'attr' => ['class' => 'btn-danger'] + ]); + } + }); + + //$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/Migrations/Version20191214153125.php b/src/Migrations/Version20191214153125.php new file mode 100644 index 00000000..ae2adf2e --- /dev/null +++ b/src/Migrations/Version20191214153125.php @@ -0,0 +1,59 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('CREATE TABLE u2f_keys (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, key_handle VARCHAR(255) NOT NULL, public_key VARCHAR(255) NOT NULL, certificate LONGTEXT NOT NULL, counter VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_4F4ADB4BA76ED395 (user_id), UNIQUE INDEX user_unique (user_id, key_handle), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB'); + $this->addSql('ALTER TABLE u2f_keys ADD CONSTRAINT FK_4F4ADB4BA76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id)'); + $this->addSql('ALTER TABLE `groups` ADD enforce_2fa TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE users ADD google_authenticator_secret VARCHAR(255) DEFAULT NULL, ADD backup_codes LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', ADD backup_codes_generation_date DATETIME DEFAULT NULL, ADD trusted_device_cookie_version INT NOT NULL'); + } + + public function down(Schema $schema) : void + { + // this down() migration is auto-generated, please modify it to your needs + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); + + $this->addSql('DROP TABLE u2f_keys'); + $this->addSql('ALTER TABLE `groups` DROP enforce_2fa'); + $this->addSql('ALTER TABLE `users` DROP google_authenticator_secret, DROP backup_codes, DROP backup_codes_generation_date, DROP trusted_device_cookie_version'); + } +} diff --git a/src/Services/TFA/BackupCodeGenerator.php b/src/Services/TFA/BackupCodeGenerator.php new file mode 100644 index 00000000..c4009223 --- /dev/null +++ b/src/Services/TFA/BackupCodeGenerator.php @@ -0,0 +1,77 @@ + 32) { + throw new \RuntimeException('Backup code can have maximum 32 digits!'); + } + if ($code_length < 6) { + throw new \RuntimeException('Code must have at least 6 digits to ensure security!'); + } + + $this->code_count = $code_count; + $this->code_length = $code_length; + } + + /** + * Generates a single backup code. + * It is a random hexadecimal value with the digit count configured in constructor + * @return string The generated backup code (e.g. 1f3870be2) + * @throws \Exception If no entropy source is available. + */ + public function generateSingleCode() : string + { + $bytes = random_bytes(32); + return substr(md5($bytes), 0, $this->code_length); + } + + + /** + * Returns a full backup code set. The code count can be configured in the constructor + * @return string[] An array containing different backup codes. + */ + public function generateCodeSet() : array + { + $array = []; + for($n=0; $n<$this->code_count; $n++) { + $array[] = $this->generateSingleCode(); + } + + return $array; + } +} \ No newline at end of file diff --git a/src/Services/TFA/BackupCodeManager.php b/src/Services/TFA/BackupCodeManager.php new file mode 100644 index 00000000..7bdc0e9e --- /dev/null +++ b/src/Services/TFA/BackupCodeManager.php @@ -0,0 +1,75 @@ +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->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/src/Twig/AppExtension.php b/src/Twig/AppExtension.php index 3249784d..02266dd5 100644 --- a/src/Twig/AppExtension.php +++ b/src/Twig/AppExtension.php @@ -33,6 +33,7 @@ use App\Services\MoneyFormatter; use App\Services\SIFormatter; use App\Services\TreeBuilder; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFilter; use Twig\TwigFunction; @@ -49,13 +50,14 @@ class AppExtension extends AbstractExtension protected $amountFormatter; protected $attachmentURLGenerator; protected $FAIconGenerator; + protected $translator; public function __construct(EntityURLGenerator $entityURLGenerator, MarkdownParser $markdownParser, - SerializerInterface $serializer, TreeBuilder $treeBuilder, - MoneyFormatter $moneyFormatter, - SIFormatter $SIFormatter, AmountFormatter $amountFormatter, - AttachmentURLGenerator $attachmentURLGenerator, - FAIconGenerator $FAIconGenerator) + SerializerInterface $serializer, TreeBuilder $treeBuilder, + MoneyFormatter $moneyFormatter, + SIFormatter $SIFormatter, AmountFormatter $amountFormatter, + AttachmentURLGenerator $attachmentURLGenerator, + FAIconGenerator $FAIconGenerator, TranslatorInterface $translator) { $this->entityURLGenerator = $entityURLGenerator; $this->markdownParser = $markdownParser; @@ -66,6 +68,7 @@ class AppExtension extends AbstractExtension $this->amountFormatter = $amountFormatter; $this->attachmentURLGenerator = $attachmentURLGenerator; $this->FAIconGenerator = $FAIconGenerator; + $this->translator = $translator; } public function getFilters() diff --git a/src/Validator/Constraints/ValidGoogleAuthCode.php b/src/Validator/Constraints/ValidGoogleAuthCode.php new file mode 100644 index 00000000..457d378c --- /dev/null +++ b/src/Validator/Constraints/ValidGoogleAuthCode.php @@ -0,0 +1,30 @@ +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/symfony.lock b/symfony.lock index 310d4f98..6562b509 100644 --- a/symfony.lock +++ b/symfony.lock @@ -5,6 +5,9 @@ "amphp/byte-stream": { "version": "v1.6.1" }, + "beberlei/assert": { + "version": "v3.2.6" + }, "composer/xdebug-handler": { "version": "1.3.3" }, @@ -165,6 +168,9 @@ "jdorn/sql-formatter": { "version": "v1.2.17" }, + "lcobucci/jwt": { + "version": "3.3.1" + }, "league/html-to-markdown": { "version": "4.8.2" }, @@ -229,6 +235,9 @@ "openlss/lib-array2xml": { "version": "1.0.0" }, + "paragonie/constant_time_encoding": { + "version": "v2.3.0" + }, "php": { "version": "7.1.3" }, @@ -301,6 +310,9 @@ "psr/simple-cache": { "version": "1.0.1" }, + "r/u2f-two-factor-bundle": { + "version": "0.7.0" + }, "roave/security-advisories": { "version": "dev-master" }, @@ -310,6 +322,19 @@ "s9e/text-formatter": { "version": "2.1.2" }, + "scheb/two-factor-bundle": { + "version": "3.16", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "master", + "version": "3.16", + "ref": "b5789cd9710e2ee555bf361079b991068a0f640b" + }, + "files": [ + "./config/packages/scheb_two_factor.yaml", + "./config/routes/scheb_two_factor.yaml" + ] + }, "sebastian/diff": { "version": "3.0.2" }, @@ -340,6 +365,9 @@ "shivas/versioning-bundle": { "version": "3.1.3" }, + "spomky-labs/otphp": { + "version": "v9.1.4" + }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -751,6 +779,9 @@ "webmozart/path-util": { "version": "2.3.0" }, + "yubico/u2flib-server": { + "version": "1.0.2" + }, "zendframework/zend-code": { "version": "3.3.1" }, diff --git a/templates/AdminPages/GroupAdmin.html.twig b/templates/AdminPages/GroupAdmin.html.twig index 7b06632a..305ae156 100644 --- a/templates/AdminPages/GroupAdmin.html.twig +++ b/templates/AdminPages/GroupAdmin.html.twig @@ -12,6 +12,10 @@ {% block additional_panes %}
- {{ form_row(form.permissions) }} + {{ form_row(form.permissions) }}
+{% endblock %} + +{% block additional_controls %} + {{ form_row(form.enforce2FA) }} {% endblock %} \ No newline at end of file diff --git a/templates/AdminPages/UserAdmin.html.twig b/templates/AdminPages/UserAdmin.html.twig index 41a89232..cdcc584d 100644 --- a/templates/AdminPages/UserAdmin.html.twig +++ b/templates/AdminPages/UserAdmin.html.twig @@ -1,5 +1,9 @@ {% extends "AdminPages/EntityAdminBase.html.twig" %} +{% import "helper.twig" as helper %} + +{# @var entity \App\Entity\UserSystem\User #} + {% block card_title %} {% trans %}user.edit.caption{% endtrans %} {% endblock %} @@ -34,6 +38,42 @@ {{ form_row(form.new_password) }} {{ form_row(form.need_pw_change) }} {{ form_row(form.disabled) }} + + {% if entity.id is not null %} +
+
+
{% trans %}user.edit.tfa.caption{% endtrans %}
+ +

{% trans %}user.edit.tfa.google_active{% endtrans %}: {{ helper.boolean(entity.googleAuthenticatorEnabled) }}

+

{% trans %}tfa_backup.remaining_tokens{% endtrans %}: {{ entity.backupCodes | length }}

+

{% trans %}tfa_backup.generation_date{% endtrans %}: + {% if entity.backupCodesGenerationDate is not null %} + {{ entity.backupCodesGenerationDate | format_datetime }} + {% else %} + {% trans %}user.edit.tfa.disabled{% endtrans %} + {% endif %} +

+

{% trans %}user.edit.tfa.u2f_keys_count{% endtrans %}: + {% if entity.u2FAuthEnabled %} + {{ entity.u2FKeys | length }} + {% else %} + {% trans %}user.edit.tfa.disabled{% endtrans %} + {% endif %} +

+ + {% set tfa_disable_disabled = not is_granted('set_password', entity) %} + {# Disable button when he has no 2FA activated #} + {% if not entity.u2FAuthEnabled and not entity.googleAuthenticatorEnabled and entity.backupCodes is empty %} + {% set tfa_disable_disabled = true %} + {% endif %} + + + +
+ {% endif %} +
diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig new file mode 100644 index 00000000..56ac7255 --- /dev/null +++ b/templates/Users/_2fa_settings.html.twig @@ -0,0 +1,162 @@ +{# @var user \App\Entity\UserSystem\User #} + +
+
+ + {% trans %}user.settings.2fa_settings{% endtrans %} +
+
+ + +
+
+ {% set google_form_attr = {} %} + {% if tfa_google.enabled %} + {% set google_form_attr = { 'data-delete-form': true, + 'data-title': 'tfa_google.disable.confirm_title' | trans, 'data-message': 'tfa_google.disable.confirm_message'|trans} %} + {% endif %} + + {{ form_start(google_form, { 'attr': google_form_attr}) }} + {% if not tfa_google.enabled %} +
+
{% trans %}tfa_google.disabled_message{% endtrans %}
+
+ +
+
+ +
+
+
    +
  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. +
  7. {% trans %}tfa_google.step.download_backup{% endtrans %}
  8. +
+
+
+ +
+ +
+
+

{% trans %}tfa_google.manual_setup.type{% endtrans %}: TOTP

+

{% trans %}tfa_google.manual_setup.username{% endtrans %}: {{ tfa_google.username }}

+

{% trans %}tfa_google.manual_setup.secret{% endtrans %}: {{ tfa_google.secret }}

+

{% trans %}tfa_google.manual_setup.digit_count{% endtrans %}: 6

+ +
+
+
+ + {{ form_row(google_form.google_confirmation) }} + {% else %} +
+
{% trans %}tfa_google.enabled_message{% endtrans %}
+
+ {% endif %} + {{ form_row(google_form.submit) }} + {{ form_end(google_form) }} +
+
+ {% if user.backupCodes is empty %} +
+
{% trans %}tfa_backup.disabled{% endtrans %}
+ {% trans %}tfa_backup.explanation{% endtrans %} +
+ {% else %} + {% set backup_form_attr = { 'data-delete-form': true, + 'data-title': 'tfa_backup.reset_codes.confirm_title' | trans, 'data-message': 'tfa_backup.reset_codes.confirm_message' | trans} %} + {{ form_start(backup_form, { 'attr': backup_form_attr}) }} +
+
{% trans %}tfa_backup.enabled{% endtrans %}
+ {% trans %}tfa_backup.explanation{% endtrans %} +
+
+

{% trans %}tfa_backup.remaining_tokens{% endtrans %}: {{ user.backupCodes | length }}

+

{% trans %}tfa_backup.generation_date{% endtrans %}: {{ user.backupCodesGenerationDate | format_datetime }}

+
+ + +
+ {{ form_widget(backup_form.reset_codes) }} +
+ {{ form_end(backup_form) }} + + {% endif %} +
+ +
+

{% trans %}tfa_u2f.explanation{% endtrans %}

+ + {% if user.u2FKeys is not empty %} + {% trans %}tfa_u2f.table_caption{% endtrans %}: +
+ + + + + + + + + + + + + {% for key in user.u2FKeys %} + + + + + + + {% endfor %} + +
#{% trans %}tfa_u2f.keys.name{% endtrans %}{% trans %}tfa_u2f.keys.added_date{% endtrans %}
{{ loop.index }}{{ key.name }}{{ key.addedDate | format_datetime }}
+
+ {% else %} +

{% trans %}tfa_u2f.no_keys_registered{% endtrans %}

+ {% endif %} + + {% trans %}tfa_u2f.add_new_key{% endtrans %} +
+ +
+

{% trans %}tfa_trustedDevices.explanation{% endtrans %}

+
+ + + + +
+
+ +
+ + +
+
\ No newline at end of file diff --git a/templates/Users/backup_codes.html.twig b/templates/Users/backup_codes.html.twig new file mode 100644 index 00000000..554beb78 --- /dev/null +++ b/templates/Users/backup_codes.html.twig @@ -0,0 +1,40 @@ +{% extends "base.html.twig" %} + +{% block title %}{{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %}{% endblock %} + +{% block body %} +
+
+
+ {{ partdb_title }} {% trans %}tfa_backup.codes.title{% endtrans %} +
+
+
{% trans %}tfa_backup.codes.explanation{% endtrans %}
+

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

+ +
+
+
    + {% for code in user.backupCodes %} +

  • {{ code }}
  • + {% endfor %} +
+
+
+ +

{% trans with {'%date%': "now" | format_datetime} %}tfa_backup.codes.page_generated_on{% endtrans %}

+ + + {% trans %}tfa_backup.codes.print{% endtrans %} + + +
+
+
+{% endblock %} \ 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/templates/base.html.twig b/templates/base.html.twig index 3099180a..b1bdc6cf 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -123,6 +123,10 @@ {% endfor %} + {% for js in encore_entry_js_files('ru2ftwofactor') %} + + {% endfor %} + {% endblock %} {% block scripts %} diff --git a/templates/helper.twig b/templates/helper.twig index 10775b2b..616bb6e7 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -80,4 +80,12 @@ {% endfor %} +{% endmacro %} + +{% macro bool_icon(bool) %} + {% if bool %} + + {% else %} + + {% endif %} {% endmacro %} \ No newline at end of file diff --git a/templates/security/2fa_base_form.html.twig b/templates/security/2fa_base_form.html.twig new file mode 100644 index 00000000..0d2c73f1 --- /dev/null +++ b/templates/security/2fa_base_form.html.twig @@ -0,0 +1,58 @@ +{% extends "main_card.html.twig" %} + +{% block title %}{% trans %}tfa.check.title{% endtrans %}{% endblock %} + +{% block card_title %} {% trans %}tfa.check.title{% endtrans %}{% endblock %} + +{% block content %} + {% if authenticationError %} + + {% endif %} + + {{ parent() }} +{% endblock %} + +{% block card_content %} + + + {# Display current two-factor provider #} + +
+ {% block form %} + + {% endblock %} + + {% if displayTrustedOption %} +
+
+
+ + +
+
+
+ {% endif %} + + {% if isCsrfProtectionEnabled %} + + {% endif %} + + {% block submit_btn %} +
+
+ + {% trans %}user.logout{% endtrans %} +
+
+ {% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/security/2fa_form.html.twig b/templates/security/2fa_form.html.twig new file mode 100644 index 00000000..c38018e6 --- /dev/null +++ b/templates/security/2fa_form.html.twig @@ -0,0 +1,14 @@ +{% extends "security/2fa_base_form.html.twig" %} + + +{% block form %} +
+ +
+ + + {% trans %}tfa.check.code.help{% endtrans %} + +
+
+{% endblock %} \ No newline at end of file diff --git a/templates/security/U2F/u2f_login.html.twig b/templates/security/U2F/u2f_login.html.twig new file mode 100644 index 00000000..3235fe1b --- /dev/null +++ b/templates/security/U2F/u2f_login.html.twig @@ -0,0 +1,17 @@ +{% extends "security/2fa_base_form.html.twig" %} + +{% block form_attributes %}id="u2fForm" data-action="auth" data-request='{{ authenticationData|raw }}'{% endblock %} + +{% block form %} + {% if not app.request.secure %} +

{% trans %}tfa_u2f.http_warning{% endtrans %}

+ {% endif %} + + +
+

+ {% trans %}user.logout{% endtrans %} +{% endblock %} + +{% block submit_btn %} +{% endblock %} \ No newline at end of file diff --git a/templates/security/U2F/u2f_register.html.twig b/templates/security/U2F/u2f_register.html.twig new file mode 100644 index 00000000..324eb106 --- /dev/null +++ b/templates/security/U2F/u2f_register.html.twig @@ -0,0 +1,28 @@ +{% extends "main_card.html.twig" %} + +{% block card_title %} {% trans %}tfa_u2f.add_key.title{% endtrans %}{% endblock %} + +{% block card_content %} +

{% trans %}tfa_u2f.explanation{% endtrans %}

+

{% trans %}tfa_u2f.add_key.backup_hint{% endtrans %}

+ + {% if not app.request.secure %} +

{% trans %}tfa_u2f.http_warning{% endtrans %}

+ {% endif %} + +
+
+
+ +
+
+ +
+
+ + + +
+ + {% trans %}tfa_u2f.add_key.back_to_settings{% endtrans %} +{% endblock %} diff --git a/tests/Controller/AdminPages/AbstractAdminControllerTest.php b/tests/Controller/AdminPages/AbstractAdminControllerTest.php index 4a191b59..a9da3b73 100644 --- a/tests/Controller/AdminPages/AbstractAdminControllerTest.php +++ b/tests/Controller/AdminPages/AbstractAdminControllerTest.php @@ -26,6 +26,7 @@ use Symfony\Component\Security\Core\Exception\AccessDeniedException; /** * @group slow + * @group DB */ abstract class AbstractAdminControllerTest extends WebTestCase { diff --git a/tests/Controller/AdminPages/AttachmentTypeControllerTest.php b/tests/Controller/AdminPages/AttachmentTypeControllerTest.php index d7cfd48c..952211a6 100644 --- a/tests/Controller/AdminPages/AttachmentTypeControllerTest.php +++ b/tests/Controller/AdminPages/AttachmentTypeControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Attachments\AttachmentType; /** * @group slow + * @group DB */ class AttachmentTypeControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/CategoryControllerTest.php b/tests/Controller/AdminPages/CategoryControllerTest.php index 48a5cff0..12460519 100644 --- a/tests/Controller/AdminPages/CategoryControllerTest.php +++ b/tests/Controller/AdminPages/CategoryControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\Category; /** * @group slow + * @group DB */ class CategoryControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/DeviceControllerTest.php b/tests/Controller/AdminPages/DeviceControllerTest.php index a608ace9..d7fa1ab0 100644 --- a/tests/Controller/AdminPages/DeviceControllerTest.php +++ b/tests/Controller/AdminPages/DeviceControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Devices\Device; /** * @group slow + * @group DB */ class DeviceControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/FootprintControllerTest.php b/tests/Controller/AdminPages/FootprintControllerTest.php index 14e8ecb1..10c3bbc2 100644 --- a/tests/Controller/AdminPages/FootprintControllerTest.php +++ b/tests/Controller/AdminPages/FootprintControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\Footprint; /** * @group slow + * @group DB */ class FootprintControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/ManufacturerControllerTest.php b/tests/Controller/AdminPages/ManufacturerControllerTest.php index 134ef8a5..7c9151a3 100644 --- a/tests/Controller/AdminPages/ManufacturerControllerTest.php +++ b/tests/Controller/AdminPages/ManufacturerControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\Manufacturer; /** * @group slow + * @group DB */ class ManufacturerControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/MeasurementUnitControllerTest.php b/tests/Controller/AdminPages/MeasurementUnitControllerTest.php index ac576409..090ca2f3 100644 --- a/tests/Controller/AdminPages/MeasurementUnitControllerTest.php +++ b/tests/Controller/AdminPages/MeasurementUnitControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\MeasurementUnit; /** * @group slow + * @group DB */ class MeasurementUnitControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/StorelocationControllerTest.php b/tests/Controller/AdminPages/StorelocationControllerTest.php index b92f5bf9..e1bd0e9f 100644 --- a/tests/Controller/AdminPages/StorelocationControllerTest.php +++ b/tests/Controller/AdminPages/StorelocationControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\Storelocation; /** * @group slow + * @group DB */ class StorelocationControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/AdminPages/SupplierControllerTest.php b/tests/Controller/AdminPages/SupplierControllerTest.php index a085bfae..31559c42 100644 --- a/tests/Controller/AdminPages/SupplierControllerTest.php +++ b/tests/Controller/AdminPages/SupplierControllerTest.php @@ -25,6 +25,7 @@ use App\Entity\Parts\Supplier; /** * @group slow + * @group DB */ class SupplierControllerTest extends AbstractAdminControllerTest { diff --git a/tests/Controller/RedirectControllerTest.php b/tests/Controller/RedirectControllerTest.php index 58bbb2c9..098ee1fb 100644 --- a/tests/Controller/RedirectControllerTest.php +++ b/tests/Controller/RedirectControllerTest.php @@ -27,6 +27,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** * @group slow + * @group DB */ class RedirectControllerTest extends WebTestCase { diff --git a/tests/Entity/UserSystem/UserTest.php b/tests/Entity/UserSystem/UserTest.php index 0ed8fdf2..6290aad2 100644 --- a/tests/Entity/UserSystem/UserTest.php +++ b/tests/Entity/UserSystem/UserTest.php @@ -21,6 +21,7 @@ namespace App\Tests\Entity\UserSystem; +use App\Entity\UserSystem\U2FKey; use App\Entity\UserSystem\User; use PHPUnit\Framework\TestCase; @@ -36,4 +37,91 @@ class UserTest extends TestCase $this->assertEquals('John Doe', $user->getFullName(false)); $this->assertEquals('John Doe (username)', $user->getFullName(true)); } + + public function googleAuthenticatorEnabledDataProvider() : array + { + return [ + [null, false], + ['', false], + ['SSSk38498', true] + ]; + } + + /** + * @dataProvider googleAuthenticatorEnabledDataProvider + */ + public function testIsGoogleAuthenticatorEnabled(?string $secret, bool $expected) + { + $user = new User(); + $user->setGoogleAuthenticatorSecret($secret); + $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->assertEqualsWithDelta(new \DateTime(), $user->getBackupCodesGenerationDate(), 0.1); + $this->assertEquals($codes, $user->getBackupCodes()); + + //Test what happens if we delete the backup keys + $user->setBackupCodes([]); + $this->assertEmpty($user->getBackupCodes()); + $this->assertNull($user->getBackupCodesGenerationDate()); + } + + public function testIsBackupCode() + { + $user = new User(); + $codes = ['aaaa', 'bbbb', 'cccc', 'dddd']; + $user->setBackupCodes($codes); + + $this->assertTrue($user->isBackupCode('aaaa')); + $this->assertTrue($user->isBackupCode('cccc')); + + $this->assertFalse($user->isBackupCode('')); + $this->assertFalse($user->isBackupCode('zzzz')); + } + + public function testInvalidateBackupCode() + { + $user = new User(); + $codes = ['aaaa', 'bbbb', 'cccc', 'dddd']; + $user->setBackupCodes($codes); + + //Ensure the code is valid + $this->assertTrue($user->isBackupCode('aaaa')); + $this->assertTrue($user->isBackupCode('bbbb')); + //Invalidate code, afterwards the code has to be invalid! + $user->invalidateBackupCode('bbbb'); + $this->assertFalse($user->isBackupCode('bbbb')); + $this->assertTrue($user->isBackupCode('aaaa')); + + //No exception must happen, when we try to invalidate an not existing backup key! + $user->invalidateBackupCode('zzzz'); + } + + public function testInvalidateTrustedDeviceTokens() + { + $user = new User(); + $old_value = $user->getTrustedTokenVersion(); + //To invalidate the token, the new value must be bigger than the old value + $user->invalidateTrustedDeviceTokens(); + $this->assertGreaterThan($old_value, $user->getTrustedTokenVersion()); + } + + public function testIsU2fEnabled() + { + $user = new User(); + $user->addU2FKey(new U2FKey()); + $this->assertTrue($user->isU2FAuthEnabled()); + + $user->getU2FKeys()->clear(); + $this->assertFalse($user->isU2FAuthEnabled()); + } } diff --git a/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php b/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php new file mode 100644 index 00000000..49a63946 --- /dev/null +++ b/tests/EventSubscriber/PasswordChangeNeededSubscriberTest.php @@ -0,0 +1,60 @@ +setGroup(null); + $this->assertFalse(PasswordChangeNeededSubscriber::TFARedirectNeeded($user)); + + //When the group does not enforce the redirect the user must not be redirected + $user->setGroup($group); + $this->assertFalse(PasswordChangeNeededSubscriber::TFARedirectNeeded($user)); + + //The user must be redirected if the group enforces 2FA and it does not have a method + $group->setEnforce2FA(true); + $this->assertTrue(PasswordChangeNeededSubscriber::TFARedirectNeeded($user)); + + //User must not be redirect if google authenticator is setup + $user->setGoogleAuthenticatorSecret('abcd'); + $this->assertFalse(PasswordChangeNeededSubscriber::TFARedirectNeeded($user)); + + //User must not be redirect if 2FA is setup + $user->setGoogleAuthenticatorSecret(null); + $user->addU2FKey(new U2FKey()); + $this->assertFalse(PasswordChangeNeededSubscriber::TFARedirectNeeded($user)); + + } +} diff --git a/tests/Services/EntityImporterTest.php b/tests/Services/EntityImporterTest.php index a16e5475..4bdf4063 100644 --- a/tests/Services/EntityImporterTest.php +++ b/tests/Services/EntityImporterTest.php @@ -28,6 +28,9 @@ use App\Services\ElementTypeNameGenerator; use App\Services\EntityImporter; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +/** + * @group DB + */ class EntityImporterTest extends WebTestCase { /** diff --git a/tests/Services/TFA/BackupCodeGeneratorTest.php b/tests/Services/TFA/BackupCodeGeneratorTest.php new file mode 100644 index 00000000..cfaf2176 --- /dev/null +++ b/tests/Services/TFA/BackupCodeGeneratorTest.php @@ -0,0 +1,75 @@ +expectException(\RuntimeException::class); + new BackupCodeGenerator(33, 10); + } + + /** + * Test if an exception is thrown if you are using a too high code length + */ + public function testLengthLowerLimit() + { + $this->expectException(\RuntimeException::class); + new BackupCodeGenerator(4, 10); + } + + + public function codeLengthDataProvider() + { + return [[6], [8], [10], [16]]; + } + + /** + * @dataProvider codeLengthDataProvider + */ + public function testGenerateSingleCode(int $code_length) + { + $generator = new BackupCodeGenerator($code_length, 10); + $this->assertRegExp("/^([a-f0-9]){{$code_length}}\$/", $generator->generateSingleCode()); + } + + public function codeCountDataProvider() + { + return [[2], [8], [10]]; + } + + /** + * @dataProvider codeCountDataProvider + */ + public function testGenerateCodeSet(int $code_count) + { + $generator = new BackupCodeGenerator(8, $code_count); + $this->assertCount($code_count, $generator->generateCodeSet()); + } +} diff --git a/tests/Services/TFA/BackupCodeManagerTest.php b/tests/Services/TFA/BackupCodeManagerTest.php new file mode 100644 index 00000000..fefe6488 --- /dev/null +++ b/tests/Services/TFA/BackupCodeManagerTest.php @@ -0,0 +1,83 @@ +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()); + } +} diff --git a/translations/SchebTwoFactorBundle+intl-icu.de.xlf b/translations/SchebTwoFactorBundle+intl-icu.de.xlf new file mode 100644 index 00000000..e9fb4cc6 --- /dev/null +++ b/translations/SchebTwoFactorBundle+intl-icu.de.xlf @@ -0,0 +1,11 @@ + + + + + + login + Login + + + + diff --git a/translations/SchebTwoFactorBundle+intl-icu.en.xlf b/translations/SchebTwoFactorBundle+intl-icu.en.xlf new file mode 100644 index 00000000..1b17584c --- /dev/null +++ b/translations/SchebTwoFactorBundle+intl-icu.en.xlf @@ -0,0 +1,11 @@ + + + + + + login + Login + + + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 7359732f..7835a8a7 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -265,7 +265,7 @@ templates\AdminPages\GroupAdmin.html.twig:9 - templates\AdminPages\UserAdmin.html.twig:12 + templates\AdminPages\UserAdmin.html.twig:16 user.edit.permissions @@ -318,7 +318,7 @@ - templates\AdminPages\UserAdmin.html.twig:4 + templates\AdminPages\UserAdmin.html.twig:8 user.edit.caption @@ -327,7 +327,7 @@ - templates\AdminPages\UserAdmin.html.twig:10 + templates\AdminPages\UserAdmin.html.twig:14 user.edit.configuration @@ -336,13 +336,101 @@ - templates\AdminPages\UserAdmin.html.twig:11 + templates\AdminPages\UserAdmin.html.twig:15 user.edit.password Passwort + + + templates\AdminPages\UserAdmin.html.twig:45 + + + user.edit.tfa.caption + Zwei-Faktor-Authentifizierung + + + + + templates\AdminPages\UserAdmin.html.twig:47 + + + user.edit.tfa.google_active + Authentifizierungsapp aktiv + + + + + templates\AdminPages\UserAdmin.html.twig:48 + templates\Users\backup_codes.html.twig:15 + templates\Users\_2fa_settings.html.twig:95 + + + tfa_backup.remaining_tokens + Verbleibende Backupcodes + + + + + templates\AdminPages\UserAdmin.html.twig:49 + templates\Users\backup_codes.html.twig:17 + templates\Users\_2fa_settings.html.twig:96 + + + tfa_backup.generation_date + Erzeugungsdatum der Backupcodes + + + + + templates\AdminPages\UserAdmin.html.twig:53 + templates\AdminPages\UserAdmin.html.twig:60 + + + user.edit.tfa.disabled + Methode deaktiviert + + + + + templates\AdminPages\UserAdmin.html.twig:56 + + + user.edit.tfa.u2f_keys_count + Aktive Sicherheitsschlüssel + + + + + templates\AdminPages\UserAdmin.html.twig:72 + + + user.edit.tfa.disable_tfa_title + Wirklich fortfahren? + + + + + templates\AdminPages\UserAdmin.html.twig:72 + + + user.edit.tfa.disable_tfa_message + alle aktiven Zwei-Faktor-Authentifizierungsmethoden des Nutzers deaktivieren und die Backupcodes löschen!
+Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müssen und neue Backupcodes ausdrucken müssen!

+Führen sie dies nur durch, wenn Sie über die Identität des (um Hilfe suchenden) Benutzers absolut sicher sind, da ansonsten eine Kompromittierung des Accounts durch einen Angreifer erfolgen könnte!]]>
+
+
+ + + templates\AdminPages\UserAdmin.html.twig:73 + + + user.edit.tfa.disable_tfa.btn + Alle Zwei-Faktor-Authentifizierungsmethoden deaktivieren + + templates\AdminPages\_attachments.html.twig:4 @@ -761,6 +849,24 @@ Subelemente werden beim Löschen nach oben verschoben. Nein + + + templates\helper.twig:87 + + + Yes + Ja + + + + + templates\helper.twig:89 + + + No + Nein + + templates\homepage.html.twig:7 @@ -1790,6 +1896,63 @@ Subelemente werden beim Löschen nach oben verschoben. Anzahl an Unterelementen + + + templates\security\2fa_base_form.html.twig:3 + templates\security\2fa_base_form.html.twig:5 + + + tfa.check.title + Zwei-Faktor-Authentifizierung benötigt + + + + + templates\security\2fa_base_form.html.twig:39 + + + tfa.code.trusted_pc + Dies ist ein vertrauenswürdiger Computer (wenn dies aktiviert ist, werden auf diesem Computer keine weiteren Zwei-Faktor-Abfragen durchgeführt) + + + + + templates\security\2fa_base_form.html.twig:52 + templates\security\login.html.twig:58 + + + login.btn + Login + + + + + templates\security\2fa_base_form.html.twig:53 + templates\_navbar.html.twig:103 + + + user.logout + Ausloggen + + + + + templates\security\2fa_form.html.twig:6 + + + tfa.check.code.label + Authenticator App Code + + + + + templates\security\2fa_form.html.twig:10 + + + tfa.check.code.help + Geben Sie hier den 6-stelligen Code aus ihrer Authenticator App ein oder einen ihrer Backupcodes, wenn der Authenticator nicht verfügbar ist. + + templates\security\login.html.twig:3 @@ -1860,16 +2023,6 @@ Subelemente werden beim Löschen nach oben verschoben. Eingeloggt bleiben (nicht empfohlen auf geteilten Computern) - - - templates\security\login.html.twig:58 - templates\security\login.html.twig:58 - - - login.btn - Login - - templates\security\login.html.twig:64 @@ -1897,6 +2050,145 @@ Subelemente werden beim Löschen nach oben verschoben. Neues Passwort anfordern + + + templates\security\U2F\u2f_login.html.twig:7 + templates\security\U2F\u2f_register.html.twig:10 + + + tfa_u2f.http_warning + Sie greifen auf diese Seite über das unsichere HTTP-Verfahren zu, daher wird U2F sehr wahrscheinlich nicht funktionieren (Fehlermeldung Bad Request). Bitten Sie einen Adminstrator das sichere HTTPS Verfahren einzurichten, wenn Sie Sicherheitsschlüssel benutzen möchten. + + + + + templates\security\U2F\u2f_login.html.twig:10 + templates\security\U2F\u2f_register.html.twig:22 + + + r_u2f_two_factor.pressbutton + Bitte Sicherheitsschlüssel einstecken und Button drücken! + + + + + templates\security\U2F\u2f_register.html.twig:3 + + + tfa_u2f.add_key.title + Sicherheitsschlüssel hinzufügen + + + + + templates\security\U2F\u2f_register.html.twig:6 + templates\Users\_2fa_settings.html.twig:111 + + + tfa_u2f.explanation + Mithilfe eines U2F/FIDO kompatiblem Sicherheitsschlüssel (z.B. YubiKey oder NitroKey) kann eine benutzerfreundliche und sichere Zwei-Faktor-Authentifizierung ermöglicht. Die Sicherheitsschlüssel können hier registriert werden, und wird eine Zwei-Faktor-Überprüfung benötigt, so muss der Schlüssel nur per USB angesteckt oder per NFC gegen das Gerät getippt werden. + + + + + templates\security\U2F\u2f_register.html.twig:7 + + + tfa_u2f.add_key.backup_hint + Um den Zugang auch bei Verlust des Schlüssels zu gewährleisten, ist es empfehlenswert einen zweiten Schlüssel als Backup zu registrieren und diesen an einem sicherem Ort zu lagern! + + + + + templates\security\U2F\u2f_register.html.twig:16 + + + r_u2f_two_factor.name + Anzeigename des Schlüssels (z.B. Backup) + + + + + templates\security\U2F\u2f_register.html.twig:19 + + + tfa_u2f.add_key.add_button + Schlüssel hinzufügen + + + + + templates\security\U2F\u2f_register.html.twig:27 + + + tfa_u2f.add_key.back_to_settings + Zurück zu den Einstellungen + + + + + templates\Users\backup_codes.html.twig:3 + templates\Users\backup_codes.html.twig:9 + + + tfa_backup.codes.title + Backupcodes + + + + + templates\Users\backup_codes.html.twig:12 + + + tfa_backup.codes.explanation + Drucken Sie diese Codes aus und bewahren Sie sie an einem sicherem Ort auf! + + + + + templates\Users\backup_codes.html.twig:13 + + + tfa_backup.codes.help + Wenn Sie keinen Zugriff auf ihr Gerät mit der Authenticator App mehr haben sollten (Smartphone verloren, Datenverlust, etc.) können Sie einen dieser Codes benutzen, um Zugriff auf ihren Account zu erhalten und evtl. eine neue Authenticator App einzurichten. Jeder dieser Codes lässt sich einmal einsetzen, es empfiehlt sich benutzte Codes zu streichen. Jeder mit Zugriff auf diese Codes kann potentiell auf ihren Account zugreifen, daher bewahren Sie sie an einem sicheren Ort auf. + + + + + templates\Users\backup_codes.html.twig:16 + + + tfa_backup.username + Benutzername + + + + + templates\Users\backup_codes.html.twig:29 + + + tfa_backup.codes.page_generated_on + Codes abgerufen am %date% + + + + + templates\Users\backup_codes.html.twig:32 + + + tfa_backup.codes.print + Drucken + + + + + templates\Users\backup_codes.html.twig:35 + + + tfa_backup.codes.copy_clipboard + In die Zwischenablage kopieren + + templates\Users\user_info.html.twig:3 @@ -2027,7 +2319,7 @@ Subelemente werden beim Löschen nach oben verschoben. - templates\Users\user_settings.html.twig:53 + templates\Users\user_settings.html.twig:55 templates\Users\user_settings.html.twig:48 @@ -2035,6 +2327,333 @@ Subelemente werden beim Löschen nach oben verschoben. Passwort ändern + + + templates\Users\_2fa_settings.html.twig:6 + + + user.settings.2fa_settings + Zwei-Faktor-Authentifizierung + + + + + templates\Users\_2fa_settings.html.twig:13 + + + tfa.settings.google.tab + Authenticator App + + + + + templates\Users\_2fa_settings.html.twig:17 + + + tfa.settings.bakup.tab + Backupcodes + + + + + templates\Users\_2fa_settings.html.twig:21 + + + tfa.settings.u2f.tab + Sicherheitsschlüssel (U2F) + + + + + templates\Users\_2fa_settings.html.twig:25 + + + tfa.settings.trustedDevices.tab + Vertrauenswürdige Geräte + + + + + templates\Users\_2fa_settings.html.twig:33 + + + tfa_google.disable.confirm_title + Möchten Sie die Authenticator App wirklich deaktivieren? + + + + + templates\Users\_2fa_settings.html.twig:33 + + + tfa_google.disable.confirm_message + +Beachten Sie außerdem, dass ihr Account ohne Zwei-Faktor-Authentifizierung nicht mehr so gut gegen Angreifer geschützt ist!]]> + + + + + templates\Users\_2fa_settings.html.twig:39 + + + tfa_google.disabled_message + Authenticator App deaktiviert + + + + + templates\Users\_2fa_settings.html.twig:48 + + + tfa_google.step.download + Google Authenticator oder FreeOTP Authenticator)]]> + + + + + templates\Users\_2fa_settings.html.twig:49 + + + tfa_google.step.scan + Scannen Sie den nebenstehenden QR-Code mit der App oder geben Sie die Daten manuell ein + + + + + templates\Users\_2fa_settings.html.twig:50 + + + tfa_google.step.input_code + Geben Sie den erzeugten Code in das untere Feld ein und bestätigen Sie + + + + + templates\Users\_2fa_settings.html.twig:51 + + + tfa_google.step.download_backup + Drucken Sie ihre Backupcodes aus und lagern sie an einem sicherem Ort + + + + + templates\Users\_2fa_settings.html.twig:58 + + + tfa_google.manual_setup + Manuelle Einrichtung + + + + + templates\Users\_2fa_settings.html.twig:62 + + + tfa_google.manual_setup.type + Typ + + + + + templates\Users\_2fa_settings.html.twig:63 + + + tfa_google.manual_setup.username + Benutzername + + + + + templates\Users\_2fa_settings.html.twig:64 + + + tfa_google.manual_setup.secret + Secret + + + + + templates\Users\_2fa_settings.html.twig:65 + + + tfa_google.manual_setup.digit_count + Anzahl Stellen + + + + + templates\Users\_2fa_settings.html.twig:74 + + + tfa_google.enabled_message + Authenticator App aktiv + + + + + templates\Users\_2fa_settings.html.twig:83 + + + tfa_backup.disabled + Backupcodes deaktiviert. Authenticator App einrichten, um Backupcodes zu aktivieren. + + + + + templates\Users\_2fa_settings.html.twig:84 + templates\Users\_2fa_settings.html.twig:92 + + + tfa_backup.explanation + Mithilfe dieser Backupcodes können Sie auf ihren Account zugreifen, selbst wenn Sie das Gerät mit der Authenticator App verlieren sollten. Drucken Sie die Codes aus und bewahren Sie sie an einem sicherem Ort auf. + + + + + templates\Users\_2fa_settings.html.twig:88 + + + tfa_backup.reset_codes.confirm_title + Codes wirklich zurücksetzen? + + + + + templates\Users\_2fa_settings.html.twig:88 + + + tfa_backup.reset_codes.confirm_message + Dies wird alle bisherigen Codes löschen und einen Satz neuer Codes generieren. Dies lässt sich nicht rückgängig machen. Denken Sie daran die neuen Codes auszudrucken und an einem sicheren Ort zu hinterlegen! + + + + + templates\Users\_2fa_settings.html.twig:91 + + + tfa_backup.enabled + Backupcodes aktiviert + + + + + templates\Users\_2fa_settings.html.twig:99 + + + tfa_backup.show_codes + Backupcodes anzeigen + + + + + templates\Users\_2fa_settings.html.twig:114 + + + tfa_u2f.table_caption + Registrierte Sicherheitsschlüssel + + + + + templates\Users\_2fa_settings.html.twig:115 + + + tfa_u2f.delete_u2f.confirm_title + Diesen Sicherheitsschlüssel wirklich entfernen? + + + + + templates\Users\_2fa_settings.html.twig:116 + + + tfa_u2f.delete_u2f.confirm_message + Wenn Sie diesen Schlüssel entfernen, dann wird kein Login mehr mit diesem möglich sein. Wenn keine Sicherheitsschlüssel verleiben, wird die Zwei-Faktor-Authentifizierung deaktiviert. + + + + + templates\Users\_2fa_settings.html.twig:123 + + + tfa_u2f.keys.name + Name des Schlüssels + + + + + templates\Users\_2fa_settings.html.twig:124 + + + tfa_u2f.keys.added_date + Datum der Registrierung + + + + + templates\Users\_2fa_settings.html.twig:134 + + + tfa_u2f.key_delete + Schlüssel löschen + + + + + templates\Users\_2fa_settings.html.twig:141 + + + tfa_u2f.no_keys_registered + Keine Sicherheitsschlüssel registriert + + + + + templates\Users\_2fa_settings.html.twig:144 + + + tfa_u2f.add_new_key + Neuen Sicherheitsschlüssel registrieren + + + + + templates\Users\_2fa_settings.html.twig:148 + + + tfa_trustedDevices.explanation + aller Computer zurücksetzen.]]> + + + + + templates\Users\_2fa_settings.html.twig:149 + + + tfa_trustedDevices.invalidate.confirm_title + Wirklich alle vertrauenswürdigen Computer entfernen? + + + + + templates\Users\_2fa_settings.html.twig:150 + + + tfa_trustedDevices.invalidate.confirm_message + Sie werden auf allen Rechnern erneut eine Zwei-Faktor-Authentifizierung durchführen müssen. Achten Sie darauf, dass Sie ihr Zwei-Faktor-Gerät zur Hand haben. + + + + + templates\Users\_2fa_settings.html.twig:154 + + + tfa_trustedDevices.invalidate.btn + Alle vertrauenswürdigen Geräte entfernen + + templates\_navbar.html.twig:4 @@ -2150,16 +2769,6 @@ Subelemente werden beim Löschen nach oben verschoben. Eingeloggt als - - - templates\_navbar.html.twig:103 - templates\base.html.twig:101 - - - user.logout - Ausloggen - - templates\_navbar.html.twig:105 @@ -2335,6 +2944,9 @@ Subelemente werden beim Löschen nach oben verschoben. src\Controller\AdminPages\BaseAdminController.php:257 + src\Controller\UserController.php:89 + src\Controller\UserSettingsController.php:125 + src\Controller\UserSettingsController.php:155 csfr_invalid @@ -2403,7 +3015,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:85 + src\Controller\SecurityController.php:90 pw_reset.user_or_email @@ -2412,7 +3024,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:102 + src\Controller\SecurityController.php:107 pw_reset.request.success @@ -2421,7 +3033,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:127 + src\Controller\SecurityController.php:132 pw_reset.username @@ -2430,7 +3042,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:130 + src\Controller\SecurityController.php:135 pw_reset.token @@ -2439,7 +3051,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:155 + src\Controller\SecurityController.php:160 pw_reset.new_pw.error @@ -2448,16 +3060,70 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\SecurityController.php:157 + src\Controller\SecurityController.php:162 pw_reset.new_pw.success Passwort wurde erfolgreich zurückgesetzt. Sie können sich nun mit dem neuen Passwort einloggen. + + + src\Controller\UserController.php:87 + + + user.edit.reset_success + Alle Zwei-Faktor-Authentisierungsmethoden wurden erfolgreich deaktiviert. + + + + + src\Controller\UserSettingsController.php:72 + + + tfa_backup.no_codes_enabled + Es sind keine Backupcodes aktiviert! + + + + + src\Controller\UserSettingsController.php:109 + + + tfa_u2f.u2f_delete.not_existing + Es existiert kein Sicherheitsschlüssel mit dieser ID! + + + + + src\Controller\UserSettingsController.php:115 + + + tfa_u2f.u2f_delete.access_denied + Sie können nur ihre eigenen Sicherheitsschlüssel löschen! + + + + + src\Controller\UserSettingsController.php:122 + + + tfa.u2f.u2f_delete.success + Sicherheitsschlüssel erfolgreich entfernt. + + + + + src\Controller\UserSettingsController.php:153 + + + tfa_trustedDevice.invalidate.success + Vertrauenswürdige Geräte erfolgreich zurückgesetzt. + + - src\Controller\UserController.php:184 + src\Controller\UserSettingsController.php:195 src\Controller\UserController.php:98 new @@ -2468,7 +3134,7 @@ Subelemente werden beim Löschen nach oben verschoben. - src\Controller\UserController.php:224 + src\Controller\UserSettingsController.php:244 src\Controller\UserController.php:130 new @@ -2477,6 +3143,33 @@ Subelemente werden beim Löschen nach oben verschoben. Passwort geändert! + + + src\Controller\UserSettingsController.php:262 + + + user.settings.2fa.google.activated + Authenticator App erfolgreich aktiviert. + + + + + src\Controller\UserSettingsController.php:269 + + + user.settings.2fa.google.disabled + Authenticator App erfolgreich deaktiviert. + + + + + src\Controller\UserSettingsController.php:284 + + + user.settings.2fa.backup_codes.regenerated + Neue Backupcodes erfolgreich erzeugt. + + src\DataTables\AttachmentDataTable.php:89 @@ -3303,6 +3996,33 @@ Subelemente werden beim Löschen nach oben verschoben. Verschiedene + + + src\Form\TFAGoogleSettingsType.php:74 + + + tfa_google.enable + Authenticator App aktivieren + + + + + src\Form\TFAGoogleSettingsType.php:78 + + + tfa_google.disable + Authenticator App deaktivieren + + + + + src\Form\TFAGoogleSettingsType.php:56 + + + google_confirmation + Bestätigungscode + + src\Form\Type\SIUnitType.php:117 @@ -3711,6 +4431,15 @@ Subelemente werden beim Löschen nach oben verschoben. Neues Element + + + obsolete + + + tfa_backup.regenerate_codes + Neue Backupcodes erzeugen + + obsolete @@ -5497,7 +6226,7 @@ Element 3 user.settings.pw_old.label - Old password + Altes Passwort @@ -5509,5 +6238,29 @@ Element 3 Passwort zurücksetzen + + + bootstrap + bootstrap + + + + + cerulean + cerulean + + + + + tfa.provider.google + Authenticator App + + + + + tfa.provider.u2f_two_factor + Sicherheitsschlüssel + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index ab4dd2b4..a95053b4 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -264,7 +264,7 @@ templates\AdminPages\GroupAdmin.html.twig:9 - templates\AdminPages\UserAdmin.html.twig:12 + templates\AdminPages\UserAdmin.html.twig:16 user.edit.permissions @@ -315,7 +315,7 @@ - templates\AdminPages\UserAdmin.html.twig:4 + templates\AdminPages\UserAdmin.html.twig:8 user.edit.caption @@ -324,7 +324,7 @@ - templates\AdminPages\UserAdmin.html.twig:10 + templates\AdminPages\UserAdmin.html.twig:14 user.edit.configuration @@ -333,13 +333,102 @@ - templates\AdminPages\UserAdmin.html.twig:11 + templates\AdminPages\UserAdmin.html.twig:15 user.edit.password Password + + + templates\AdminPages\UserAdmin.html.twig:45 + + + user.edit.tfa.caption + Two-factor authentication + + + + + templates\AdminPages\UserAdmin.html.twig:47 + + + user.edit.tfa.google_active + Authenticator app active + + + + + templates\AdminPages\UserAdmin.html.twig:48 + templates\Users\backup_codes.html.twig:15 + templates\Users\_2fa_settings.html.twig:95 + + + tfa_backup.remaining_tokens + Remaining backup codes count + + + + + templates\AdminPages\UserAdmin.html.twig:49 + templates\Users\backup_codes.html.twig:17 + templates\Users\_2fa_settings.html.twig:96 + + + tfa_backup.generation_date + Generation date of the backup codes + + + + + templates\AdminPages\UserAdmin.html.twig:53 + templates\AdminPages\UserAdmin.html.twig:60 + + + user.edit.tfa.disabled + Method not enabled + + + + + templates\AdminPages\UserAdmin.html.twig:56 + + + user.edit.tfa.u2f_keys_count + Active security keys + + + + + templates\AdminPages\UserAdmin.html.twig:72 + + + user.edit.tfa.disable_tfa_title + Do you really want to proceed? + + + + + templates\AdminPages\UserAdmin.html.twig:72 + + + user.edit.tfa.disable_tfa_message + all active two-factor authentication methods of the user and delete the backup codes! +
+The user will have to set up all two-factor authentication methods again and print new backup codes!

+Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!]]>
+
+
+ + + templates\AdminPages\UserAdmin.html.twig:73 + + + user.edit.tfa.disable_tfa.btn + Disable all two-factor authentication methods + + templates\AdminPages\_attachments.html.twig:4 @@ -757,6 +846,24 @@ Subelements will be moved upwards.]]> False + + + templates\helper.twig:87 + + + Yes + Yes + + + + + templates\helper.twig:89 + + + No + No + + templates\homepage.html.twig:7 @@ -1781,6 +1888,63 @@ Subelements will be moved upwards.]]> Count of children elements + + + templates\security\2fa_base_form.html.twig:3 + templates\security\2fa_base_form.html.twig:5 + + + tfa.check.title + Two-factor authentication needed + + + + + templates\security\2fa_base_form.html.twig:39 + + + tfa.code.trusted_pc + This is a trusted computer (if this is enabled, no further two-factor queries are performed on this computer) + + + + + templates\security\2fa_base_form.html.twig:52 + templates\security\login.html.twig:58 + + + login.btn + Login + + + + + templates\security\2fa_base_form.html.twig:53 + templates\_navbar.html.twig:103 + + + user.logout + Logout + + + + + templates\security\2fa_form.html.twig:6 + + + tfa.check.code.label + Enter the 6-digit code from your Authenticator App or one of your backup codes if the Authenticator is not available. + + + + + templates\security\2fa_form.html.twig:10 + + + tfa.check.code.help + Authenticator app code + + templates\security\login.html.twig:3 @@ -1851,16 +2015,6 @@ Subelements will be moved upwards.]]> Remember me (should not be used on shared computers) - - - templates\security\login.html.twig:58 - templates\security\login.html.twig:58 - - - login.btn - Login - - templates\security\login.html.twig:64 @@ -1888,6 +2042,145 @@ Subelements will be moved upwards.]]> Request a new password + + + templates\security\U2F\u2f_login.html.twig:7 + templates\security\U2F\u2f_register.html.twig:10 + + + tfa_u2f.http_warning + You are accessing this page using the insecure HTTP method, so U2F will most likely not work (Bad Request error message). Ask an administrator to set up the secure HTTPS method if you want to use security keys. + + + + + templates\security\U2F\u2f_login.html.twig:10 + templates\security\U2F\u2f_register.html.twig:22 + + + r_u2f_two_factor.pressbutton + Please plug in your security key and press its button! + + + + + templates\security\U2F\u2f_register.html.twig:3 + + + tfa_u2f.add_key.title + Add security key + + + + + templates\security\U2F\u2f_register.html.twig:6 + templates\Users\_2fa_settings.html.twig:111 + + + tfa_u2f.explanation + With the help of a U2F/FIDO compatible security key (e.g. YubiKey or NitroKey), user-friendly and secure two-factor authentication can be achieved. The security keys can be registered here, and if two-factor verification is required, the key only needs to be inserted via USB or typed against the device via NFC. + + + + + templates\security\U2F\u2f_register.html.twig:7 + + + tfa_u2f.add_key.backup_hint + To ensure access even if the key is lost, it is recommended to register a second key as backup and store it in a safe place! + + + + + templates\security\U2F\u2f_register.html.twig:16 + + + r_u2f_two_factor.name + Shown key name (e.g. Backup) + + + + + templates\security\U2F\u2f_register.html.twig:19 + + + tfa_u2f.add_key.add_button + Add security key + + + + + templates\security\U2F\u2f_register.html.twig:27 + + + tfa_u2f.add_key.back_to_settings + Back to settings + + + + + templates\Users\backup_codes.html.twig:3 + templates\Users\backup_codes.html.twig:9 + + + tfa_backup.codes.title + Backup codes + + + + + templates\Users\backup_codes.html.twig:12 + + + tfa_backup.codes.explanation + Print out these codes and keep them in a safe place! + + + + + templates\Users\backup_codes.html.twig:13 + + + tfa_backup.codes.help + If you no longer have access to your device with the Authenticator App (lost smartphone, data loss, etc.) you can use one of these codes to access your account and possibly set up a new Authenticator App. Each of these codes can be used once, it is recommended to delete used codes. Anyone with access to these codes can potentially access your account, so keep them in a safe place. + + + + + templates\Users\backup_codes.html.twig:16 + + + tfa_backup.username + Username + + + + + templates\Users\backup_codes.html.twig:29 + + + tfa_backup.codes.page_generated_on + Page generated on %date% + + + + + templates\Users\backup_codes.html.twig:32 + + + tfa_backup.codes.print + Print + + + + + templates\Users\backup_codes.html.twig:35 + + + tfa_backup.codes.copy_clipboard + Copy to clipboard + + templates\Users\user_info.html.twig:3 @@ -2018,7 +2311,7 @@ Subelements will be moved upwards.]]> - templates\Users\user_settings.html.twig:53 + templates\Users\user_settings.html.twig:55 templates\Users\user_settings.html.twig:48 @@ -2026,6 +2319,333 @@ Subelements will be moved upwards.]]> Change password + + + templates\Users\_2fa_settings.html.twig:6 + + + user.settings.2fa_settings + Two-Factor Authentication + + + + + templates\Users\_2fa_settings.html.twig:13 + + + tfa.settings.google.tab + Authenticator app + + + + + templates\Users\_2fa_settings.html.twig:17 + + + tfa.settings.bakup.tab + Backup codes + + + + + templates\Users\_2fa_settings.html.twig:21 + + + tfa.settings.u2f.tab + Security keys (U2F) + + + + + templates\Users\_2fa_settings.html.twig:25 + + + tfa.settings.trustedDevices.tab + Trusted devices + + + + + templates\Users\_2fa_settings.html.twig:33 + + + tfa_google.disable.confirm_title + Do you really want to disable the Authenticator App? + + + + + templates\Users\_2fa_settings.html.twig:33 + + + tfa_google.disable.confirm_message + +Also note that without two-factor authentication your account is not as well protected against attackers!]]> + + + + + templates\Users\_2fa_settings.html.twig:39 + + + tfa_google.disabled_message + Authenticator app deactivated! + + + + + templates\Users\_2fa_settings.html.twig:48 + + + tfa_google.step.download + Google Authenticator oder FreeOTP Authenticator)]]> + + + + + templates\Users\_2fa_settings.html.twig:49 + + + tfa_google.step.scan + Scan the adjoining QR Code with the app or enter the data manually + + + + + templates\Users\_2fa_settings.html.twig:50 + + + tfa_google.step.input_code + Enter the generated code in the field below and confirm + + + + + templates\Users\_2fa_settings.html.twig:51 + + + tfa_google.step.download_backup + Print out your backup codes and store them in a safe place + + + + + templates\Users\_2fa_settings.html.twig:58 + + + tfa_google.manual_setup + Manual setup + + + + + templates\Users\_2fa_settings.html.twig:62 + + + tfa_google.manual_setup.type + Type + + + + + templates\Users\_2fa_settings.html.twig:63 + + + tfa_google.manual_setup.username + Username + + + + + templates\Users\_2fa_settings.html.twig:64 + + + tfa_google.manual_setup.secret + Secret + + + + + templates\Users\_2fa_settings.html.twig:65 + + + tfa_google.manual_setup.digit_count + Digit count + + + + + templates\Users\_2fa_settings.html.twig:74 + + + tfa_google.enabled_message + Authenticator App enabled + + + + + templates\Users\_2fa_settings.html.twig:83 + + + tfa_backup.disabled + Backup codes disabled. Setup authenticator app to enable backup codes. + + + + + templates\Users\_2fa_settings.html.twig:84 + templates\Users\_2fa_settings.html.twig:92 + + + tfa_backup.explanation + You can use these backup codes to access your account even if you lose the device with the Authenticator App. Print out the codes and keep them in a safe place. + + + + + templates\Users\_2fa_settings.html.twig:88 + + + tfa_backup.reset_codes.confirm_title + Really reset codes? + + + + + templates\Users\_2fa_settings.html.twig:88 + + + tfa_backup.reset_codes.confirm_message + This will delete all previous codes and generate a set of new codes. This cannot be undone. Remember to print out the new codes and store them in a safe place! + + + + + templates\Users\_2fa_settings.html.twig:91 + + + tfa_backup.enabled + Backup codes enabled + + + + + templates\Users\_2fa_settings.html.twig:99 + + + tfa_backup.show_codes + Show backup codes + + + + + templates\Users\_2fa_settings.html.twig:114 + + + tfa_u2f.table_caption + Registered security keys + + + + + templates\Users\_2fa_settings.html.twig:115 + + + tfa_u2f.delete_u2f.confirm_title + Really remove this security key? + + + + + templates\Users\_2fa_settings.html.twig:116 + + + tfa_u2f.delete_u2f.confirm_message + If you remove this key, then no more login with this key will be possible. If no security keys remain, two-factor authentication will be disabled. + + + + + templates\Users\_2fa_settings.html.twig:123 + + + tfa_u2f.keys.name + Key name + + + + + templates\Users\_2fa_settings.html.twig:124 + + + tfa_u2f.keys.added_date + Registration date + + + + + templates\Users\_2fa_settings.html.twig:134 + + + tfa_u2f.key_delete + Delete key + + + + + templates\Users\_2fa_settings.html.twig:141 + + + tfa_u2f.no_keys_registered + No keys registered yet. + + + + + templates\Users\_2fa_settings.html.twig:144 + + + tfa_u2f.add_new_key + Register new security key + + + + + templates\Users\_2fa_settings.html.twig:148 + + + tfa_trustedDevices.explanation + all computers here.]]> + + + + + templates\Users\_2fa_settings.html.twig:149 + + + tfa_trustedDevices.invalidate.confirm_title + Really remove all trusted computers? + + + + + templates\Users\_2fa_settings.html.twig:150 + + + tfa_trustedDevices.invalidate.confirm_message + You will have to perform two-factor authentication again on all computers. Make sure you have your two-factor device at hand. + + + + + templates\Users\_2fa_settings.html.twig:154 + + + tfa_trustedDevices.invalidate.btn + Reset trusted devices + + templates\_navbar.html.twig:4 @@ -2141,16 +2761,6 @@ Subelements will be moved upwards.]]> Logged in as - - - templates\_navbar.html.twig:103 - templates\base.html.twig:101 - - - user.logout - Logout - - templates\_navbar.html.twig:105 @@ -2325,6 +2935,9 @@ Subelements will be moved upwards.]]> src\Controller\AdminPages\BaseAdminController.php:257 + src\Controller\UserController.php:89 + src\Controller\UserSettingsController.php:125 + src\Controller\UserSettingsController.php:155 csfr_invalid @@ -2391,7 +3004,7 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:85 + src\Controller\SecurityController.php:90 pw_reset.user_or_email @@ -2400,7 +3013,7 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:102 + src\Controller\SecurityController.php:107 pw_reset.request.success @@ -2409,7 +3022,7 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:127 + src\Controller\SecurityController.php:132 pw_reset.username @@ -2418,7 +3031,7 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:130 + src\Controller\SecurityController.php:135 pw_reset.token @@ -2427,7 +3040,7 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:155 + src\Controller\SecurityController.php:160 pw_reset.new_pw.error @@ -2436,16 +3049,70 @@ Subelements will be moved upwards.]]> - src\Controller\SecurityController.php:157 + src\Controller\SecurityController.php:162 pw_reset.new_pw.success Password was reset successfully. You can now login with your new password. + + + src\Controller\UserController.php:87 + + + user.edit.reset_success + All two-factor authentication methods were successfully disabled. + + + + + src\Controller\UserSettingsController.php:72 + + + tfa_backup.no_codes_enabled + No backup codes enabled! + + + + + src\Controller\UserSettingsController.php:109 + + + tfa_u2f.u2f_delete.not_existing + No security key with this ID is existing. + + + + + src\Controller\UserSettingsController.php:115 + + + tfa_u2f.u2f_delete.access_denied + You can not delete the security keys of other users! + + + + + src\Controller\UserSettingsController.php:122 + + + tfa.u2f.u2f_delete.success + Security key successfully removed. + + + + + src\Controller\UserSettingsController.php:153 + + + tfa_trustedDevice.invalidate.success + Trusted devices successfully reset. + + - src\Controller\UserController.php:184 + src\Controller\UserSettingsController.php:195 src\Controller\UserController.php:98 @@ -2455,7 +3122,7 @@ Subelements will be moved upwards.]]> - src\Controller\UserController.php:224 + src\Controller\UserSettingsController.php:244 src\Controller\UserController.php:130 @@ -2463,6 +3130,33 @@ Subelements will be moved upwards.]]> Password changed! + + + src\Controller\UserSettingsController.php:262 + + + user.settings.2fa.google.activated + Authenticator App successfully activated. + + + + + src\Controller\UserSettingsController.php:269 + + + user.settings.2fa.google.disabled + Authenticator App erfolgreich deaktiviert. + + + + + src\Controller\UserSettingsController.php:284 + + + user.settings.2fa.backup_codes.regenerated + New backup codes successfully generated. + + src\DataTables\AttachmentDataTable.php:89 @@ -3289,6 +3983,33 @@ Subelements will be moved upwards.]]> Miscellaneous + + + src\Form\TFAGoogleSettingsType.php:74 + + + tfa_google.enable + Enable authenticator app + + + + + src\Form\TFAGoogleSettingsType.php:78 + + + tfa_google.disable + Deactivate authenticator app + + + + + src\Form\TFAGoogleSettingsType.php:56 + + + google_confirmation + Confirmation code + + src\Form\Type\SIUnitType.php:117 @@ -3689,6 +4410,15 @@ Subelements will be moved upwards.]]> New Element + + + obsolete + + + tfa_backup.regenerate_codes + Generate new backup codes + + obsolete @@ -5477,5 +6207,35 @@ Element 3 Reset password + + + u2f_two_factor + Security key (U2F) + + + + + google + google + + + + + tfa.provider.u2f_two_factor + Security key (U2F) + + + + + tfa.provider.google + Authenticator app + + + + + Login successful + Login successful + + diff --git a/webpack.config.js b/webpack.config.js index 38c7dd63..66ae51c2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -40,6 +40,8 @@ Encore * and one CSS file (e.g. app.css) if you JavaScript imports CSS. */ .addEntry('app', './assets/js/app.js') + + .addEntry('ru2ftwofactor', './assets/js/u2f_auth.js') //.addEntry('ajaxUI', './assets/ts_src/ajax_ui.ts') //.addEntry('page1', './assets/js/page1.js') //.addEntry('page2', './assets/js/page2.js') diff --git a/yarn.lock b/yarn.lock index 75261cf5..c0817730 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" @@ -1679,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" @@ -2379,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" @@ -2431,6 +2471,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" @@ -3322,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" @@ -3934,6 +3986,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 +5117,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 +5574,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" @@ -5902,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" @@ -6509,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" @@ -6617,6 +6702,11 @@ typescript@^3.3.4000: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.3.tgz#b36840668a16458a7025b9eabfad11b66ab85c69" integrity sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw== +u2f-api@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/u2f-api/-/u2f-api-1.1.1.tgz#594f600ff7bb49e8bb03e9f533e1f7bfc95f2e0f" + integrity sha512-tbmMBmg9eaFv+cdcBwja/hbpdXwcvBV0YSQh674nmP3HI0hNWcNAp9LRJ0jl9HGWLF7gF/3UKHzIxlXCW8j7kw== + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -7086,7 +7176,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 +7218,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"