From 2fa0963374879c69821aa320924d610c2a9802ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 14 Dec 2019 16:35:19 +0100 Subject: [PATCH 01/25] Prepared DB and models for Two Factor authentication --- composer.json | 2 + composer.lock | 410 +++++++++++++++++++++- config/bundles.php | 2 + config/packages/scheb_two_factor.yaml | 24 ++ config/routes/scheb_two_factor.yaml | 7 + src/Entity/Base/MasterAttachmentTrait.php | 4 +- src/Entity/Parts/Storelocation.php | 10 +- src/Entity/Parts/Supplier.php | 10 +- src/Entity/UserSystem/Group.php | 28 ++ src/Entity/UserSystem/U2FKey.php | 153 ++++++++ src/Entity/UserSystem/User.php | 253 +++++++++++-- src/Migrations/Version20191214153125.php | 40 +++ symfony.lock | 31 ++ 13 files changed, 935 insertions(+), 39 deletions(-) create mode 100644 config/packages/scheb_two_factor.yaml create mode 100644 config/routes/scheb_two_factor.yaml create mode 100644 src/Entity/UserSystem/U2FKey.php create mode 100644 src/Migrations/Version20191214153125.php diff --git a/composer.json b/composer.json index 60961feb..f70d3460 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,9 @@ "nyholm/psr7": "^1.1", "ocramius/proxy-manager": "2.1.*", "omines/datatables-bundle": "^0.3.1", + "r/u2f-two-factor-bundle": "^0.7.0", "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", diff --git a/composer.lock b/composer.lock index 2845f21e..cd2c1f89 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": "b2a4dbd5f3b049839c54b31fc4098eed", + "content-hash": "addae3263b772b9d16dce2392f8af0e7", "packages": [ + { + "name": "beberlei/assert", + "version": "v3.2.6", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "99508be011753690fe108ded450f5caaae180cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/99508be011753690fe108ded450f5caaae180cfa", + "reference": "99508be011753690fe108ded450f5caaae180cfa", + "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-10-10T10:33:57+00:00" + }, { "name": "doctrine/annotations", "version": "v1.8.0", @@ -1695,6 +1757,61 @@ ], "time": "2014-01-12T16:20:24+00:00" }, + { + "name": "lcobucci/jwt", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" + }, + "dist": { + "type": "zip", + "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.0", @@ -2305,6 +2422,68 @@ ], "time": "2019-08-09T12:19:19+00:00" }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2" + }, + "dist": { + "type": "zip", + "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.0", @@ -3067,6 +3246,69 @@ ], "time": "2017-10-23T01:57:42+00:00" }, + { + "name": "r/u2f-two-factor-bundle", + "version": "0.7.0", + "source": { + "type": "git", + "url": "https://github.com/darookee/u2f-two-factor-bundle.git", + "reference": "dcf391e694a8f237883b4c39cfe7367c344c1556" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/darookee/u2f-two-factor-bundle/zipball/dcf391e694a8f237883b4c39cfe7367c344c1556", + "reference": "dcf391e694a8f237883b4c39cfe7367c344c1556", + "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\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "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", + "Symfony2", + "fido", + "two-factor", + "two-step", + "yubikey" + ], + "time": "2019-06-05T14:42:26+00:00" + }, { "name": "s9e/regexp-builder", "version": "1.4.3", @@ -3173,6 +3415,74 @@ ], "time": "2019-11-17T16:03:56+00:00" }, + { + "name": "scheb/two-factor-bundle", + "version": "v4.11.0", + "source": { + "type": "git", + "url": "https://github.com/scheb/two-factor-bundle.git", + "reference": "eadac02014233ab45dac215d42fd06aaf629b09a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/scheb/two-factor-bundle/zipball/eadac02014233ab45dac215d42fd06aaf629b09a", + "reference": "eadac02014233ab45dac215d42fd06aaf629b09a", + "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-08T16:03:05+00:00" + }, { "name": "sensio/framework-extra-bundle", "version": "v5.5.1", @@ -3352,6 +3662,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", @@ -7721,6 +8092,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", diff --git a/config/bundles.php b/config/bundles.php index 277541c1..98992e27 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -21,4 +21,6 @@ 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], ]; diff --git a/config/packages/scheb_two_factor.yaml b/config/packages/scheb_two_factor.yaml new file mode 100644 index 00000000..6c692aaf --- /dev/null +++ b/config/packages/scheb_two_factor.yaml @@ -0,0 +1,24 @@ +# 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 + + 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/routes/scheb_two_factor.yaml b/config/routes/scheb_two_factor.yaml new file mode 100644 index 00000000..b574a0c9 --- /dev/null +++ b/config/routes/scheb_two_factor.yaml @@ -0,0 +1,7 @@ +2fa_login: + path: /2fa + defaults: + _controller: "scheb_two_factor.form_controller:form" + +2fa_login_check: + path: /2fa_check 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..a5bfdce8 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; + 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..1b7ec68b --- /dev/null +++ b/src/Entity/UserSystem/U2FKey.php @@ -0,0 +1,153 @@ +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; + } +} \ No newline at end of file diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 745a060d..ac935920 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -53,28 +53,37 @@ 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\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 { + use MasterAttachmentTrait; + /** The User id of the anonymous user */ public const ID_ANONYMOUS = 1; @@ -172,6 +181,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; + + /** @var Collection + * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user") + */ + protected $u2fKeys; + /** * @var array * @ORM\Column(type="json") @@ -227,6 +263,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe { parent::__construct(); $this->permissions = new PermissionsEmbed(); + $this->u2fKeys = new ArrayCollection(); } /** @@ -457,6 +494,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 +510,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 +519,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 +532,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 +541,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 +554,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** + * Gets the department of this user * @return string */ public function getDepartment(): ?string @@ -516,8 +563,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 +575,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe } /** + * Get the email of the user. * @return string */ public function getEmail(): ?string @@ -536,9 +584,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 +596,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 +606,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 +628,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 +640,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 +649,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 +661,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 +682,148 @@ 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; + $this->backupCodesGenerationDate = new \DateTime(); + return $this; + } + + /** + * Return the date when the backup codes were generated. + * @return \DateTime + */ + 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++; + } + + public function isU2FAuthEnabled(): bool + { + return count($this->u2fKeys) > 0; + } + + /** @return Collection */ + public function getU2FKeys(): Collection + { + return $this->u2fKeys; + } + + public function addU2FKey(TwoFactorKeyInterface $key): void + { + $this->u2fKeys->add($key); + } + + public function removeU2FKey(TwoFactorKeyInterface $key): void + { + $this->u2fKeys->remove($key); + } } diff --git a/src/Migrations/Version20191214153125.php b/src/Migrations/Version20191214153125.php new file mode 100644 index 00000000..68473e7c --- /dev/null +++ b/src/Migrations/Version20191214153125.php @@ -0,0 +1,40 @@ +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/symfony.lock b/symfony.lock index 1e783bbe..c2c27eed 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" }, @@ -226,6 +232,9 @@ "openlss/lib-array2xml": { "version": "1.0.0" }, + "paragonie/constant_time_encoding": { + "version": "v2.3.0" + }, "php": { "version": "7.1.3" }, @@ -274,6 +283,9 @@ "psr/simple-cache": { "version": "1.0.1" }, + "r/u2f-two-factor-bundle": { + "version": "0.7.0" + }, "roave/security-advisories": { "version": "dev-master" }, @@ -283,6 +295,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" }, @@ -313,6 +338,9 @@ "shivas/versioning-bundle": { "version": "3.1.3" }, + "spomky-labs/otphp": { + "version": "v9.1.4" + }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -724,6 +752,9 @@ "webmozart/path-util": { "version": "2.3.0" }, + "yubico/u2flib-server": { + "version": "1.0.2" + }, "zendframework/zend-code": { "version": "3.3.1" }, From 35b5640627ad3d9284d46b519af9d41d51bde7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 23 Dec 2019 17:20:28 +0100 Subject: [PATCH 02/25] Added an basic form to add Google Authenticator. --- assets/js/app.js | 2 + assets/ts_src/event_listeners.ts | 11 +++ package.json | 1 + src/Controller/UserController.php | 27 ++++++- src/Form/TFASettingsType.php | 42 +++++++++++ .../Constraints/ValidGoogleAuthCode.php | 12 +++ .../ValidGoogleAuthCodeValidator.php | 65 ++++++++++++++++ templates/Users/_2fa_settings.html.twig | 44 +++++++++++ templates/Users/user_settings.html.twig | 2 + yarn.lock | 74 ++++++++++++++++++- 10 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 src/Form/TFASettingsType.php create mode 100644 src/Validator/Constraints/ValidGoogleAuthCode.php create mode 100644 src/Validator/Constraints/ValidGoogleAuthCodeValidator.php create mode 100644 templates/Users/_2fa_settings.html.twig diff --git a/assets/js/app.js b/assets/js/app.js index 9287e85d..ccad4ba3 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -90,6 +90,8 @@ require('./jquery.tristate.js'); require('darkmode-js'); +window.QRCode = require('qrcode'); + require('../ts_src/ajax_ui'); import {ajaxUI} from "../ts_src/ajax_ui"; diff --git a/assets/ts_src/event_listeners.ts b/assets/ts_src/event_listeners.ts index 1a4f203a..8a3ed399 100644 --- a/assets/ts_src/event_listeners.ts +++ b/assets/ts_src/event_listeners.ts @@ -23,6 +23,7 @@ import {ajaxUI} from "./ajax_ui"; import "bootbox"; import "marked"; import * as marked from "marked"; +import "qrcode"; import {parse} from "marked"; /************************************ @@ -458,6 +459,16 @@ $(document).on("ajaxUI:start ajaxUI:reload attachment:create", function() { $('select.attachment_type_selector').change(updater).each(updater); }); +$(document).on("ajaxUI:start ajaxUI:reload", function() { + $('.qrcode').each(function() { + let canvas = $(this); + //@ts-ignore + QRCode.toCanvas(canvas[0], canvas.data('content'), function(error) { + if(error) console.error(error); + }) + }); +}); + //Need for proper body padding, with every navbar height $(window).resize(function () { let height : number = $('#navbar').height() + 10; diff --git a/package.json b/package.json index 9d9f378a..99980aad 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "marked": "^0.7.0", "patternfly-bootstrap-treeview": "^2.1.8", "pdfmake": "^0.1.53", + "qrcode": "^1.4.4", "ts-loader": "^5.3.3", "typescript": "^3.3.4000" } diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index a93c8b3e..62f6dddd 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -25,12 +25,14 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\UserAttachment; use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; +use App\Form\TFASettingsType; use App\Form\UserAdminForm; use App\Form\UserSettingsType; use App\Services\EntityExporter; use App\Services\EntityImporter; use App\Services\StructuralElementRecursionHelper; use Doctrine\ORM\EntityManagerInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; use Symfony\Component\Asset\Packages; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\RepeatedType; @@ -150,7 +152,7 @@ class UserController extends AdminPages\BaseAdminController /** * @Route("/settings", name="user_settings") */ - public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder) + public function userSettings(Request $request, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticator $googleAuthenticator) { /** * @var User @@ -224,6 +226,22 @@ class UserController extends AdminPages\BaseAdminController $this->addFlash('success', 'user.settings.pw_changed_flash'); } + //Handle 2FA things + $tfa_form = $this->createForm(TFASettingsType::class, $user); + $tfa_form->handleRequest($request); + if (!$user->getGoogleAuthenticatorSecret()) { + $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); + $tfa_form->setData($user); + } + + if ($tfa_form->isSubmitted() && $tfa_form->isValid()) { + //Save 2FA settings (save secrets) + $user->setGoogleAuthenticatorSecret($tfa_form->get('googleAuthenticatorSecret')->getData()); + $em->flush(); + $this->addFlash('success', 'user.settings.2fa.google.activated'); + } + + /****************************** * Output both forms *****************************/ @@ -232,6 +250,13 @@ class UserController extends AdminPages\BaseAdminController 'settings_form' => $form->createView(), 'pw_form' => $pw_form->createView(), 'page_need_reload' => $page_need_reload, + + 'tfa_form' => $tfa_form->createView(), + 'tfa_google' => [ + 'qrContent' => $googleAuthenticator->getQRContent($user), + 'secret' => $user->getGoogleAuthenticatorSecret(), + 'username' => $user->getGoogleAuthenticatorUsername() + ] ]); } diff --git a/src/Form/TFASettingsType.php b/src/Form/TFASettingsType.php new file mode 100644 index 00000000..ae1d9326 --- /dev/null +++ b/src/Form/TFASettingsType.php @@ -0,0 +1,42 @@ +add('google_confirmation', TextType::class, [ + 'mapped' => false, + 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*'], + 'constraints' => [new ValidGoogleAuthCode()] + ]); + + $builder->add('googleAuthenticatorSecret', HiddenType::class,[ + 'disabled' => false, + ]); + + + $builder->add('submit', SubmitType::class); + $builder->add('cancel', ResetType::class); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/ValidGoogleAuthCode.php b/src/Validator/Constraints/ValidGoogleAuthCode.php new file mode 100644 index 00000000..ef89a896 --- /dev/null +++ b/src/Validator/Constraints/ValidGoogleAuthCode.php @@ -0,0 +1,12 @@ +googleAuthenticator = $googleAuthenticator; + } + + /** + * @inheritDoc + */ + public function validate($value, Constraint $constraint) + { + if (!$constraint instanceof ValidGoogleAuthCode) { + throw new UnexpectedTypeException($constraint, ValidGoogleAuthCode::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!\is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + if(!ctype_digit($value)) { + $this->context->addViolation('validator.google_code.only_digits_allowed'); + } + + //Number must have 6 digits + if(strlen($value) !== 6) { + $this->context->addViolation('validator.google_code.wrong_digit_count'); + } + + //Try to retrieve the user we want to check + if($this->context->getObject() instanceof FormInterface && + $this->context->getObject()->getParent() instanceof FormInterface + && $this->context->getObject()->getParent()->getData() instanceof User) { + $user = $this->context->getObject()->getParent()->getData(); + + //Check if the given code is valid + if(!$this->googleAuthenticator->checkCode($user, $value)) { + $this->context->addViolation('validator.google_code.wrong_code'); + } + + } + } +} \ No newline at end of file diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig new file mode 100644 index 00000000..19b9d23b --- /dev/null +++ b/templates/Users/_2fa_settings.html.twig @@ -0,0 +1,44 @@ +
+
+ + {% trans %}user.settings.2fa_settings{% endtrans %} +
+
+ {{ form_start(tfa_form) }} + +
+
+
+
+ +
+
+
    +
  1. {% trans %}tfa_google.step.download{% endtrans %}
  2. +
  3. {% trans %}tfa_google.step.scan{% endtrans %}
  4. +
  5. {% trans %}tfa_google.step.input_code{% endtrans %}
  6. +
