From ac978abe1dce7b43dfa926211a825cd5e89cc034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 4 Oct 2022 00:08:58 +0200 Subject: [PATCH] Allow to register Webauthn Keys --- assets/js/webauthn_tfa.js | 12 +- composer.json | 5 +- composer.lock | 286 +++++++++++++++++- config/bundles.php | 2 + migrations/Version20221003212851.php | 33 ++ .../WebauthnKeyRegistrationController.php | 27 +- src/Entity/UserSystem/User.php | 20 +- src/Entity/UserSystem/WebauthnKey.php | 98 ++++++ symfony.lock | 12 + templates/Users/_2fa_settings.html.twig | 16 +- ....html.twig => webauthn_register.html.twig} | 0 11 files changed, 486 insertions(+), 25 deletions(-) create mode 100644 migrations/Version20221003212851.php create mode 100644 src/Entity/UserSystem/WebauthnKey.php rename templates/security/Webauthn/{u2f_register.html.twig => webauthn_register.html.twig} (100%) diff --git a/assets/js/webauthn_tfa.js b/assets/js/webauthn_tfa.js index 5c7c5c94..fda5a066 100644 --- a/assets/js/webauthn_tfa.js +++ b/assets/js/webauthn_tfa.js @@ -117,11 +117,7 @@ class WebauthnTFA { constructor() { const register_dom_ready = (fn) => { - if (document.readyState !== 'loading') { - fn(); - } else { - document.addEventListener('DOMContentLoaded', fn); - } + document.addEventListener('turbo:load', fn) } register_dom_ready(() => { @@ -162,12 +158,6 @@ class WebauthnTFA { this.register(form, {publicKey: options}); }); } - - - - //Catch submit event and do webauthn stuff - - }); } diff --git a/composer.json b/composer.json index 3955f213..d94c81f7 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "florianv/swap": "^4.0", "florianv/swap-bundle": "dev-master", "gregwar/captcha-bundle": "^2.1.0", + "jbtronics/2fa-webauthn": "dev-master", "league/html-to-markdown": "^5.0.1", "liip/imagine-bundle": "^2.2", "nelmio/security-bundle": "^3.0", @@ -69,8 +70,8 @@ "twig/inky-extra": "^3.0", "twig/intl-extra": "^3.0", "twig/markdown-extra": "^3.0", - "webmozart/assert": "^1.4", - "jbtronics/2fa-webauthn": "dev-master" + "web-auth/webauthn-symfony-bundle": "^3.3", + "webmozart/assert": "^1.4" }, "require-dev": { "dama/doctrine-test-bundle": "^7.0", diff --git a/composer.lock b/composer.lock index cecdb5e6..fa8a9f12 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e94249960fe23d18a3977983c795266c", + "content-hash": "acfef40c9197b33a535f02f56ae7fd3c", "packages": [ { "name": "beberlei/assert", @@ -5727,6 +5727,74 @@ ], "time": "2020-11-03T09:10:25+00:00" }, + { + "name": "spomky-labs/cbor-bundle", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-bundle.git", + "reference": "65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-bundle/zipball/65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f", + "reference": "65a5a65e7fc20eca383a0be8f3ed287a4fe80b1f", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "spomky-labs/cbor-php": "^1.0|^2.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4|^5.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-beberlei-assert": "^0.12", + "phpstan/phpstan-deprecation-rules": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpstan/phpstan-strict-rules": "^0.12", + "phpunit/phpunit": "^9.0", + "symfony/framework-bundle": "^4.4|^5.0", + "symfony/phpunit-bridge": "^4.4|^5.0", + "thecodingmachine/phpstan-safe-rule": "^1.0@beta" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SpomkyLabs\\CborBundle\\": "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/cbor-bundle/contributors" + } + ], + "description": "CBOR Encoder/Decoder Bundle for Symfony.", + "homepage": "https://github.com/spomky-labs", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "bundle", + "cbor", + "symfony" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-bundle/issues", + "source": "https://github.com/Spomky-Labs/cbor-bundle/tree/v2.0.3" + }, + "time": "2020-07-12T22:47:45+00:00" + }, { "name": "spomky-labs/cbor-php", "version": "v2.1.0", @@ -12665,6 +12733,222 @@ ], "time": "2022-02-18T07:13:44+00:00" }, + { + "name": "web-auth/webauthn-symfony-bundle", + "version": "v3.3.12", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", + "reference": "15f2091dc351f190d27a377a0dbbc117e6be5329" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/15f2091dc351f190d27a377a0dbbc117e6be5329", + "reference": "15f2091dc351f190d27a377a0dbbc117e6be5329", + "shasum": "" + }, + "require": { + "spomky-labs/cbor-bundle": "^2.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", + "web-auth/webauthn-lib": "self.version", + "web-token/jwt-signature": "^2.0.9" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Webauthn\\Bundle\\": "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/web-auth/webauthn-symfony-bundle/contributors" + } + ], + "description": "FIDO2/Webauthn Security Bundle For Symfony", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/v3.3.12" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-11-21T11:14:31+00:00" + }, + { + "name": "web-token/jwt-core", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-core.git", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-core/zipball/53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "reference": "53beb6f6c1eec4fa93c1c3e5d9e5701e71fa1678", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.17|^0.9", + "ext-json": "*", + "ext-mbstring": "*", + "fgrosse/phpasn1": "^2.0", + "php": ">=7.2", + "spomky-labs/base64url": "^1.0|^2.0" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Core\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "Core component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-core/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-03-17T14:55:52+00:00" + }, + { + "name": "web-token/jwt-signature", + "version": "v2.2.11", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature.git", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature/zipball/015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "reference": "015b59aaf3b6e8fb9f5bd1338845b7464c7d8103", + "shasum": "" + }, + "require": { + "web-token/jwt-core": "^2.1" + }, + "suggest": { + "web-token/jwt-signature-algorithm-ecdsa": "ECDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-eddsa": "EdDSA Based Signature Algorithms", + "web-token/jwt-signature-algorithm-experimental": "Experimental Signature Algorithms", + "web-token/jwt-signature-algorithm-hmac": "HMAC Based Signature Algorithms", + "web-token/jwt-signature-algorithm-none": "None Signature Algorithm", + "web-token/jwt-signature-algorithm-rsa": "RSA Based Signature Algorithms" + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\Signature\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-signature/contributors" + } + ], + "description": "Signature component of the JWT Framework.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature/tree/v2.2.11" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2021-03-01T19:55:28+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", diff --git a/config/bundles.php b/config/bundles.php index 64f77e59..8ca67ae7 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -25,4 +25,6 @@ return [ Symfony\UX\Turbo\TurboBundle::class => ['all' => true], Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true], Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], + SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true], + Webauthn\Bundle\WebauthnBundle::class => ['all' => true], ]; diff --git a/migrations/Version20221003212851.php b/migrations/Version20221003212851.php new file mode 100644 index 00000000..bef8ae47 --- /dev/null +++ b/migrations/Version20221003212851.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE webauthn_keys (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, public_key_credential_id LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', type VARCHAR(255) NOT NULL, transports LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', attestation_type VARCHAR(255) NOT NULL, trust_path LONGTEXT NOT NULL COMMENT \'(DC2Type:trust_path)\', aaguid TINYTEXT NOT NULL COMMENT \'(DC2Type:aaguid)\', credential_public_key LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', user_handle VARCHAR(255) NOT NULL, counter INT 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_799FD143A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE webauthn_keys ADD CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE webauthn_keys DROP FOREIGN KEY FK_799FD143A76ED395'); + $this->addSql('DROP TABLE webauthn_keys'); + } +} diff --git a/src/Controller/WebauthnKeyRegistrationController.php b/src/Controller/WebauthnKeyRegistrationController.php index 512f6658..945fc2f0 100644 --- a/src/Controller/WebauthnKeyRegistrationController.php +++ b/src/Controller/WebauthnKeyRegistrationController.php @@ -2,17 +2,21 @@ namespace App\Controller; +use App\Entity\UserSystem\WebauthnKey; +use Doctrine\ORM\EntityManagerInterface; use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Annotation\Route; +use function Symfony\Component\Translation\t; + class WebauthnKeyRegistrationController extends AbstractController { /** * @Route("/webauthn/register", name="webauthn_register") */ - public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper) + public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em) { //If form was submitted, check the auth response @@ -21,18 +25,33 @@ class WebauthnKeyRegistrationController extends AbstractController //Retrieve other data from the form, that you want to store with the key $keyName = $request->request->get('keyName'); + if (empty($keyName)) { + $keyName = 'Key ' . date('Y-m-d H:i:s'); + } //Check the response - $new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse); + try { + $new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse); + } catch (\Exception $exception) { + $this->addFlash('error', t('tfa_u2f.add_key.registration_error')); + return $this->redirectToRoute('webauthn_register'); + } + + $keyEntity = WebauthnKey::fromRegistration($new_key); + $keyEntity->setName($keyName); + $keyEntity->setUser($this->getUser()); + + $em->persist($keyEntity); + $em->flush(); - dump($new_key); $this->addFlash('success', 'Key registered successfully'); + return $this->redirectToRoute('user_settings'); } return $this->render( - 'Security/U2F/u2f_register.html.twig', + 'Security/Webauthn/webauthn_register.html.twig', [ 'registrationRequest' => $registrationHelper->generateRegistrationRequestAsJSON(), ] diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 5c9c0de9..1671c419 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -57,6 +57,7 @@ use App\Entity\PriceInformations\Currency; use App\Security\Interfaces\HasPermissionsInterface; use App\Validator\Constraints\Selectable; use App\Validator\Constraints\ValidPermission; +use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Webauthn\PublicKeyCredentialUserEntity; use function count; @@ -241,11 +242,17 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe */ protected ?DateTime $backupCodesGenerationDate = null; - /** @var Collection + /** @var Collection * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) */ protected $u2fKeys; + /** + * @var Collection + * @ORM\OneToMany(targetEntity="App\Entity\UserSystem\WebauthnKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true) + */ + protected $webauthn_keys; + /** * @var Currency|null The currency the user wants to see prices in. * Dont use fetch=EAGER here, this will cause problems with setting the currency setting. @@ -274,6 +281,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe parent::__construct(); $this->permissions = new PermissionsEmbed(); $this->u2fKeys = new ArrayCollection(); + $this->webauthn_keys = new ArrayCollection(); } /** @@ -851,7 +859,8 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe public function isWebAuthnAuthenticatorEnabled(): bool { - return count($this->u2fKeys) > 0; + return count($this->u2fKeys) > 0 + || count($this->webauthn_keys) > 0; } public function getLegacyU2FKeys(): iterable @@ -870,6 +879,11 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe public function getWebauthnKeys(): iterable { - return []; + return $this->webauthn_keys; + } + + public function addWebauthnKey(WebauthnKey $webauthnKey): void + { + $this->webauthn_keys->add($webauthnKey); } } diff --git a/src/Entity/UserSystem/WebauthnKey.php b/src/Entity/UserSystem/WebauthnKey.php new file mode 100644 index 00000000..a4de7f10 --- /dev/null +++ b/src/Entity/UserSystem/WebauthnKey.php @@ -0,0 +1,98 @@ +name; + } + + /** + * @param string $name + * @return WebauthnKey + */ + public function setName(string $name): WebauthnKey + { + $this->name = $name; + return $this; + } + + /** + * @return User|null + */ + public function getUser(): ?User + { + return $this->user; + } + + /** + * @param User|null $user + * @return WebauthnKey + */ + public function setUser(?User $user): WebauthnKey + { + $this->user = $user; + return $this; + } + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + + + + + public static function fromRegistration(BasePublicKeyCredentialSource $registration): self + { + return new static( + $registration->getPublicKeyCredentialId(), + $registration->getType(), + $registration->getTransports(), + $registration->getAttestationType(), + $registration->getTrustPath(), + $registration->getAaguid(), + $registration->getCredentialPublicKey(), + $registration->getUserHandle(), + $registration->getCounter(), + $registration->getOtherUI() + ); + } +} \ No newline at end of file diff --git a/symfony.lock b/symfony.lock index d5ff4847..347abebf 100644 --- a/symfony.lock +++ b/symfony.lock @@ -394,6 +394,9 @@ "shivas/versioning-bundle": { "version": "3.1.3" }, + "spomky-labs/cbor-bundle": { + "version": "v2.0.3" + }, "symfony/apache-pack": { "version": "1.0", "recipe": { @@ -814,6 +817,15 @@ "vimeo/psalm": { "version": "3.5.1" }, + "web-auth/webauthn-symfony-bundle": { + "version": "3.3", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.0", + "ref": "9926090a80c2cceeffe96e6c3312b397ea55d4a7" + } + }, "webmozart/assert": { "version": "1.4.0" }, diff --git a/templates/Users/_2fa_settings.html.twig b/templates/Users/_2fa_settings.html.twig index fc621fc7..0131c6c5 100644 --- a/templates/Users/_2fa_settings.html.twig +++ b/templates/Users/_2fa_settings.html.twig @@ -110,7 +110,7 @@

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

- {% if user.u2FKeys is not empty %} + {% if user.legacyU2FKeys is not empty or user.webauthnKeys is not empty %} {% trans %}tfa_u2f.table_caption{% endtrans %}:
- {% for key in user.u2FKeys %} + {% for key in user.legacyU2FKeys %} - {{ loop.index }} + {{ loop.index }} (U2F) {{ key.name }} {{ key.addedDate | format_datetime }} {% endfor %} + {% for key in user.webauthnKeys %} + + {{ loop.index }} (WebAuthn) + {{ key.name }} + {{ key.addedDate | format_datetime }} + + + {% endfor %}
@@ -143,7 +151,7 @@

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

{% endif %} - {% trans %}tfa_u2f.add_new_key{% endtrans %} + {% trans %}tfa_u2f.add_new_key{% endtrans %}
diff --git a/templates/security/Webauthn/u2f_register.html.twig b/templates/security/Webauthn/webauthn_register.html.twig similarity index 100% rename from templates/security/Webauthn/u2f_register.html.twig rename to templates/security/Webauthn/webauthn_register.html.twig