diff --git a/ci/qa/phpstan-baseline.neon b/ci/qa/phpstan-baseline.neon index 70ba5d68..cee9b2a5 100644 --- a/ci/qa/phpstan-baseline.neon +++ b/ci/qa/phpstan-baseline.neon @@ -41,42 +41,37 @@ parameters: path: ../../dev/Controller/SPController.php - - message: "#^Parameter \\#2 \\$allowCredentials of method Webauthn\\\\Bundle\\\\Service\\\\PublicKeyCredentialRequestOptionsFactory\\:\\:create\\(\\) expects array\\, array\\ given\\.$#" + message: "#^Parameter \\#1 \\$publicKeyCredentialUserEntity of method Surfnet\\\\Webauthn\\\\Repository\\\\PublicKeyCredentialSourceRepository\\:\\:findAllForUserEntity\\(\\) expects Webauthn\\\\PublicKeyCredentialUserEntity, Webauthn\\\\PublicKeyCredentialUserEntity\\|null given\\.$#" count: 1 path: ../../src/Controller/AuthenticationController.php - - message: "#^Method Surfnet\\\\Webauthn\\\\Controller\\\\ExceptionController\\:\\:getPageTitleAndDescription\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: ../../src/Controller/ExceptionController.php - - - - message: "#^Property Surfnet\\\\Webauthn\\\\Entity\\\\PublicKeyCredentialSource\\:\\:\\$id is unused\\.$#" + message: "#^Parameter \\#2 \\$allowCredentials of method Webauthn\\\\Bundle\\\\Service\\\\PublicKeyCredentialRequestOptionsFactory\\:\\:create\\(\\) expects array\\, array\\ given\\.$#" count: 1 - path: ../../src/Entity/PublicKeyCredentialSource.php + path: ../../src/Controller/AuthenticationController.php - - message: "#^Class Surfnet\\\\Webauthn\\\\Entity\\\\User has an uninitialized readonly property \\$id\\. Assign it in the constructor\\.$#" + message: "#^Method Surfnet\\\\Webauthn\\\\Controller\\\\ExceptionController\\:\\:getPageTitleAndDescription\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 - path: ../../src/Entity/User.php + path: ../../src/Controller/ExceptionController.php - - message: "#^Class Surfnet\\\\Webauthn\\\\Entity\\\\User has an uninitialized readonly property \\$name\\. Assign it in the constructor\\.$#" + message: "#^Property Surfnet\\\\Webauthn\\\\Entity\\\\PublicKeyCredentialSource\\:\\:\\$fmt is never read, only written\\.$#" count: 1 - path: ../../src/Entity/User.php + path: ../../src/Entity/PublicKeyCredentialSource.php - - message: "#^Method Surfnet\\\\Webauthn\\\\Entity\\\\User\\:\\:getPublicKeyCredentialSources\\(\\) should return array\\ but returns array\\\\.$#" + message: "#^Class Surfnet\\\\Webauthn\\\\Entity\\\\User has an uninitialized readonly property \\$displayName\\. Assign it in the constructor\\.$#" count: 1 path: ../../src/Entity/User.php - - message: "#^Parameter \\$repositoryClass of attribute class Doctrine\\\\ORM\\\\Mapping\\\\Entity constructor expects class\\-string\\\\>\\|null, 'Surfnet\\\\\\\\Webauthn\\\\\\\\Repository\\\\\\\\UserRepository' given\\.$#" + message: "#^Class Surfnet\\\\Webauthn\\\\Entity\\\\User has an uninitialized readonly property \\$id\\. Assign it in the constructor\\.$#" count: 1 path: ../../src/Entity/User.php - - message: "#^Property Surfnet\\\\Webauthn\\\\Entity\\\\User\\:\\:\\$publicKeyCredentialSources with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" + message: "#^Class Surfnet\\\\Webauthn\\\\Entity\\\\User has an uninitialized readonly property \\$name\\. Assign it in the constructor\\.$#" count: 1 path: ../../src/Entity/User.php @@ -100,6 +95,16 @@ parameters: count: 1 path: ../../src/GlobalViewParameters.php + - + message: "#^Parameter \\#1 \\$data of function unserialize expects string, mixed given\\.$#" + count: 2 + path: ../../src/Migrations/Version20250213135649.php + + - + message: "#^Part \\$id \\(mixed\\) of encapsed string cannot be cast to string\\.$#" + count: 1 + path: ../../src/Migrations/Version20250213135649.php + - message: "#^Method Surfnet\\\\Webauthn\\\\PublicKeyCredentialCreationOptionsStore\\:\\:get\\(\\) should return Webauthn\\\\PublicKeyCredentialCreationOptions but returns mixed\\.$#" count: 1 @@ -111,17 +116,17 @@ parameters: path: ../../src/PublicKeyCredentialRequestOptionsStore.php - - message: "#^Method Surfnet\\\\Webauthn\\\\Repository\\\\PublicKeyCredentialSourceRepository\\:\\:allForUser\\(\\) should return array\\ but returns mixed\\.$#" + message: "#^Method Surfnet\\\\Webauthn\\\\Repository\\\\PublicKeyCredentialSourceRepository\\:\\:findAllForUserEntity\\(\\) should return array\\ but returns mixed\\.$#" count: 1 path: ../../src/Repository/PublicKeyCredentialSourceRepository.php - - message: "#^Parameter \\#7 \\$credentialPublicKey of class Surfnet\\\\Webauthn\\\\Entity\\\\PublicKeyCredentialSource constructor expects string, string\\|null given\\.$#" + message: "#^Call to function is_null\\(\\) with Doctrine\\\\ORM\\\\EntityManagerInterface will always evaluate to false\\.$#" count: 1 - path: ../../src/Repository/PublicKeyCredentialSourceRepository.php + path: ../../src/Repository/UserRepository.php - - message: "#^Call to function is_null\\(\\) with Doctrine\\\\ORM\\\\EntityManagerInterface will always evaluate to false\\.$#" + message: "#^Class Surfnet\\\\Webauthn\\\\Repository\\\\UserRepository extends generic class Doctrine\\\\Bundle\\\\DoctrineBundle\\\\Repository\\\\ServiceEntityRepository but does not specify its types\\: T$#" count: 1 path: ../../src/Repository/UserRepository.php @@ -136,10 +141,20 @@ parameters: path: ../../src/Repository/UserRepository.php - - message: "#^Method Surfnet\\\\Webauthn\\\\Repository\\\\UserRepository\\:\\:getByUserId\\(\\) should return Surfnet\\\\Webauthn\\\\Entity\\\\User but returns Surfnet\\\\Webauthn\\\\Entity\\\\User\\|null\\.$#" + message: "#^Parameter \\#1 \\$name of class Surfnet\\\\Webauthn\\\\Entity\\\\User constructor expects string, string\\|null given\\.$#" + count: 1 + path: ../../src/Repository/UserRepository.php + + - + message: "#^Parameter \\#3 \\$displayName of class Surfnet\\\\Webauthn\\\\Entity\\\\User constructor expects string, string\\|null given\\.$#" count: 1 path: ../../src/Repository/UserRepository.php + - + message: "#^Access to an undefined property Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\:\\:\\$id\\.$#" + count: 1 + path: ../../src/Security/AuthenticationListener.php + - message: "#^Method Surfnet\\\\Webauthn\\\\Service\\\\ClientMetadataService\\:\\:generateMetadata\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 diff --git a/composer.json b/composer.json index e71df15c..fc6e0e52 100644 --- a/composer.json +++ b/composer.json @@ -57,9 +57,9 @@ "symfony/yaml": "^6.4", "twig/twig": "^3.8", "web-auth/cose-lib": "^4.4", - "web-auth/webauthn-lib": "^4.9", - "web-auth/webauthn-stimulus": "^4.9", - "web-auth/webauthn-symfony-bundle": "^4.9", + "web-auth/webauthn-lib": "^5.1", + "web-auth/webauthn-stimulus": "^5.1", + "web-auth/webauthn-symfony-bundle": "^5.1", "web-token/jwt-signature-algorithm-rsa": "^3.4" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 5d044ca3..3927489d 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": "049e4ef9ddb55685851600e950709f18", + "content-hash": "31a065716fef803ea297fbf5c3ef1086", "packages": [ { "name": "beberlei/assert", @@ -2041,70 +2041,6 @@ }, "time": "2023-12-09T10:31:14+00:00" }, - { - "name": "lcobucci/clock", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/lcobucci/clock.git", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", - "shasum": "" - }, - "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/clock": "^1.0" - }, - "provide": { - "psr/clock-implementation": "1.0" - }, - "require-dev": { - "infection/infection": "^0.29", - "lcobucci/coding-standard": "^11.1.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.10.25", - "phpstan/phpstan-deprecation-rules": "^1.1.3", - "phpstan/phpstan-phpunit": "^1.3.13", - "phpstan/phpstan-strict-rules": "^1.5.1", - "phpunit/phpunit": "^11.3.6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Lcobucci\\Clock\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Luís Cobucci", - "email": "lcobucci@gmail.com" - } - ], - "description": "Yet another clock abstraction", - "support": { - "issues": "https://github.com/lcobucci/clock/issues", - "source": "https://github.com/lcobucci/clock/tree/3.3.1" - }, - "funding": [ - { - "url": "https://github.com/lcobucci", - "type": "github" - }, - { - "url": "https://www.patreon.com/lcobucci", - "type": "patreon" - } - ], - "time": "2024-09-24T20:45:14+00:00" - }, { "name": "monolog/monolog", "version": "3.8.1", @@ -6871,89 +6807,6 @@ ], "time": "2024-12-26T19:01:29+00:00" }, - { - "name": "symfony/psr-http-message-bridge", - "version": "v6.4.13", - "source": { - "type": "git", - "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "c9cf83326a1074f83a738fc5320945abf7fb7fec" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c9cf83326a1074f83a738fc5320945abf7fb7fec", - "reference": "c9cf83326a1074f83a738fc5320945abf7fb7fec", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/http-message": "^1.0|^2.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0" - }, - "conflict": { - "php-http/discovery": "<1.15", - "symfony/http-kernel": "<6.2" - }, - "require-dev": { - "nyholm/psr7": "^1.1", - "php-http/discovery": "^1.15", - "psr/log": "^1.1.4|^2|^3", - "symfony/browser-kit": "^5.4|^6.0|^7.0", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", - "symfony/framework-bundle": "^6.2|^7.0", - "symfony/http-kernel": "^6.2|^7.0" - }, - "type": "symfony-bridge", - "autoload": { - "psr-4": { - "Symfony\\Bridge\\PsrHttpMessage\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "PSR HTTP message bridge", - "homepage": "https://symfony.com", - "keywords": [ - "http", - "http-message", - "psr-17", - "psr-7" - ], - "support": { - "source": "https://github.com/symfony/psr-http-message-bridge/tree/v6.4.13" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:18:03+00:00" - }, { "name": "symfony/routing", "version": "v7.2.3", @@ -8936,44 +8789,40 @@ }, { "name": "web-auth/webauthn-lib", - "version": "4.9.2", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-lib.git", - "reference": "008b25171c27cf4813420d0de31cc059bcc71f1a" + "reference": "7aa58ea290c421066d068b031f3f653dab20430c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/008b25171c27cf4813420d0de31cc059bcc71f1a", - "reference": "008b25171c27cf4813420d0de31cc059bcc71f1a", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/7aa58ea290c421066d068b031f3f653dab20430c", + "reference": "7aa58ea290c421066d068b031f3f653dab20430c", "shasum": "" }, "require": { "ext-json": "*", - "ext-mbstring": "*", "ext-openssl": "*", - "lcobucci/clock": "^2.2|^3.0", "paragonie/constant_time_encoding": "^2.6|^3.0", - "php": ">=8.1", + "php": ">=8.2", + "phpdocumentor/reflection-docblock": "^5.3", "psr/clock": "^1.0", "psr/event-dispatcher": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", "psr/log": "^1.0|^2.0|^3.0", "spomky-labs/cbor-php": "^3.0", "spomky-labs/pki-framework": "^1.0", + "symfony/clock": "^6.4|^7.0", "symfony/deprecation-contracts": "^3.2", - "symfony/uid": "^6.1|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", "web-auth/cose-lib": "^4.2.3" }, "suggest": { - "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", - "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", "psr/log-implementation": "Recommended to receive logs from the library", "symfony/event-dispatcher": "Recommended to use dispatched events", - "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", - "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", - "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" }, "type": "library", @@ -9010,7 +8859,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-lib/tree/4.9.2" + "source": "https://github.com/web-auth/webauthn-lib/tree/5.1.2" }, "funding": [ { @@ -9022,20 +8871,20 @@ "type": "patreon" } ], - "time": "2025-01-04T09:47:58+00:00" + "time": "2025-02-16T10:15:04+00:00" }, { "name": "web-auth/webauthn-stimulus", - "version": "4.9.2", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/web-auth/ux.git", - "reference": "32b40b8a00298fad8fe2a1adbfe6ce240fbd94b1" + "reference": "2c8f8d33f7675b7915f2d55b733cf7ef219c737b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/ux/zipball/32b40b8a00298fad8fe2a1adbfe6ce240fbd94b1", - "reference": "32b40b8a00298fad8fe2a1adbfe6ce240fbd94b1", + "url": "https://api.github.com/repos/web-auth/ux/zipball/2c8f8d33f7675b7915f2d55b733cf7ef219c737b", + "reference": "2c8f8d33f7675b7915f2d55b733cf7ef219c737b", "shasum": "" }, "conflict": { @@ -9077,7 +8926,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/ux/tree/4.9.2" + "source": "https://github.com/web-auth/ux/tree/5.1.2" }, "funding": [ { @@ -9089,41 +8938,38 @@ "type": "patreon" } ], - "time": "2024-06-30T09:37:10+00:00" + "time": "2025-02-01T21:57:01+00:00" }, { "name": "web-auth/webauthn-symfony-bundle", - "version": "4.9.2", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/web-auth/webauthn-symfony-bundle.git", - "reference": "80aa16fa6f16ab8f017a4108ffcd2ecc12264c07" + "reference": "20582697d824b7fc4cef3fc7cb51d8ec36bc205e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/80aa16fa6f16ab8f017a4108ffcd2ecc12264c07", - "reference": "80aa16fa6f16ab8f017a4108ffcd2ecc12264c07", + "url": "https://api.github.com/repos/web-auth/webauthn-symfony-bundle/zipball/20582697d824b7fc4cef3fc7cb51d8ec36bc205e", + "reference": "20582697d824b7fc4cef3fc7cb51d8ec36bc205e", "shasum": "" }, "require": { - "nyholm/psr7": "^1.5", - "php": ">=8.1", - "phpdocumentor/reflection-docblock": "^5.3", + "php": ">=8.2", "psr/event-dispatcher": "^1.0", - "symfony/config": "^6.1|^7.0", - "symfony/dependency-injection": "^6.1|^7.0", - "symfony/framework-bundle": "^6.1|^7.0", - "symfony/http-client": "^6.1|^7.0", - "symfony/property-access": "^6.1|^7.0", - "symfony/property-info": "^6.1|^7.0", - "symfony/psr-http-message-bridge": "^2.1|^6.1|^7.0", - "symfony/security-bundle": "^6.1|^7.0", - "symfony/security-core": "^6.1|^7.0", - "symfony/security-http": "^6.1|^7.0", - "symfony/serializer": "^6.1|^7.0", - "symfony/validator": "^6.1|^7.0", - "web-auth/webauthn-lib": "self.version", - "web-token/jwt-library": "^3.3|^4.0" + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/security-bundle": "^6.4|^7.0", + "symfony/security-core": "^6.4|^7.0", + "symfony/security-http": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "web-auth/webauthn-lib": "self.version" + }, + "suggest": { + "symfony/security-bundle": "Symfony firewall using a JSON API (perfect for script applications)" }, "type": "symfony-bundle", "extra": { @@ -9162,7 +9008,7 @@ "webauthn" ], "support": { - "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/4.9.2" + "source": "https://github.com/web-auth/webauthn-symfony-bundle/tree/5.1.2" }, "funding": [ { @@ -9174,7 +9020,7 @@ "type": "patreon" } ], - "time": "2025-01-04T09:38:56+00:00" + "time": "2025-02-16T10:09:45+00:00" }, { "name": "web-token/jwt-library", diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index d194e126..e2e4e30f 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -33,7 +33,7 @@ when@dev: when@test: framework: - test: ~ + test: true session: storage_factory_id: session.storage.factory.mock_file profiler: diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0c2d2091..6f0f1708 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,13 +1,18 @@ security: # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers providers: - in_memory: { memory: null } + users: + entity: + class: 'Surfnet\Webauthn\Entity\User' + property: 'name' firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: - provider: in_memory + webauthn: + registration: + enabled: true monitor: pattern: ^/(internal/)?(info|health)$ security: false @@ -21,4 +26,4 @@ security: # Note: Only the *first* access control that matches will be used access_control: # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } \ No newline at end of file + # - { path: ^/profile, roles: ROLE_USER } diff --git a/config/routes/webauthn_routes.yaml b/config/routes/webauthn_routes.yaml new file mode 100644 index 00000000..fa3e8e9a --- /dev/null +++ b/config/routes/webauthn_routes.yaml @@ -0,0 +1,3 @@ +webauthn_routes: + resource: . + type: webauthn diff --git a/package.json b/package.json index 22871d73..39eebd3c 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,21 @@ { "browserslist": "last 2 versions, ie 11, > 1%", "devDependencies": { - "@babel/core": "^7.26.9", - "@babel/preset-env": "^7.26.9", + "@babel/core": "^7.20.5", + "@babel/preset-env": "^7.20.2", "@hotwired/stimulus": "^3.2.2", - "@simplewebauthn/browser": "^10.0.0", - "@symfony/stimulus-bridge": "^3.2.3", + "@simplewebauthn/browser": "^13.0.0", + "@symfony/stimulus-bridge": "^3.2.2", "@symfony/webpack-encore": "^4", "@types/query-string": "^6.3.0", "@web-auth/webauthn-stimulus": "file:vendor/web-auth/webauthn-stimulus/assets", "compass-mixins": "^0", "file-loader": "^6.0.0", "query-string": "^6.8.3", - "sass": "^1.85", + "sass": "^1.43", "sass-loader": "^13.0", "stylelint": "^15.10", - "webpack": "^5.98.0", + "webpack": "^5.94.0", "webpack-cli": "^5.0.0", "webpack-dev-server": "^4.11", "webpack-import-glob-loader": "^1.6.3" @@ -26,7 +26,7 @@ "css-loader": "^6", "glob-parent": ">=5.1.2", "jquery": "^3", - "postcss": ">=8.5.3", + "postcss": ">=8.4.31", "select2": "^4", "semver": ">7.5.2", "serialize-javascript": ">=2.1.1", diff --git a/src/Controller/AssertionResponseController.php b/src/Controller/AssertionResponseController.php deleted file mode 100644 index 0e7f2c1a..00000000 --- a/src/Controller/AssertionResponseController.php +++ /dev/null @@ -1,125 +0,0 @@ -logger->info('Verifying if there is a pending authentication from SP'); - - if (!$this->authenticationService->authenticationRequired()) { - $this->logger->warning('No authentication required'); - return ValidationJsonResponse::noAuthenticationRequired(new NoActiveAuthenrequestException()); - } - $nameId = $this->authenticationService->getNameId(); - $logger = WithContextLogger::from($this->logger, ['nameId' => $nameId]); - - $logger->info('Verify valid public key credential response'); - - try { - $content = $request->getContent(); - $publicKeyCredential = $this->publicKeyCredentialLoader->load($content); - $response = $publicKeyCredential->response; - if (!$response instanceof AuthenticatorAssertionResponse) { - throw new UnrecoverableErrorException('Invalid response type'); - } - } catch (Exception $exception) { - $logger->warning(sprintf('Invalid public key credential response "%s"', $exception->getMessage())); - return ValidationJsonResponse::reportErrorMessage($exception); - } - - $logger->info('Verify if there is an existing public key credential assertion options in session'); - try { - $publicKeyCredentialRequestOptions = $this->store->get(); - $allowedCredentials = $publicKeyCredentialRequestOptions->allowCredentials; - if (count($allowedCredentials) !== 1) { - $logger->error('One credential source allowed'); - throw new UnrecoverableErrorException('More than one publicKeyCredentialSource found in store'); - } - /** @var PublicKeyCredentialSource $publicKeyCredentialSource */ - $publicKeyCredentialSource = reset($allowedCredentials); - } catch (Exception $exception) { - $logger->warning(sprintf('Invalid attestation response "%s"', $exception->getMessage())); - return ValidationJsonResponse::reportErrorMessage($exception); - } - - $logger->info('Validate assertion response'); - - $psr17Factory = new Psr17Factory(); - $psrHttpFactory = new PsrHttpFactory($psr17Factory); - $psr7Request = $psrHttpFactory->createRequest($request); - - try { - $this->assertionResponseValidator->check( - $publicKeyCredentialSource, - $response, - $publicKeyCredentialRequestOptions, - $psr7Request, - $nameId - ); - } catch (Exception $throwable) { - $logger->warning(sprintf('Invalid attestation "%s"', $throwable->getMessage())); - return ValidationJsonResponse::invalid($throwable); - } - - $logger->info('Attestation success, user verified'); - - $this->authenticationService->authenticate(); - $this->store->clear(); - - return ValidationJsonResponse::valid(); - } -} diff --git a/src/Controller/AttestationRequestController.php b/src/Controller/AttestationRequestController.php deleted file mode 100644 index a14dd1ea..00000000 --- a/src/Controller/AttestationRequestController.php +++ /dev/null @@ -1,61 +0,0 @@ -creationOptionsStore->get(); - $userEntity = $publicKeyCredentialCreationOptions->user; - - try { - $response = $this->creationOptionsHandler->onCreationOptions( - $publicKeyCredentialCreationOptions, - $userEntity - ); - } catch (RuntimeException $e) { - $this->logger->warning(sprintf('Unable to create the attestation options: "%s"', $e->getMessage())); - return ValidationJsonResponse::reportErrorMessage($e); - } - - return $response; - } -} diff --git a/src/Controller/AttestationResponseController.php b/src/Controller/AttestationResponseController.php deleted file mode 100755 index 60565e65..00000000 --- a/src/Controller/AttestationResponseController.php +++ /dev/null @@ -1,130 +0,0 @@ -logger->info('Verifying if there is a pending registration from SP'); - - if (!$this->registrationService->registrationRequired()) { - $this->logger->warning('Registration is not required'); - return ValidationJsonResponse::noRegistrationRequired(new NoActiveAuthenrequestException()); - } - - $this->logger->info('Verify valid public key credential response'); - - - try { - $publicKeyCredential = $this->publicKeyCredentialLoader->load($request->getContent()); - $response = $publicKeyCredential->response; - if (!$response instanceof AuthenticatorAttestationResponse) { - throw new UnrecoverableErrorException('Invalid response type'); - } - } catch (Exception $e) { - $this->logger->warning(sprintf('Invalid public key credential response "%s"', $e->getMessage())); - return ValidationJsonResponse::reportErrorMessage($e); - } - - $this->logger->info('Verify if there is an existing public key credential creation options in session'); - - try { - $publicKeyCredentialCreationOptions = $this->store->get(); - } catch (Exception $e) { - $this->logger->warning('No pending public key credential creation options in session'); - return ValidationJsonResponse::reportErrorMessage($e); - } - - $nameId = $publicKeyCredentialCreationOptions->user->id; - $logger = WithContextLogger::from($this->logger, ['nameId' => $nameId]); - - $logger->info('Validate attestation response'); - - $psr17Factory = new Psr17Factory(); - $psrHttpFactory = new PsrHttpFactory($psr17Factory); - $psr7Request = $psrHttpFactory->createRequest($request); - try { - $pkco = $this->attestationResponseValidator->check($response, $publicKeyCredentialCreationOptions, $psr7Request); - $this->mds->verifyMeetsRequiredAuthenticatorStatus($pkco); - } catch (Exception $e) { - $logger->warning(sprintf('Invalid attestation "%s"', $e->getMessage())); - return ValidationJsonResponse::invalid($e); - } - - $credentialSource = $this->credentialSourceRepository->create( - $publicKeyCredential, - $nameId - ); - - $logger->info('Saving user'); - - $this->userRegistrationRepository->saveUserEntity($publicKeyCredentialCreationOptions->user); - $this->credentialSourceRepository->saveCredentialSource($credentialSource); - - $logger->info('Register user'); - - $this->registrationService->register($nameId); - $this->store->clear(); - - $logger->info('Attestation verify success, user registered'); - return ValidationJsonResponse::valid(); - } -} diff --git a/src/Controller/AuthenticationController.php b/src/Controller/AuthenticationController.php index 19f8abef..dfecc245 100644 --- a/src/Controller/AuthenticationController.php +++ b/src/Controller/AuthenticationController.php @@ -74,7 +74,7 @@ public function __invoke(Request $request): Response } try { - $user = $this->userRepository->getByUserId($nameId); + $user = $this->userRepository->findOneByUserHandle($nameId); } catch (Throwable $exception) { $logger->error(sprintf( 'User with nameId "%s" not found, error "%s"', @@ -86,7 +86,7 @@ public function __invoke(Request $request): Response $this->logger->info('Registration is not finalized create public key credential creation options'); - $allowedCredentials = $this->publicKeyCredentialSourceRepository->allForUser($user); + $allowedCredentials = $this->publicKeyCredentialSourceRepository->findAllForUserEntity($user); if (count($allowedCredentials) !== 1) { $logger->error('One credential source allowed'); diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 5883cd20..35797568 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -21,7 +21,6 @@ namespace Surfnet\Webauthn\Controller; use Surfnet\Webauthn\Exception\NoAuthnrequestException; -use Surfnet\Webauthn\PublicKeyCredentialCreationOptionsStore; use Surfnet\Webauthn\Repository\UserRepository; use Surfnet\Webauthn\Service\ClientMetadataService; use Psr\Log\LoggerInterface; @@ -29,7 +28,6 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; -use Webauthn\Bundle\Service\PublicKeyCredentialCreationOptionsFactory; use Symfony\Component\HttpFoundation\Request; final class RegistrationController extends AbstractController @@ -37,8 +35,6 @@ final class RegistrationController extends AbstractController public function __construct( private readonly RegistrationService $registrationService, private readonly UserRepository $userRepository, - private readonly PublicKeyCredentialCreationOptionsFactory $publicKeyCredentialCreationOptionsFactory, - private readonly PublicKeyCredentialCreationOptionsStore $creationOptionsStore, private readonly LoggerInterface $logger, private readonly ClientMetadataService $clientMetadataService, private readonly string $userDisplayName @@ -64,21 +60,13 @@ public function __invoke(Request $request): Response return $this->registrationService->replyToServiceProvider(); } - $this->logger->info('Registration is not finalized. Create public key credential creation options'); - - $userEntity = $this->userRepository->createUser($this->userDisplayName); - $publicKeyCredentialCreationOptions = $this->publicKeyCredentialCreationOptionsFactory->create( - 'default', - $userEntity - ); - $this->creationOptionsStore->set($publicKeyCredentialCreationOptions); - $this->logger->info('Return registration page for user attestation'); return $this->render( 'default\registration.html.twig', [ - 'userEntity' => $userEntity + 'name' => $this->userRepository->generateUserName(), + 'displayName' => $this->userDisplayName, ] + $this->clientMetadataService->generateMetadata($request) ); } diff --git a/src/Controller/RequestOptionsController.php b/src/Controller/RequestOptionsController.php deleted file mode 100644 index 95699f6d..00000000 --- a/src/Controller/RequestOptionsController.php +++ /dev/null @@ -1,89 +0,0 @@ -getContent(), true); - if (!is_array($requestContent) || !array_key_exists('username', $requestContent)) { - throw new UserNotFoundException( - 'The user was not found in the request json content. It should be stored in the username field.' - ); - } - try { - $nameId = $requestContent['username']; - $user = $this->userRepository->getByUserId($nameId); - } catch (Throwable $exception) { - $this->logger->error(sprintf( - 'User with nameId "%s" not found, error "%s"', - $nameId, - $exception->getMessage() - )); - throw new UserNotFoundException(); - } - $publicKeyCredentialRequestOptions = $this->requestOptionsStore->get(); - - try { - $response = $this->requestOptionsHandler->onRequestOptions( - $publicKeyCredentialRequestOptions, - $user - ); - } catch (RuntimeException $e) { - $this->logger->warning(sprintf('Unable to create the authentication options: "%s"', $e->getMessage())); - return ValidationJsonResponse::reportErrorMessage($e); - } - - return $response; - } -} diff --git a/src/Entity/PublicKeyCredentialSource.php b/src/Entity/PublicKeyCredentialSource.php index 0a3e1dbd..a1d9b252 100644 --- a/src/Entity/PublicKeyCredentialSource.php +++ b/src/Entity/PublicKeyCredentialSource.php @@ -21,7 +21,6 @@ namespace Surfnet\Webauthn\Entity; use Doctrine\ORM\Mapping as ORM; -use Surfnet\Webauthn\Exception\RuntimeException; use Surfnet\Webauthn\Repository\PublicKeyCredentialSourceRepository; use Symfony\Component\Uid\Uuid; use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource; @@ -35,10 +34,10 @@ #[ORM\Entity(repositoryClass: PublicKeyCredentialSourceRepository::class)] class PublicKeyCredentialSource extends BasePublicKeyCredentialSource { - #[ORM\Id] - #[ORM\GeneratedValue] - #[ORM\Column(type:"integer")] - private int $id; + #[ORM\Id] + #[ORM\Column(type:"string", length:36, unique: true)] + #[ORM\GeneratedValue(strategy: "NONE")] + private string $id; /** * Override the $backupEligible, $backupStatus and $uvInitialized fields which we do not use, but needs @@ -61,6 +60,7 @@ public function __construct( #[ORM\Column(type: "string")] private string $fmt ) { + $this->id = Uuid::v4()->toRfc4122(); parent::__construct( $publicKeyCredentialId, $type, @@ -74,53 +74,8 @@ public function __construct( ); } - /** - * Warning: the id field is accessed directly in :\Webauthn\CeremonyStep\CheckAllowedCredentialList::process - * The entity tracks a numeric auto increment id value, but the CheckAllowedCredentialList expects the - * publicKeyCredentialId. - */ - public function __get(string $name): mixed - { - if ($name === 'id') { - return $this->publicKeyCredentialId; - } - throw new RuntimeException(sprintf('Not allowed to access "%s" via the magic __get function', $name)); - } - - public function getFmt(): string - { - return $this->fmt; - } - - /** - * This should be fixed in WebAuthn framework, mapping was incorrect. - */ - public function jsonSerialize(): array - { - return [ - 'id' => $this->base64UrlEncode($this->publicKeyCredentialId), - 'type' => $this->type, - 'transports' => $this->transports, - 'attestationType' => $this->attestationType, - 'trustPath' => $this->trustPath, - 'aaguid' => $this->aaguid->toBase32(), - 'credentialPublicKey' => $this->base64UrlEncode($this->credentialPublicKey), - 'userHandle' => $this->base64UrlEncode($this->userHandle), - 'counter' => $this->counter, - ]; - } - - /** - * Encode data to Base64URL - * From: https://base64.guru/developers/php/examples/base64url - */ - private function base64UrlEncode(string $data): string + public function getId(): string { - // First of all you should encode $data to Base64 string - $b64 = base64_encode($data); - // Convert Base64 to Base64URL by replacing “+” with “-” and “/” with “_” - $url = strtr($b64, '+/', '-_'); - // Remove padding character from the end of line and return the Base64URL result - return rtrim($url, '='); + return $this->id; } } diff --git a/src/Entity/User.php b/src/Entity/User.php index e46872dd..75b0116a 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -36,7 +36,8 @@ class User extends PublicKeyCredentialUserEntity implements UserInterface { #[ORM\Id] - #[ORM\Column(type:"string", length:36)] + #[ORM\Column(type:"string", length:36, unique: true)] + #[ORM\GeneratedValue(strategy: "NONE")] public readonly string $id; #[Assert\Length(max: 100)] @@ -45,23 +46,6 @@ class User extends PublicKeyCredentialUserEntity implements UserInterface #[Assert\Length(max: 100)] public readonly string $displayName; - /** - * @var Collection - */ - #[ORM\ManyToMany(targetEntity: PublicKeyCredentialSourceEntity::class)] - #[ORM\JoinTable( - name: "users_user_handles", - joinColumns:[new JoinColumn(name: "user_id", referencedColumnName: "id")], - inverseJoinColumns:[new JoinColumn(name:"user_handle", referencedColumnName: "id", unique: true)] - )] - protected Collection $publicKeyCredentialSources; - - public function __construct(string $id, string $name, string $displayName) - { - parent::__construct($name, $id, $displayName); - $this->publicKeyCredentialSources = new ArrayCollection(); - } - /** * WebAuthn project does not care about roles of any user. */ @@ -74,26 +58,10 @@ public function getPassword(): void { } - public function getSalt(): void - { - } - - public function getUsername(): ?string - { - return $this->name; - } - public function eraseCredentials(): void { } - /** - * @return PublicKeyCredentialSource[] - */ - public function getPublicKeyCredentialSources(): array - { - return $this->publicKeyCredentialSources->getValues(); - } public function getUserIdentifier(): string { diff --git a/src/Migrations/Version20250213135649.php b/src/Migrations/Version20250213135649.php new file mode 100644 index 00000000..470d0870 --- /dev/null +++ b/src/Migrations/Version20250213135649.php @@ -0,0 +1,97 @@ +addSql('# Updating php serialized fields to json fields.'); + + $result = $this->connection->executeQuery(self::$select); + + $rows = $result->fetchAllAssociative(); + $this->write("Records to migrate: {$result->rowCount()}"); + + if ($result->rowCount() === 0) { + return; + } + + foreach ($rows as $row) { + $id = $row['id']; + $transports = $row['transports']; + $otherUi = $row['other_ui']; + + $this->write("Migating: {$id}"); + + $transports = json_encode(unserialize($transports), JSON_THROW_ON_ERROR); + $otherUi = json_encode(unserialize($otherUi), JSON_THROW_ON_ERROR); + + $this->connection->executeUpdate( + self::$update, + [ + 'id' => $id, + 'transports' => $transports, + 'other_ui' => $otherUi, + ], + ); + } + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE users_user_handles DROP FOREIGN KEY FK_EFD91D5DF4D23BE4'); + $this->addSql('ALTER TABLE users_user_handles DROP FOREIGN KEY FK_EFD91D5DA76ED395'); + $this->addSql('DROP TABLE users_user_handles'); + $this->addSql('ALTER TABLE public_key_credential_sources CHANGE id id VARCHAR(36) NOT NULL, CHANGE transports transports JSON NOT NULL COMMENT \'(DC2Type:json)\', CHANGE other_ui other_ui JSON DEFAULT NULL COMMENT \'(DC2Type:json)\''); + } + + public function down(Schema $schema): void + { + $this->addSql('CREATE TABLE users_user_handles (user_id VARCHAR(36) CHARACTER SET utf8mb4 NOT NULL COLLATE `utf8mb4_unicode_ci`, user_handle INT NOT NULL, UNIQUE INDEX UNIQ_EFD91D5DF4D23BE4 (user_handle), INDEX IDX_EFD91D5DA76ED395 (user_id), PRIMARY KEY(user_id, user_handle)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB COMMENT = \'\' '); + $this->addSql('ALTER TABLE users_user_handles ADD CONSTRAINT FK_EFD91D5DF4D23BE4 FOREIGN KEY (user_handle) REFERENCES public_key_credential_sources (id)'); + $this->addSql('ALTER TABLE users_user_handles ADD CONSTRAINT FK_EFD91D5DA76ED395 FOREIGN KEY (user_id) REFERENCES users (id)'); + $this->addSql('ALTER TABLE public_key_credential_sources CHANGE id id INT AUTO_INCREMENT NOT NULL, CHANGE transports transports LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', CHANGE other_ui other_ui LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:array)\''); + } +} diff --git a/src/Repository/MetadataStatementRepository.php b/src/Repository/MetadataStatementRepository.php index 42033cf2..ddd4dad5 100644 --- a/src/Repository/MetadataStatementRepository.php +++ b/src/Repository/MetadataStatementRepository.php @@ -27,11 +27,13 @@ use Jose\Component\Signature\JWSVerifier; use Jose\Component\Signature\Serializer\CompactSerializer; use Surfnet\Webauthn\Exception\RuntimeException; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\SerializerInterface; +use Webauthn\Exception\MetadataStatementLoadingException; +use Webauthn\Exception\MissingMetadataStatementException; use Webauthn\MetadataService\CertificateChain\CertificateChainValidator; use Webauthn\MetadataService\CertificateChain\CertificateToolbox; -use Webauthn\MetadataService\Exception\MetadataStatementLoadingException; -use Webauthn\MetadataService\Exception\MissingMetadataStatementException; -use Webauthn\MetadataService\Service\MetadataBLOBPayloadEntry; +use Webauthn\MetadataService\Service\MetadataBLOBPayload; use Webauthn\MetadataService\Statement\MetadataStatement; use Webauthn\MetadataService\Statement\StatusReport; @@ -68,18 +70,12 @@ public function __construct( private readonly string $jwtMdsRootCertFileName, private readonly string $mdsCacheDir, private readonly CertificateChainValidator $certificateChainValidator, + private readonly SerializerInterface $serializer, ) { $payload = $this->warmCache(); - $data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); - - if (!is_array($data)) { - throw new RuntimeException('Unable to read the contents from the JWT metadata statement service file'); - } - - /** @var array $datum */ - foreach ($data['entries'] as $datum) { - $entry = MetadataBLOBPayloadEntry::createFromArray($datum); + $blob = $this->serializer->deserialize($payload, MetadataBLOBPayload::class, JsonEncoder::FORMAT); + foreach ($blob->entries as $entry) { $mds = $entry->metadataStatement; if ($mds !== null && $entry->aaguid !== null) { $this->statements[$entry->aaguid] = $mds; diff --git a/src/Repository/PublicKeyCredentialSourceRepository.php b/src/Repository/PublicKeyCredentialSourceRepository.php index 6a7a8964..3b9f5f80 100644 --- a/src/Repository/PublicKeyCredentialSourceRepository.php +++ b/src/Repository/PublicKeyCredentialSourceRepository.php @@ -20,15 +20,11 @@ namespace Surfnet\Webauthn\Repository; -use Surfnet\Webauthn\Entity\PublicKeyCredentialSource; -use Surfnet\Webauthn\Entity\User; -use Assert\Assertion; use Doctrine\Persistence\ManagerRegistry; -use Webauthn\AttestationStatement\AttestationObject; -use Webauthn\AuthenticatorAttestationResponse; +use Surfnet\Webauthn\Entity\PublicKeyCredentialSource; use Webauthn\Bundle\Repository\DoctrineCredentialSourceRepository; -use Webauthn\PublicKeyCredential; -use Webauthn\PublicKeyCredentialDescriptor; +use Webauthn\PublicKeyCredentialSource as WebauthnPublicKeyCredentialSource; +use Webauthn\PublicKeyCredentialUserEntity; /** * @extends DoctrineCredentialSourceRepository @@ -40,50 +36,33 @@ public function __construct(ManagerRegistry $registry) parent::__construct($registry, PublicKeyCredentialSource::class); } - public function create(PublicKeyCredential $publicKeyCredential, string $userHandle): PublicKeyCredentialSource + public function saveCredentialSource(WebauthnPublicKeyCredentialSource $publicKeyCredentialSource): void { - $response = $publicKeyCredential->getResponse(); - Assertion::isInstanceOf( - $response, - AuthenticatorAttestationResponse::class, - 'This method is only available with public key credential containing an authenticator attestation response.' - ); - $publicKeyCredentialDescriptor = $publicKeyCredential->getPublicKeyCredentialDescriptor([ - PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_INTERNAL, - PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB, - PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE, - PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC - ]); - /** @var AttestationObject $attestationObject */ - $attestationObject = $response->attestationObject; - $attestationStatement = $attestationObject->attStmt; - $authenticatorData = $attestationObject->authData; - $attestedCredentialData = $authenticatorData->attestedCredentialData; - Assertion::notNull($attestedCredentialData, 'No attested credential data available'); - return new PublicKeyCredentialSource( - $publicKeyCredentialDescriptor->id, - $publicKeyCredentialDescriptor->type, - $publicKeyCredentialDescriptor->transports, - $attestationStatement->type, - $attestationStatement->trustPath, - $attestedCredentialData->aaguid, - $attestedCredentialData->credentialPublicKey, - $userHandle, - $authenticatorData->signCount, - $attestationStatement->fmt - ); + if (!$publicKeyCredentialSource instanceof PublicKeyCredentialSource) { + $publicKeyCredentialSource = new PublicKeyCredentialSource( + $publicKeyCredentialSource->publicKeyCredentialId, + $publicKeyCredentialSource->type, + $publicKeyCredentialSource->transports, + $publicKeyCredentialSource->attestationType, + $publicKeyCredentialSource->trustPath, + $publicKeyCredentialSource->aaguid, + $publicKeyCredentialSource->credentialPublicKey, + $publicKeyCredentialSource->userHandle, + $publicKeyCredentialSource->counter, + 'fmt', + ); + } + parent::saveCredentialSource($publicKeyCredentialSource); } - /** - * @return PublicKeyCredentialSource[] - */ - public function allForUser(User $user): array + public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array { - $qb = $this->getEntityManager()->createQueryBuilder(); - return $qb->select('c') + return $this->getEntityManager() + ->createQueryBuilder() ->from($this->class, 'c') - ->where('c.userHandle = :user_handle') - ->setParameter(':user_handle', $user->id) + ->select('c') + ->where('c.userHandle = :userHandle') + ->setParameter(':userHandle', $publicKeyCredentialUserEntity->id) ->getQuery() ->execute(); } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 14d66b93..a8c14edd 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -22,15 +22,17 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Ramsey\Uuid\Uuid; use Surfnet\Webauthn\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface; use LogicException; +use Webauthn\Bundle\Repository\CanGenerateUserEntity; use Webauthn\Bundle\Repository\CanRegisterUserEntity; use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface; use Webauthn\PublicKeyCredentialUserEntity; -final readonly class UserRepository implements ServiceEntityRepositoryInterface, CanRegisterUserEntity, PublicKeyCredentialUserEntityRepositoryInterface +final class UserRepository extends ServiceEntityRepository implements ServiceEntityRepositoryInterface, CanGenerateUserEntity, PublicKeyCredentialUserEntityRepositoryInterface, CanRegisterUserEntity { private EntityManagerInterface $manager; @@ -46,6 +48,8 @@ public function __construct(ManagerRegistry $registry) )); } $this->manager = $manager; + + parent::__construct($registry, User::class); } public function save(User $user): void @@ -54,23 +58,17 @@ public function save(User $user): void $this->manager->flush(); } - public function getByUserId(string $id): User - { - return $this->manager->find(User::class, $id); - } - public function findOneByUsername(string $username): ?PublicKeyCredentialUserEntity { $qb = $this->manager->createQueryBuilder(); return $qb->select('u') ->from(User::class, 'u') - ->where('u.name = :name') - ->setParameter(':name', $username) + ->where('u.id = :id') + ->setParameter(':id', $username) ->setMaxResults(1) ->getQuery() - ->getOneOrNullResult() - ; + ->getOneOrNullResult(); } public function findOneByUserHandle(string $userHandle): ?PublicKeyCredentialUserEntity @@ -79,33 +77,26 @@ public function findOneByUserHandle(string $userHandle): ?PublicKeyCredentialUse return $qb->select('u') ->from(User::class, 'u') - ->where('u.user_handle = :user_handle') - ->setParameter(':user_handle', $userHandle) + ->where('u.id = :id') + ->setParameter(':id', $userHandle) ->setMaxResults(1) ->getQuery() - ->getOneOrNullResult() - ; + ->getOneOrNullResult(); } - public function createUser(string $displayName) : PublicKeyCredentialUserEntity + public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void { - $id = Uuid::uuid4()->toString(); - return new User($id, $id, $displayName); + $this->manager->persist($userEntity); + $this->manager->flush(); } - public function createUserEntity(string $username, string $displayName, ?string $icon) : PublicKeyCredentialUserEntity + public function generateUserEntity(?string $username, ?string $displayName): PublicKeyCredentialUserEntity { $id = Uuid::uuid4()->toString(); - return new User($id, $id, $displayName); - } - - public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity) : void - { - $this->manager->persist($userEntity); - $this->manager->flush(); + return new User($username, $id, $displayName); } - public function generateNextUserEntityId(): string + public function generateUserName(): string { return Uuid::uuid4()->toString(); } diff --git a/src/Security/AuthenticationListener.php b/src/Security/AuthenticationListener.php new file mode 100644 index 00000000..4ccedb53 --- /dev/null +++ b/src/Security/AuthenticationListener.php @@ -0,0 +1,69 @@ +getRequest(); + $routeName = $request->attributes->get('_route'); + $nameId = $event->getUser()->id; + $logger = WithContextLogger::from($this->logger, ['nameId' => $nameId]); + + if ($routeName == 'webauthn.controller.security.main.creation.result') { + $logger->info('Attestation verify success, user registered'); + + $this->registrationService->register($nameId); + $this->store->clear(); + return; + } + + if ($routeName == 'webauthn.controller.security.main.request.result') { + $logger->info('Assertion success, user verified'); + + $this->authenticationService->authenticate(); + $this->store->clear(); + return; + } + + throw new RuntimeException("Authentication listener encountered unexpected route"); + } +} diff --git a/src/Service/AuthenticatorStatusValidator.php b/src/Service/AuthenticatorStatusValidator.php deleted file mode 100644 index 26660726..00000000 --- a/src/Service/AuthenticatorStatusValidator.php +++ /dev/null @@ -1,102 +0,0 @@ -allowedStatus = [ - AuthenticatorStatus::FIDO_CERTIFIED, - AuthenticatorStatus::FIDO_CERTIFIED_L1, - AuthenticatorStatus::FIDO_CERTIFIED_L2, - AuthenticatorStatus::FIDO_CERTIFIED_L3, - AuthenticatorStatus::FIDO_CERTIFIED_L4, - AuthenticatorStatus::FIDO_CERTIFIED_L5, - AuthenticatorStatus::FIDO_CERTIFIED_L1plus, - AuthenticatorStatus::FIDO_CERTIFIED_L2plus, - AuthenticatorStatus::FIDO_CERTIFIED_L3plus, - ]; - $this->deniedStatus = [ - AuthenticatorStatus::REVOKED, - AuthenticatorStatus::ATTESTATION_KEY_COMPROMISE, - AuthenticatorStatus::USER_KEY_PHYSICAL_COMPROMISE, - AuthenticatorStatus::USER_KEY_REMOTE_COMPROMISE, - AuthenticatorStatus::USER_VERIFICATION_BYPASS - ]; - } - - /** - * One of the status reports must meet one of the allowed statuses. - * - * @param array $statusReports - * @throws AuthenticatorStatusNotSupportedException - */ - public function validate(array $statusReports): void - { - $meetsRequirement = false; - $reportsProcessed = 0; - $reportLog = []; - /* The status of the attestation can be multivalued, containing both a certification as a revocation. - First test for valid certification, then for reasons to deny - */ - foreach ($statusReports as $report) { - if (in_array($report->status, $this->allowedStatus)) { - $meetsRequirement = true; - } - $reportsProcessed++; - $reportLog[] = $report->status; - } - if ($meetsRequirement) { - foreach ($statusReports as $report) { - if (in_array($report->status, $this->deniedStatus)) { - $meetsRequirement = false; - } - } - } - - if (!$meetsRequirement) { - throw new AuthenticatorStatusNotSupportedException( - sprintf( - 'Of the %d StatusReports tested, none met one of the required FIDO Certified statuses, - or the status was explicitly denied. ' . - 'Reports tested: "%s"', - $reportsProcessed, - implode(', ', $reportLog) - ) - ); - } - } -} diff --git a/src/Service/ClientMetadataService.php b/src/Service/ClientMetadataService.php index 80748af6..ff983972 100644 --- a/src/Service/ClientMetadataService.php +++ b/src/Service/ClientMetadataService.php @@ -35,7 +35,7 @@ public function __construct( public function generateMetadata(Request $request): array { - $timestamp = (new DateTime)->format(DateTime::ISO8601); + $timestamp = (new DateTime)->format(DateTime::ATOM); $hostname = $request->getHost(); $userAgent = $request->headers->get('User-Agent'); $ipAddress = $request->getClientIp(); diff --git a/src/Service/MetadataStatementService.php b/src/Service/MetadataStatementService.php index 99f54f85..2780dd46 100644 --- a/src/Service/MetadataStatementService.php +++ b/src/Service/MetadataStatementService.php @@ -20,19 +20,16 @@ namespace Surfnet\Webauthn\Service; -use Surfnet\Webauthn\Exception\AuthenticatorStatusNotSupportedException; -use Webauthn\MetadataService\Exception\MissingMetadataStatementException; +use Webauthn\Exception\MissingMetadataStatementException; use Webauthn\MetadataService\MetadataStatementRepository; use Webauthn\MetadataService\Statement\MetadataStatement; use Webauthn\MetadataService\StatusReportRepository; use Surfnet\Webauthn\Repository\MetadataStatementRepository as SurfMetadataStatementRepository; -use Webauthn\PublicKeyCredentialSource; class MetadataStatementService implements MetadataStatementRepository, StatusReportRepository { public function __construct( private readonly SurfMetadataStatementRepository $repository, - private readonly AuthenticatorStatusValidator $statusValidator ) { } @@ -52,13 +49,4 @@ public function findStatusReportsByAAGUID(string $aaguid): array { return (array) $this->repository->getStatusReports($aaguid); } - - /** - * @throws AuthenticatorStatusNotSupportedException - */ - public function verifyMeetsRequiredAuthenticatorStatus(PublicKeyCredentialSource $pkco): void - { - $statusReports = $this->findStatusReportsByAAGUID((string) $pkco->aaguid); - $this->statusValidator->validate($statusReports); - } } diff --git a/symfony.lock b/symfony.lock index 2437a1a3..5ac45f19 100644 --- a/symfony.lock +++ b/symfony.lock @@ -587,9 +587,6 @@ "symfony/property-access": { "version": "v4.3.3" }, - "symfony/psr-http-message-bridge": { - "version": "v1.2.0" - }, "symfony/routing": { "version": "4.2", "recipe": { @@ -761,7 +758,17 @@ "version": "4.7.9" }, "web-auth/webauthn-symfony-bundle": { - "version": "v2.0.3" + "version": "4.9", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "3.0", + "ref": "a5dff33bd46575bea263af94069650af7742dcb6" + }, + "files": [ + "config/packages/webauthn.yaml", + "config/routes/webauthn_routes.yaml" + ] }, "web-token/jwt-core": { "version": "v2.0.9" diff --git a/templates/default/authentication.html.twig b/templates/default/authentication.html.twig index 7c62334f..44d91203 100644 --- a/templates/default/authentication.html.twig +++ b/templates/default/authentication.html.twig @@ -6,9 +6,10 @@
@@ -21,9 +22,10 @@ id="inputUsername" value="{{ nameId }}" class="form-control block w-full px-4 py-2 text-xl font-normal text-gray-700 bg-white bg-clip-padding border border-solid border-gray-300 rounded transition ease-in-out m-0 focus:text-gray-700 focus:bg-white focus:border-blue-600 focus:outline-none" + autocomplete="username webauthn" /> - diff --git a/templates/default/registration.html.twig b/templates/default/registration.html.twig index 8ba838f5..7a959be3 100755 --- a/templates/default/registration.html.twig +++ b/templates/default/registration.html.twig @@ -7,8 +7,8 @@