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] 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" },