+
+
+ + {{ form_row(tfa_form.google_confirmation) }} +
+
+ +
+
+ + {{ form_row(tfa_form.submit) }} + {{ form_row(tfa_form.cancel) }} + {{ form_end(tfa_form) }} +
+
\ No newline at end of file diff --git a/templates/Users/user_settings.html.twig b/templates/Users/user_settings.html.twig index df0c93fa..8319c204 100644 --- a/templates/Users/user_settings.html.twig +++ b/templates/Users/user_settings.html.twig @@ -47,6 +47,8 @@ {% block content %} {{ parent() }} + {% include "Users/_2fa_settings.html.twig" %} +
diff --git a/yarn.lock b/yarn.lock index 75261cf5..28e056a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1490,12 +1490,30 @@ browserslist@^4.0.0, browserslist@^4.6.0, browserslist@^4.8.2: electron-to-chromium "^1.3.322" node-releases "^1.1.42" +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + buffer-equal@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= -buffer-from@^1.0.0: +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + +buffer-from@^1.0.0, buffer-from@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== @@ -1519,6 +1537,14 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" +buffer@^5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2431,6 +2457,11 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dijkstrajs@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b" + integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs= + dir-glob@^2.0.0: version "2.2.2" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" @@ -3934,6 +3965,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isarray@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -5060,6 +5096,11 @@ png-js@^1.0.0: resolved "https://registry.yarnpkg.com/png-js/-/png-js-1.0.0.tgz#e5484f1e8156996e383aceebb3789fd75df1874d" integrity sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g== +pngjs@^3.3.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" + integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== + popper.js@>=1.12.9, popper.js@^1.14.1, popper.js@^1.14.7: version "1.16.0" resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.0.tgz#2e1816bcbbaa518ea6c2e15a466f4cb9c6e2fbb3" @@ -5512,6 +5553,19 @@ q@^1.1.2: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qrcode@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.4.4.tgz#f0c43568a7e7510a55efc3b88d9602f71963ea83" + integrity sha512-oLzEC5+NKFou9P0bMj5+v6Z40evexeE29Z9cummZXZ9QXyMr3lphkURzxjXgPJC5azpxcshoDWV1xE46z+/c3Q== + dependencies: + buffer "^5.4.3" + buffer-alloc "^1.2.0" + buffer-from "^1.1.1" + dijkstrajs "^1.0.1" + isarray "^2.0.1" + pngjs "^3.3.0" + yargs "^13.2.4" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" @@ -7086,7 +7140,7 @@ yargs-parser@^12.0.0: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^13.1.0: +yargs-parser@^13.1.0, yargs-parser@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== @@ -7128,3 +7182,19 @@ yargs@13.2.4: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^13.1.0" + +yargs@^13.2.4: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" From 10ca8953096c1460969a2653cfadc1b5e5dfc3f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 23 Dec 2019 18:45:32 +0100 Subject: [PATCH 03/25] Implemented the two factor auth login form. --- assets/ts_src/ajax_ui.ts | 5 +++ config/packages/scheb_two_factor.yaml | 1 + config/packages/security.yaml | 11 ++++- config/routes/scheb_two_factor.yaml | 4 +- templates/security/2fa_form.html.twig | 61 +++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 templates/security/2fa_form.html.twig diff --git a/assets/ts_src/ajax_ui.ts b/assets/ts_src/ajax_ui.ts index 98da4eaf..af6661bb 100644 --- a/assets/ts_src/ajax_ui.ts +++ b/assets/ts_src/ajax_ui.ts @@ -363,6 +363,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/config/packages/scheb_two_factor.yaml b/config/packages/scheb_two_factor.yaml index 6c692aaf..41feb3d2 100644 --- a/config/packages/scheb_two_factor.yaml +++ b/config/packages/scheb_two_factor.yaml @@ -11,6 +11,7 @@ scheb_two_factor: 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 diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 5fd51805..4fa6ccb1 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,7 @@ 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: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS } diff --git a/config/routes/scheb_two_factor.yaml b/config/routes/scheb_two_factor.yaml index b574a0c9..b4ef9116 100644 --- a/config/routes/scheb_two_factor.yaml +++ b/config/routes/scheb_two_factor.yaml @@ -1,7 +1,7 @@ 2fa_login: - path: /2fa + path: /{_locale}/2fa defaults: _controller: "scheb_two_factor.form_controller:form" 2fa_login_check: - path: /2fa_check + path: /{_locale}/2fa_check diff --git a/templates/security/2fa_form.html.twig b/templates/security/2fa_form.html.twig new file mode 100644 index 00000000..cfd1b587 --- /dev/null +++ b/templates/security/2fa_form.html.twig @@ -0,0 +1,61 @@ +{% 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 #} + +
+
+ +
+ + + {% trans %}tfa.check.code.help{% endtrans %} + +
+
+ {% if displayTrustedOption %} +
+
+
+ + +
+
+
+ {% endif %} + + + {% if isCsrfProtectionEnabled %} + + {% endif %} +
+
+ + {% trans %}user.logout{% endtrans %} +
+
+
+{% endblock %} \ No newline at end of file From eb3c34b75f7e71f47e9546db0384d30629cf424a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 23 Dec 2019 19:00:27 +0100 Subject: [PATCH 04/25] Added autocomplete attributes on password change form in user settings. --- src/Controller/UserController.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 62f6dddd..8985e16d 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -34,9 +34,11 @@ use App\Services\StructuralElementRecursionHelper; use Doctrine\ORM\EntityManagerInterface; use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticator; use Symfony\Component\Asset\Packages; +use Symfony\Component\Form\Extension\Core\Type\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; @@ -193,9 +195,17 @@ class UserController extends AdminPages\BaseAdminController $demo_mode = $this->getParameter('demo_mode'); $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' => $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' => $demo_mode, @@ -203,10 +213,13 @@ class UserController extends AdminPages\BaseAdminController '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, - ])], + 'min' => 6, + 'max' => 128, + ])], ]) ->add('submit', SubmitType::class, ['label' => 'save']) ->getForm(); From 25105ba7d4fbd7a9fc12ec20fbc997f66eb2d943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 24 Dec 2019 15:20:26 +0100 Subject: [PATCH 05/25] Set default value of enforce_2fa to fix travis. --- src/Entity/UserSystem/Group.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/UserSystem/Group.php b/src/Entity/UserSystem/Group.php index a5bfdce8..259623ef 100644 --- a/src/Entity/UserSystem/Group.php +++ b/src/Entity/UserSystem/Group.php @@ -68,7 +68,7 @@ class Group extends StructuralDBElement implements HasPermissionsInterface * @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; + protected $enforce2FA = false; public function __construct() { From 8c5cf6f9e0a6b6cd46b61e6aa81a4df59e93388b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 24 Dec 2019 16:07:22 +0100 Subject: [PATCH 06/25] Show data for manual setup of the google authenticator. --- src/Form/TFASettingsType.php | 2 +- templates/Users/_2fa_settings.html.twig | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Form/TFASettingsType.php b/src/Form/TFASettingsType.php index ae1d9326..fa9da818 100644 --- a/src/Form/TFASettingsType.php +++ b/src/Form/TFASettingsType.php @@ -20,7 +20,7 @@ class TFASettingsType extends AbstractType { $builder->add('google_confirmation', TextType::class, [ 'mapped' => false, - 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*'], + 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*', 'autocomplete' => 'off'], 'constraints' => [new ValidGoogleAuthCode()] ]); diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig index 19b9d23b..f24ec557 100644 --- a/templates/Users/_2fa_settings.html.twig +++ b/templates/Users/_2fa_settings.html.twig @@ -26,10 +26,26 @@
  • {% trans %}tfa_google.step.download{% endtrans %}
  • {% trans %}tfa_google.step.scan{% endtrans %}
  • {% trans %}tfa_google.step.input_code{% endtrans %}
  • +
  • {% trans %}tfa_google.step.download_backup{% endtrans %}
  • +
    + +
    +
    +

    {% 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(tfa_form.google_confirmation) }}
    From 8add8c919dd0609df0f530a2ad2296ce654c1682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Thu, 26 Dec 2019 12:46:44 +0100 Subject: [PATCH 07/25] Allow to disable the google authenticator 2fa. --- src/Controller/UserController.php | 31 ++++++---- src/Form/TFAGoogleSettingsType.php | 77 +++++++++++++++++++++++++ src/Form/TFASettingsType.php | 42 -------------- templates/Users/_2fa_settings.html.twig | 67 +++++++++++---------- 4 files changed, 133 insertions(+), 84 deletions(-) create mode 100644 src/Form/TFAGoogleSettingsType.php delete mode 100644 src/Form/TFASettingsType.php diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 8985e16d..4f346981 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -25,7 +25,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\UserAttachment; use App\Entity\UserSystem\User; use App\Form\Permissions\PermissionsType; -use App\Form\TFASettingsType; +use App\Form\TFAGoogleSettingsType; use App\Form\UserAdminForm; use App\Form\UserSettingsType; use App\Services\EntityExporter; @@ -240,18 +240,26 @@ class UserController extends AdminPages\BaseAdminController } //Handle 2FA things - $tfa_form = $this->createForm(TFASettingsType::class, $user); - $tfa_form->handleRequest($request); - if (!$user->getGoogleAuthenticatorSecret()) { + $google_form = $this->createForm(TFAGoogleSettingsType::class, $user); + $google_enabled = $user->isGoogleAuthenticatorEnabled(); + if (!$form->isSubmitted() && !$google_enabled) { $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret()); - $tfa_form->setData($user); + $google_form->get('googleAuthenticatorSecret')->setData($user->getGoogleAuthenticatorSecret()); } + $google_form->handleRequest($request); - if ($tfa_form->isSubmitted() && $tfa_form->isValid()) { - //Save 2FA settings (save secrets) - $user->setGoogleAuthenticatorSecret($tfa_form->get('googleAuthenticatorSecret')->getData()); - $em->flush(); - $this->addFlash('success', 'user.settings.2fa.google.activated'); + if($google_form->isSubmitted() && $google_form->isValid()) { + if (!$google_enabled) { + //Save 2FA settings (save secrets) + $user->setGoogleAuthenticatorSecret($google_form->get('googleAuthenticatorSecret')->getData()); + $em->flush(); + $this->addFlash('success', 'user.settings.2fa.google.activated'); + } elseif ($google_enabled) { + //Remove secret to disable google authenticator + $user->setGoogleAuthenticatorSecret(null); + $em->flush(); + $this->addFlash('success', 'user.settings.2fa.google.disabled'); + } } @@ -264,8 +272,9 @@ class UserController extends AdminPages\BaseAdminController 'pw_form' => $pw_form->createView(), 'page_need_reload' => $page_need_reload, - 'tfa_form' => $tfa_form->createView(), + 'google_form' => $google_form->createView(), 'tfa_google' => [ + 'enabled' => $google_enabled, 'qrContent' => $googleAuthenticator->getQRContent($user), 'secret' => $user->getGoogleAuthenticatorSecret(), 'username' => $user->getGoogleAuthenticatorUsername() diff --git a/src/Form/TFAGoogleSettingsType.php b/src/Form/TFAGoogleSettingsType.php new file mode 100644 index 00000000..3b5362bf --- /dev/null +++ b/src/Form/TFAGoogleSettingsType.php @@ -0,0 +1,77 @@ +translator = $translator; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->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' => $this->translator->trans('tfa_google.enable') + ]); + } else { + $form->add('submit', SubmitType::class, [ + 'label' => $this->translator->trans('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/Form/TFASettingsType.php b/src/Form/TFASettingsType.php deleted file mode 100644 index fa9da818..00000000 --- a/src/Form/TFASettingsType.php +++ /dev/null @@ -1,42 +0,0 @@ -add('google_confirmation', TextType::class, [ - 'mapped' => false, - 'attr' => ['maxlength' => '6', 'minlength' => '6', 'pattern' => '\d*', 'autocomplete' => 'off'], - 'constraints' => [new ValidGoogleAuthCode()] - ]); - - $builder->add('googleAuthenticatorSecret', HiddenType::class,[ - 'disabled' => false, - ]); - - - $builder->add('submit', SubmitType::class); - $builder->add('cancel', ResetType::class); - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults([ - 'data_class' => User::class, - ]); - } -} \ No newline at end of file diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig index f24ec557..0ac1c6cd 100644 --- a/templates/Users/_2fa_settings.html.twig +++ b/templates/Users/_2fa_settings.html.twig @@ -4,7 +4,7 @@ {% trans %}user.settings.2fa_settings{% endtrans %}
    - {{ form_start(tfa_form) }} +
    @@ -90,6 +94,37 @@ {% 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 %} +
    +
    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/security/2fa_base_form.html.twig b/templates/security/2fa_base_form.html.twig new file mode 100644 index 00000000..b75e14cf --- /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 index cfd1b587..c38018e6 100644 --- a/templates/security/2fa_form.html.twig +++ b/templates/security/2fa_form.html.twig @@ -1,61 +1,14 @@ -{% extends "main_card.html.twig" %} +{% extends "security/2fa_base_form.html.twig" %} -{% block title %}{% trans %}tfa.check.title{% endtrans %}{% endblock %} -{% block card_title %} {% trans %}tfa.check.title{% endtrans %}{% endblock %} - -{% block content %} - {% if authenticationError %} -