├── LICENSE ├── composer.json └── src ├── Controller ├── AssertionControllerFactory.php ├── AssertionRequestController.php ├── AssertionResponseController.php ├── AttestationControllerFactory.php ├── AttestationRequestController.php ├── AttestationResponseController.php ├── DummyController.php └── DummyControllerFactory.php ├── DataCollector └── WebauthnCollector.php ├── DependencyInjection ├── Compiler │ ├── AttestationStatementSupportCompilerPass.php │ ├── CertificateChainCheckerSetterCompilerPass.php │ ├── CoseAlgorithmCompilerPass.php │ ├── CounterCheckerSetterCompilerPass.php │ ├── DynamicRouteCompilerPass.php │ ├── EnforcedSafetyNetApiKeyVerificationCompilerPass.php │ ├── ExtensionOutputCheckerCompilerPass.php │ ├── LoggerSetterCompilerPass.php │ └── MetadataStatementSupportCompilerPass.php ├── Configuration.php ├── Factory │ └── Security │ │ ├── WebauthnFactory.php │ │ └── WebauthnServicesFactory.php └── WebauthnExtension.php ├── Doctrine └── Type │ ├── AAGUIDDataType.php │ ├── AttestedCredentialDataType.php │ ├── Base64BinaryDataType.php │ ├── PublicKeyCredentialDescriptorCollectionType.php │ ├── PublicKeyCredentialDescriptorType.php │ └── TrustPathDataType.php ├── Dto ├── AdditionalPublicKeyCredentialCreationOptionsRequest.php ├── ServerPublicKeyCredentialCreationOptionsRequest.php └── ServerPublicKeyCredentialRequestOptionsRequest.php ├── Event ├── AuthenticatorAssertionResponseValidationFailedEvent.php ├── AuthenticatorAssertionResponseValidationSucceededEvent.php ├── AuthenticatorAttestationResponseValidationFailedEvent.php ├── AuthenticatorAttestationResponseValidationSucceededEvent.php ├── PublicKeyCredentialCreationOptionsCreatedEvent.php └── PublicKeyCredentialRequestOptionsCreatedEvent.php ├── Repository ├── DummyPublicKeyCredentialSourceRepository.php ├── DummyPublicKeyCredentialUserEntityRepository.php ├── PublicKeyCredentialSourceRepository.php └── PublicKeyCredentialUserEntityRepository.php ├── Resources ├── config │ ├── android_safetynet.php │ ├── controller.php │ ├── cose.php │ ├── dev_services.php │ ├── doctrine-mapping │ │ ├── PublicKeyCredentialEntity.orm.xml │ │ ├── PublicKeyCredentialSource.orm.xml │ │ └── PublicKeyCredentialUserEntity.orm.xml │ ├── metadata_statement_supports.php │ ├── routing.php │ ├── security.php │ └── services.php └── views │ └── data_collector │ ├── tab │ ├── attestation.html.twig │ └── request.html.twig │ └── template.html.twig ├── Routing └── Loader.php ├── Security ├── Authentication │ ├── Exception │ │ ├── WebauthnAuthenticationEvent.php │ │ └── WebauthnAuthenticationEvents.php │ └── Token │ │ ├── WebauthnToken.php │ │ └── WebauthnTokenInterface.php ├── Authorization │ └── Voter │ │ ├── IsUserPresentVoter.php │ │ └── IsUserVerifiedVoter.php ├── Guesser │ ├── CurrentUserEntityGuesser.php │ ├── RequestBodyUserEntityGuesser.php │ └── UserEntityGuesser.php ├── Handler │ ├── CreationOptionsHandler.php │ ├── DefaultCreationOptionsHandler.php │ ├── DefaultFailureHandler.php │ ├── DefaultRequestOptionsHandler.php │ ├── DefaultSuccessHandler.php │ ├── FailureHandler.php │ ├── RequestOptionsHandler.php │ └── SuccessHandler.php ├── Http │ └── Authenticator │ │ ├── Passport │ │ └── Credentials │ │ │ └── WebauthnCredentials.php │ │ └── WebauthnAuthenticator.php ├── Storage │ ├── Item.php │ ├── OptionsStorage.php │ └── SessionStorage.php └── WebauthnFirewallConfig.php ├── Service ├── AuthenticatorAssertionResponseValidator.php ├── AuthenticatorAttestationResponseValidator.php ├── DefaultFailureHandler.php ├── DefaultSuccessHandler.php ├── PublicKeyCredentialCreationOptionsFactory.php └── PublicKeyCredentialRequestOptionsFactory.php └── WebauthnBundle.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2022 Spomky-Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-auth/webauthn-symfony-bundle", 3 | "type": "symfony-bundle", 4 | "license": "MIT", 5 | "description": "FIDO2/Webauthn Security Bundle For Symfony", 6 | "keywords": [ 7 | "FIDO", 8 | "FIDO2", 9 | "webauthn" 10 | ], 11 | "homepage": "https://github.com/web-auth", 12 | "authors": [ 13 | { 14 | "name": "Florent Morselli", 15 | "homepage": "https://github.com/Spomky" 16 | }, 17 | { 18 | "name": "All contributors", 19 | "homepage": "https://github.com/web-auth/webauthn-symfony-bundle/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=8.1", 24 | "nyholm/psr7": "^1.1", 25 | "spomky-labs/cbor-bundle": "^3.0", 26 | "symfony/config": "^6.0", 27 | "symfony/dependency-injection": "^6.0", 28 | "symfony/framework-bundle": "^6.0", 29 | "symfony/http-client": "^6.0", 30 | "symfony/psr-http-message-bridge": "^2.0", 31 | "symfony/security-bundle": "^6.0", 32 | "symfony/serializer": "^6.0", 33 | "symfony/validator": "^6.0", 34 | "web-auth/webauthn-lib": "self.version", 35 | "web-token/jwt-signature": "^3.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Webauthn\\Bundle\\": "src/" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controller/AssertionControllerFactory.php: -------------------------------------------------------------------------------- 1 | logger = new NullLogger(); 37 | } 38 | 39 | public function setLogger(LoggerInterface $logger): void 40 | { 41 | $this->logger = $logger; 42 | } 43 | 44 | public function createAssertionRequestController( 45 | string $profile, 46 | OptionsStorage $optionStorage, 47 | RequestOptionsHandler $optionsHandler, 48 | FailureHandler $failureHandler, 49 | ): AssertionRequestController { 50 | return new AssertionRequestController( 51 | $this->serializer, 52 | $this->validator, 53 | $this->publicKeyCredentialUserEntityRepository, 54 | $this->publicKeyCredentialSourceRepository, 55 | $this->publicKeyCredentialRequestOptionsFactory, 56 | $profile, 57 | $optionStorage, 58 | $optionsHandler, 59 | $failureHandler, 60 | $this->logger 61 | ); 62 | } 63 | 64 | public function createAssertionResponseController( 65 | OptionsStorage $optionStorage, 66 | SuccessHandler $successHandler, 67 | FailureHandler $failureHandler, 68 | ): AssertionResponseController { 69 | return new AssertionResponseController( 70 | $this->httpMessageFactory, 71 | $this->publicKeyCredentialLoader, 72 | $this->attestationResponseValidator, 73 | $this->logger, 74 | $optionStorage, 75 | $successHandler, 76 | $failureHandler, 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Controller/AssertionRequestController.php: -------------------------------------------------------------------------------- 1 | getContentType(), 'Only JSON content type allowed'); 50 | $content = $request->getContent(); 51 | Assertion::string($content, 'Invalid data'); 52 | $creationOptionsRequest = $this->getServerPublicKeyCredentialRequestOptionsRequest($content); 53 | $extensions = $creationOptionsRequest->extensions !== null ? AuthenticationExtensionsClientInputs::createFromArray( 54 | $creationOptionsRequest->extensions 55 | ) : null; 56 | $userEntity = $creationOptionsRequest->username === null ? null : $this->userEntityRepository->findOneByUsername( 57 | $creationOptionsRequest->username 58 | ); 59 | $allowedCredentials = $userEntity === null ? [] : $this->getCredentials($userEntity); 60 | $publicKeyCredentialRequestOptions = $this->publicKeyCredentialRequestOptionsFactory->create( 61 | $this->profile, 62 | $allowedCredentials, 63 | $creationOptionsRequest->userVerification, 64 | $extensions 65 | ); 66 | 67 | $response = $this->optionsHandler->onRequestOptions($publicKeyCredentialRequestOptions, $userEntity); 68 | $this->optionsStorage->store(Item::create($publicKeyCredentialRequestOptions, $userEntity),); 69 | 70 | return $response; 71 | } catch (Throwable $throwable) { 72 | $this->logger->error($throwable->getMessage()); 73 | 74 | return $this->failureHandler->onFailure($request, $throwable); 75 | } 76 | } 77 | 78 | /** 79 | * @return PublicKeyCredentialDescriptor[] 80 | */ 81 | private function getCredentials(PublicKeyCredentialUserEntity $userEntity): array 82 | { 83 | $credentialSources = $this->credentialSourceRepository->findAllForUserEntity($userEntity); 84 | 85 | return array_map( 86 | static fn (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor => $credential->getPublicKeyCredentialDescriptor(), 87 | $credentialSources 88 | ); 89 | } 90 | 91 | private function getServerPublicKeyCredentialRequestOptionsRequest( 92 | string $content 93 | ): ServerPublicKeyCredentialRequestOptionsRequest { 94 | $data = $this->serializer->deserialize( 95 | $content, 96 | ServerPublicKeyCredentialRequestOptionsRequest::class, 97 | 'json', 98 | [ 99 | AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true, 100 | ] 101 | ); 102 | Assertion::isInstanceOf($data, ServerPublicKeyCredentialRequestOptionsRequest::class, 'Invalid data'); 103 | $errors = $this->validator->validate($data); 104 | if (count($errors) > 0) { 105 | $messages = []; 106 | foreach ($errors as $error) { 107 | $messages[] = $error->getPropertyPath() . ': ' . $error->getMessage(); 108 | } 109 | throw new RuntimeException(implode("\n", $messages)); 110 | } 111 | 112 | return $data; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Controller/AssertionResponseController.php: -------------------------------------------------------------------------------- 1 | getContentType(), 'Only JSON content type allowed'); 38 | $content = $request->getContent(); 39 | Assertion::string($content, 'Invalid data'); 40 | $publicKeyCredential = $this->publicKeyCredentialLoader->load($content); 41 | $response = $publicKeyCredential->getResponse(); 42 | Assertion::isInstanceOf($response, AuthenticatorAssertionResponse::class, 'Invalid response'); 43 | $data = $this->optionsStorage->get(); 44 | $publicKeyCredentialRequestOptions = $data->getPublicKeyCredentialOptions(); 45 | Assertion::isInstanceOf( 46 | $publicKeyCredentialRequestOptions, 47 | PublicKeyCredentialRequestOptions::class, 48 | 'Invalid response' 49 | ); 50 | $userEntity = $data->getPublicKeyCredentialUserEntity(); 51 | $psr7Request = $this->httpMessageFactory->createRequest($request); 52 | $this->assertionResponseValidator->check( 53 | $publicKeyCredential->getRawId(), 54 | $response, 55 | $publicKeyCredentialRequestOptions, 56 | $psr7Request, 57 | $userEntity?->getId() 58 | ); 59 | 60 | return $this->successHandler->onSuccess($request); 61 | } catch (Throwable $throwable) { 62 | $this->logger->error($throwable->getMessage()); 63 | 64 | return $this->failureHandler->onFailure($request, $throwable); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Controller/AttestationControllerFactory.php: -------------------------------------------------------------------------------- 1 | serializer, 43 | $this->validator, 44 | $this->publicKeyCredentialSourceRepository, 45 | $this->publicKeyCredentialCreationOptionsFactory, 46 | $profile, 47 | $optionStorage, 48 | $creationOptionsHandler, 49 | $failureHandler 50 | ); 51 | } 52 | 53 | /** 54 | * @param string[] $securedRelyingPartyIds 55 | */ 56 | public function createAttestationResponseController( 57 | OptionsStorage $optionStorage, 58 | SuccessHandler $successHandler, 59 | FailureHandler $failureHandler, 60 | array $securedRelyingPartyIds 61 | ): AttestationResponseController { 62 | return new AttestationResponseController( 63 | $this->httpMessageFactory, 64 | $this->publicKeyCredentialLoader, 65 | $this->attestationResponseValidator, 66 | $this->publicKeyCredentialSourceRepository, 67 | $optionStorage, 68 | $successHandler, 69 | $failureHandler, 70 | $securedRelyingPartyIds 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Controller/AttestationRequestController.php: -------------------------------------------------------------------------------- 1 | getContentType(), 'Only JSON content type allowed'); 50 | $content = $request->getContent(); 51 | Assertion::string($content, 'Invalid data'); 52 | 53 | $userEntity = $this->userEntityGuesser->findUserEntity($request); 54 | $publicKeyCredentialCreationOptions = $this->getPublicKeyCredentialCreationOptions( 55 | $content, 56 | $userEntity 57 | ); 58 | 59 | $response = $this->creationOptionsHandler->onCreationOptions( 60 | $publicKeyCredentialCreationOptions, 61 | $userEntity 62 | ); 63 | $this->optionsStorage->store(Item::create($publicKeyCredentialCreationOptions, $userEntity)); 64 | 65 | return $response; 66 | } catch (Throwable $throwable) { 67 | return $this->failureHandler->onFailure($request, $throwable); 68 | } 69 | } 70 | 71 | /** 72 | * @return PublicKeyCredentialDescriptor[] 73 | */ 74 | private function getCredentials(PublicKeyCredentialUserEntity $userEntity): array 75 | { 76 | $credentialSources = $this->credentialSourceRepository->findAllForUserEntity($userEntity); 77 | 78 | return array_map( 79 | static fn (PublicKeyCredentialSource $credential): PublicKeyCredentialDescriptor => $credential->getPublicKeyCredentialDescriptor(), 80 | $credentialSources 81 | ); 82 | } 83 | 84 | private function getPublicKeyCredentialCreationOptions( 85 | string $content, 86 | PublicKeyCredentialUserEntity $userEntity 87 | ): PublicKeyCredentialCreationOptions { 88 | $excludedCredentials = $this->getCredentials($userEntity); 89 | $creationOptionsRequest = $this->getServerPublicKeyCredentialCreationOptionsRequest($content); 90 | $authenticatorSelection = $creationOptionsRequest->authenticatorSelection; 91 | if (is_array($authenticatorSelection)) { 92 | $authenticatorSelection = AuthenticatorSelectionCriteria::createFromArray($authenticatorSelection); 93 | } 94 | $extensions = $creationOptionsRequest->extensions; 95 | if (is_array($extensions)) { 96 | $extensions = AuthenticationExtensionsClientInputs::createFromArray($extensions); 97 | } 98 | 99 | return $this->publicKeyCredentialCreationOptionsFactory->create( 100 | $this->profile, 101 | $userEntity, 102 | $excludedCredentials, 103 | $authenticatorSelection, 104 | $creationOptionsRequest->attestation, 105 | $extensions 106 | ); 107 | } 108 | 109 | private function getServerPublicKeyCredentialCreationOptionsRequest( 110 | string $content 111 | ): AdditionalPublicKeyCredentialCreationOptionsRequest { 112 | $data = $this->serializer->deserialize( 113 | $content, 114 | AdditionalPublicKeyCredentialCreationOptionsRequest::class, 115 | 'json' 116 | ); 117 | Assertion::isInstanceOf($data, AdditionalPublicKeyCredentialCreationOptionsRequest::class, 'Invalid data'); 118 | $errors = $this->validator->validate($data); 119 | if (count($errors) > 0) { 120 | $messages = []; 121 | foreach ($errors as $error) { 122 | $messages[] = $error->getPropertyPath() . ': ' . $error->getMessage(); 123 | } 124 | throw new RuntimeException(implode("\n", $messages)); 125 | } 126 | 127 | return $data; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Controller/AttestationResponseController.php: -------------------------------------------------------------------------------- 1 | getContentType(), 'Only JSON content type allowed'); 44 | $content = $request->getContent(); 45 | Assertion::string($content, 'Invalid data'); 46 | $publicKeyCredential = $this->publicKeyCredentialLoader->load($content); 47 | $response = $publicKeyCredential->getResponse(); 48 | Assertion::isInstanceOf($response, AuthenticatorAttestationResponse::class, 'Invalid response'); 49 | 50 | $storedData = $this->optionStorage->get(); 51 | 52 | $publicKeyCredentialCreationOptions = $storedData->getPublicKeyCredentialOptions(); 53 | Assertion::isInstanceOf( 54 | $publicKeyCredentialCreationOptions, 55 | PublicKeyCredentialCreationOptions::class, 56 | 'Unable to find the public key credential creation options' 57 | ); 58 | $userEntity = $storedData->getPublicKeyCredentialUserEntity(); 59 | Assertion::isInstanceOf( 60 | $userEntity, 61 | PublicKeyCredentialUserEntity::class, 62 | 'Unable to find the public key credential user entity' 63 | ); 64 | $psr7Request = $this->httpMessageFactory->createRequest($request); 65 | $credentialSource = $this->attestationResponseValidator->check( 66 | $response, 67 | $publicKeyCredentialCreationOptions, 68 | $psr7Request, 69 | $this->securedRelyingPartyIds 70 | ); 71 | 72 | if ($this->credentialSourceRepository->findOneByCredentialId( 73 | $credentialSource->getPublicKeyCredentialId() 74 | ) !== null) { 75 | throw new InvalidArgumentException('The credentials already exists'); 76 | } 77 | $this->credentialSourceRepository->saveCredentialSource($credentialSource); 78 | 79 | return $this->successHandler->onSuccess($request); 80 | } catch (Throwable $throwable) { 81 | return $this->failureHandler->onFailure($request, $throwable); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Controller/DummyController.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | private array $publicKeyCredentialCreationOptions = []; 29 | 30 | /** 31 | * @var array 32 | */ 33 | private array $authenticatorAttestationResponseValidationSucceeded = []; 34 | 35 | /** 36 | * @var array 37 | */ 38 | private array $authenticatorAttestationResponseValidationFailed = []; 39 | 40 | /** 41 | * @var array 42 | */ 43 | private array $publicKeyCredentialRequestOptions = []; 44 | 45 | /** 46 | * @var array 47 | */ 48 | private array $authenticatorAssertionResponseValidationSucceeded = []; 49 | 50 | /** 51 | * @var array 52 | */ 53 | private array $authenticatorAssertionResponseValidationFailed = []; 54 | 55 | public function collect(Request $request, Response $response, ?Throwable $exception = null): void 56 | { 57 | $this->data = [ 58 | 'publicKeyCredentialCreationOptions' => $this->publicKeyCredentialCreationOptions, 59 | 'authenticatorAttestationResponseValidationSucceeded' => $this->authenticatorAttestationResponseValidationSucceeded, 60 | 'authenticatorAttestationResponseValidationFailed' => $this->authenticatorAttestationResponseValidationFailed, 61 | 'publicKeyCredentialRequestOptions' => $this->publicKeyCredentialRequestOptions, 62 | 'authenticatorAssertionResponseValidationSucceeded' => $this->authenticatorAssertionResponseValidationSucceeded, 63 | 'authenticatorAssertionResponseValidationFailed' => $this->authenticatorAssertionResponseValidationFailed, 64 | ]; 65 | } 66 | 67 | /** 68 | * @return array|Data 69 | */ 70 | public function getData(): array|Data 71 | { 72 | return $this->data; 73 | } 74 | 75 | public function getName(): string 76 | { 77 | return 'webauthn_collector'; 78 | } 79 | 80 | public function reset(): void 81 | { 82 | $this->data = []; 83 | } 84 | 85 | /** 86 | * {@inheritDoc} 87 | * 88 | * @return array> 89 | */ 90 | public static function getSubscribedEvents(): array 91 | { 92 | return [ 93 | PublicKeyCredentialCreationOptionsCreatedEvent::class => ['addPublicKeyCredentialCreationOptions'], 94 | PublicKeyCredentialRequestOptionsCreatedEvent::class => ['addPublicKeyCredentialRequestOptions'], 95 | AuthenticatorAttestationResponseValidationSucceededEvent::class => [ 96 | 'addAuthenticatorAttestationResponseValidationSucceeded', 97 | ], 98 | AuthenticatorAttestationResponseValidationFailedEvent::class => [ 99 | 'addAuthenticatorAttestationResponseValidationFailed', 100 | ], 101 | AuthenticatorAssertionResponseValidationSucceededEvent::class => [ 102 | 'addAuthenticatorAssertionResponseValidationSucceeded', 103 | ], 104 | AuthenticatorAssertionResponseValidationFailedEvent::class => [ 105 | 'addAuthenticatorAssertionResponseValidationFailed', 106 | ], 107 | ]; 108 | } 109 | 110 | public function addPublicKeyCredentialCreationOptions(PublicKeyCredentialCreationOptionsCreatedEvent $event): void 111 | { 112 | $cloner = new VarCloner(); 113 | $this->publicKeyCredentialCreationOptions[] = [ 114 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialCreationOptions()), 115 | 'json' => json_encode( 116 | $event->getPublicKeyCredentialCreationOptions(), 117 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 118 | ), 119 | ]; 120 | } 121 | 122 | public function addAuthenticatorAttestationResponseValidationSucceeded( 123 | AuthenticatorAttestationResponseValidationSucceededEvent $event 124 | ): void { 125 | $cloner = new VarCloner(); 126 | $this->authenticatorAttestationResponseValidationSucceeded[] = [ 127 | 'attestation_response' => $cloner->cloneVar($event->getAuthenticatorAttestationResponse()), 128 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialCreationOptions()), 129 | 'options_json' => json_encode( 130 | $event->getPublicKeyCredentialCreationOptions(), 131 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 132 | ), 133 | 'credential_source' => $cloner->cloneVar($event->getPublicKeyCredentialSource()), 134 | ]; 135 | } 136 | 137 | public function addAuthenticatorAttestationResponseValidationFailed( 138 | AuthenticatorAttestationResponseValidationFailedEvent $event 139 | ): void { 140 | $cloner = new VarCloner(); 141 | $this->authenticatorAttestationResponseValidationFailed[] = [ 142 | 'attestation_response' => $cloner->cloneVar($event->getAuthenticatorAttestationResponse()), 143 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialCreationOptions()), 144 | 'options_json' => json_encode( 145 | $event->getPublicKeyCredentialCreationOptions(), 146 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 147 | ), 148 | 'exception' => $cloner->cloneVar($event->getThrowable()), 149 | ]; 150 | } 151 | 152 | public function addPublicKeyCredentialRequestOptions(PublicKeyCredentialRequestOptionsCreatedEvent $event): void 153 | { 154 | $cloner = new VarCloner(); 155 | $this->publicKeyCredentialRequestOptions[] = [ 156 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialRequestOptions()), 157 | 'json' => json_encode( 158 | $event->getPublicKeyCredentialRequestOptions(), 159 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 160 | ), 161 | ]; 162 | } 163 | 164 | public function addAuthenticatorAssertionResponseValidationSucceeded( 165 | AuthenticatorAssertionResponseValidationSucceededEvent $event 166 | ): void { 167 | $cloner = new VarCloner(); 168 | $this->authenticatorAssertionResponseValidationSucceeded[] = [ 169 | 'user_handle' => $cloner->cloneVar($event->getUserHandle()), 170 | 'credential_id' => $cloner->cloneVar($event->getCredentialId()), 171 | 'assertion_response' => $cloner->cloneVar($event->getAuthenticatorAssertionResponse()), 172 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialRequestOptions()), 173 | 'options_json' => json_encode( 174 | $event->getPublicKeyCredentialRequestOptions(), 175 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 176 | ), 177 | 'credential_source' => $cloner->cloneVar($event->getPublicKeyCredentialSource()), 178 | ]; 179 | } 180 | 181 | public function addAuthenticatorAssertionResponseValidationFailed( 182 | AuthenticatorAssertionResponseValidationFailedEvent $event 183 | ): void { 184 | $cloner = new VarCloner(); 185 | $this->authenticatorAssertionResponseValidationFailed[] = [ 186 | 'user_handle' => $cloner->cloneVar($event->getUserHandle()), 187 | 'credential_id' => $cloner->cloneVar($event->getCredentialId()), 188 | 'assertion_response' => $cloner->cloneVar($event->getAuthenticatorAssertionResponse()), 189 | 'options' => $cloner->cloneVar($event->getPublicKeyCredentialRequestOptions()), 190 | 'options_json' => json_encode( 191 | $event->getPublicKeyCredentialRequestOptions(), 192 | JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT 193 | ), 194 | 'exception' => $cloner->cloneVar($event->getThrowable()), 195 | ]; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/AttestationStatementSupportCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition(AttestationStatementSupportManager::class)) { 22 | return; 23 | } 24 | 25 | $definition = $container->getDefinition(AttestationStatementSupportManager::class); 26 | $taggedServices = $container->findTaggedServiceIds(self::TAG); 27 | foreach ($taggedServices as $id => $attributes) { 28 | $definition->addMethodCall('add', [new Reference($id)]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CertificateChainCheckerSetterCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasAlias(CertificateChainChecker::class) 21 | || ! $container->hasDefinition(AuthenticatorAttestationResponseValidator::class) 22 | ) { 23 | return; 24 | } 25 | 26 | $definition = $container->getDefinition(AuthenticatorAttestationResponseValidator::class); 27 | $definition->addMethodCall('setCertificateChainChecker', [new Reference(CertificateChainChecker::class)]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CoseAlgorithmCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition('webauthn.cose.algorithm.manager')) { 21 | return; 22 | } 23 | 24 | $definition = $container->getDefinition('webauthn.cose.algorithm.manager'); 25 | 26 | $taggedServices = $container->findTaggedServiceIds(self::TAG); 27 | foreach ($taggedServices as $id => $attributes) { 28 | $definition->addMethodCall('add', [new Reference($id)]); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/CounterCheckerSetterCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasAlias(CounterChecker::class) 22 | || ! $container->hasDefinition(AuthenticatorAssertionResponseValidator::class) 23 | ) { 24 | return; 25 | } 26 | 27 | $definition = $container->getDefinition(AuthenticatorAssertionResponseValidator::class); 28 | $definition->addMethodCall('setCounterChecker', [new Reference(CounterChecker::class)]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/DynamicRouteCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition(Loader::class)) { 22 | return; 23 | } 24 | 25 | $definition = $container->getDefinition(Loader::class); 26 | 27 | $taggedServices = $container->findTaggedServiceIds(self::TAG); 28 | foreach ($taggedServices as $id => $tags) { 29 | foreach ($tags as $attributes) { 30 | Assertion::keyExists($attributes, 'path', sprintf('The path is missing for "%s"', $id)); 31 | Assertion::keyExists($attributes, 'host', sprintf('The host is missing for "%s"', $id)); 32 | $definition->addMethodCall('add', [$attributes['path'], $attributes['host'], $id]); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/EnforcedSafetyNetApiKeyVerificationCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition(AndroidSafetyNetAttestationStatementSupport::class) 20 | || ! $container->hasAlias('webauthn.android_safetynet.http_client') 21 | || ! $container->hasParameter('webauthn.android_safetynet.api_key') 22 | || $container->getParameter('webauthn.android_safetynet.api_key') === null 23 | || ! $container->hasAlias('webauthn.android_safetynet.request_factory') 24 | ) { 25 | return; 26 | } 27 | 28 | $definition = $container->getDefinition(AndroidSafetyNetAttestationStatementSupport::class); 29 | $definition->addMethodCall('enableApiVerification', [ 30 | new Reference('webauthn.android_safetynet.http_client'), 31 | $container->getParameter('webauthn.android_safetynet.api_key'), 32 | new Reference('webauthn.android_safetynet.request_factory'), 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/ExtensionOutputCheckerCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasDefinition(ExtensionOutputCheckerHandler::class)) { 22 | return; 23 | } 24 | 25 | $definition = $container->getDefinition(ExtensionOutputCheckerHandler::class); 26 | 27 | $taggedServices = $container->findTaggedServiceIds(self::TAG); 28 | foreach ($taggedServices as $id => $attributes) { 29 | $definition->addMethodCall('add', [new Reference($id)]); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/LoggerSetterCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasAlias('webauthn.logger')) { 28 | return; 29 | } 30 | 31 | $this->setLoggerToServiceDefinition($container, AuthenticatorAssertionResponseValidator::class); 32 | $this->setLoggerToServiceDefinition($container, AuthenticatorAttestationResponseValidator::class); 33 | $this->setLoggerToServiceDefinition($container, PublicKeyCredentialLoader::class); 34 | $this->setLoggerToServiceDefinition($container, AttestationObjectLoader::class); 35 | $this->setLoggerToServiceDefinition($container, ThrowExceptionIfInvalid::class); 36 | $this->setLoggerToServiceDefinition($container, DummyPublicKeyCredentialUserEntityRepository::class); 37 | $this->setLoggerToServiceDefinition($container, DummyPublicKeyCredentialSourceRepository::class); 38 | $this->setLoggerToServiceDefinition($container, WebauthnAuthenticator::class); 39 | $this->setLoggerToServiceDefinition($container, AssertionControllerFactory::class); 40 | } 41 | 42 | private function setLoggerToServiceDefinition(ContainerBuilder $container, string $service): void 43 | { 44 | if (! $container->hasDefinition($service)) { 45 | return; 46 | } 47 | 48 | $definition = $container->getDefinition($service); 49 | $definition->addMethodCall('setLogger', [new Reference('webauthn.logger')]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DependencyInjection/Compiler/MetadataStatementSupportCompilerPass.php: -------------------------------------------------------------------------------- 1 | hasAlias(MetadataStatementRepository::class) 23 | || ! $container->hasAlias(CertificateChainChecker::class) 24 | || ! $container->hasAlias(StatusReportRepository::class) 25 | ) { 26 | return; 27 | } 28 | if (! $container->hasDefinition(AuthenticatorAttestationResponseValidator::class)) { 29 | return; 30 | } 31 | 32 | $definition = $container->getDefinition(AuthenticatorAttestationResponseValidator::class); 33 | $definition->addMethodCall( 34 | 'enableMetadataStatementSupport', 35 | [ 36 | new Reference(MetadataStatementRepository::class), 37 | new Reference(StatusReportRepository::class), 38 | new Reference(CertificateChainChecker::class), 39 | ] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | [ 38 | 'rp' => [ 39 | 'name' => 'Secured Application', 40 | ], 41 | ], 42 | ]; 43 | /** @noRector Rector\DeadCode\Rector\Assign\RemoveUnusedVariableAssignRector */ 44 | $defaultRequestProfiles = [ 45 | 'default' => [], 46 | ]; 47 | 48 | $treeBuilder = new TreeBuilder($this->alias); 49 | $treeBuilder->getRootNode() 50 | ->addDefaultsIfNotSet() 51 | ->beforeNormalization() 52 | ->ifArray() 53 | ->then(static function ($v): array { 54 | if (! isset($v['creation_profiles'])) { 55 | $v['creation_profiles'] = null; 56 | } 57 | if (! isset($v['request_profiles'])) { 58 | $v['request_profiles'] = null; 59 | } 60 | 61 | return $v; 62 | }) 63 | ->end() 64 | ->children() 65 | ->scalarNode('http_message_factory') 66 | ->cannotBeEmpty() 67 | ->defaultValue('webauthn.http_message_factory.default') 68 | ->info('Creates PSR-7 HTTP Request and Response instances from Symfony ones.') 69 | ->end() 70 | ->scalarNode('request_factory') 71 | ->cannotBeEmpty() 72 | ->defaultValue('webauthn.request_factory.default') 73 | ->info('PSR-17 Request Factory.') 74 | ->end() 75 | ->scalarNode('http_client') 76 | ->cannotBeEmpty() 77 | ->defaultValue('webauthn.http_client.default') 78 | ->info('A PSR-18 HTTP client.') 79 | ->end() 80 | ->scalarNode('logger') 81 | ->defaultValue('webauthn.logger.default') 82 | ->info('A PSR-3 logger to receive logs during the processes') 83 | ->end() 84 | ->scalarNode('credential_repository') 85 | ->cannotBeEmpty() 86 | ->defaultValue(DummyPublicKeyCredentialSourceRepository::class) 87 | ->info('This repository is responsible of the credential storage') 88 | ->end() 89 | ->scalarNode('user_repository') 90 | ->cannotBeEmpty() 91 | ->defaultValue(DummyPublicKeyCredentialUserEntityRepository::class) 92 | ->info('This repository is responsible of the user storage') 93 | ->end() 94 | ->scalarNode('token_binding_support_handler') 95 | ->defaultValue(IgnoreTokenBindingHandler::class) 96 | ->cannotBeEmpty() 97 | ->info('This handler will check the token binding header from the request. By default, it is ignored.') 98 | ->end() 99 | ->scalarNode('counter_checker') 100 | ->defaultValue(ThrowExceptionIfInvalid::class) 101 | ->cannotBeEmpty() 102 | ->info('This service will check if the counter is valid. By default it throws an exception (recommended).') 103 | ->end() 104 | ->arrayNode('android_safetynet') 105 | ->addDefaultsIfNotSet() 106 | ->info('Additional configuration options for the Android SafetyNet attestation.') 107 | ->children() 108 | ->integerNode('leeway') 109 | ->defaultValue(0) 110 | ->min(0) 111 | ->info( 112 | 'Leeway for timestamp verification in response (in millisecond). At least 2000 msec are recommended.' 113 | ) 114 | ->end() 115 | ->integerNode('max_age') 116 | ->min(0) 117 | ->defaultValue(60000) 118 | ->info('Maximum age of the response (in millisecond)') 119 | ->end() 120 | ->scalarNode('api_key') 121 | ->defaultNull() 122 | ->info( 123 | 'If set, the application will verify the statements using Google API. See https://console.cloud.google.com/apis/library to get it.' 124 | ) 125 | ->end() 126 | ->end() 127 | ->end() 128 | ->arrayNode('metadata') 129 | ->canBeEnabled() 130 | ->info('Enable the support of the Metadata Statements. Please read the documentation for this feature.') 131 | ->children() 132 | ->scalarNode('mds_repository') 133 | ->isRequired() 134 | ->info('The Metadata Statement repository.') 135 | ->end() 136 | ->scalarNode('status_report_repository') 137 | ->isRequired() 138 | ->info('The Status Report repository.') 139 | ->end() 140 | ->scalarNode('certificate_chain_checker') 141 | ->cannotBeEmpty() 142 | ->defaultValue(PhpCertificateChainChecker::class) 143 | ->info('A Certificate Chain checker.') 144 | ->end() 145 | ->end() 146 | ->end() 147 | ->arrayNode('controllers') 148 | ->canBeEnabled() 149 | ->children() 150 | ->arrayNode('creation') 151 | ->treatFalseLike([]) 152 | ->treatNullLike([]) 153 | ->treatTrueLike([]) 154 | ->useAttributeAsKey('name') 155 | ->arrayPrototype() 156 | ->addDefaultsIfNotSet() 157 | ->children() 158 | ->scalarNode('options_path') 159 | ->isRequired() 160 | ->end() 161 | ->scalarNode('result_path') 162 | ->isRequired() 163 | ->end() 164 | ->scalarNode('host') 165 | ->defaultValue(null) 166 | ->end() 167 | ->scalarNode('profile') 168 | ->defaultValue('default') 169 | ->end() 170 | ->scalarNode('user_entity_guesser') 171 | ->isRequired() 172 | ->end() 173 | ->scalarNode('options_storage') 174 | ->defaultValue(SessionStorage::class) 175 | ->info('Service responsible of the options/user entity storage during the ceremony') 176 | ->end() 177 | ->scalarNode('success_handler') 178 | ->defaultValue(DefaultSuccessHandler::class) 179 | ->end() 180 | ->scalarNode('failure_handler') 181 | ->defaultValue(DefaultFailureHandler::class) 182 | ->end() 183 | ->scalarNode('options_handler') 184 | ->defaultValue(DefaultCreationOptionsHandler::class) 185 | ->end() 186 | ->arrayNode('secured_rp_ids') 187 | ->treatFalseLike([]) 188 | ->treatTrueLike([]) 189 | ->treatNullLike([]) 190 | ->useAttributeAsKey('name') 191 | ->scalarPrototype() 192 | ->end() 193 | ->end() 194 | ->end() 195 | ->end() 196 | ->end() 197 | ->arrayNode('request') 198 | ->treatFalseLike([]) 199 | ->treatNullLike([]) 200 | ->treatTrueLike([]) 201 | ->useAttributeAsKey('name') 202 | ->arrayPrototype() 203 | ->addDefaultsIfNotSet() 204 | ->children() 205 | ->scalarNode('options_path') 206 | ->isRequired() 207 | ->end() 208 | ->scalarNode('result_path') 209 | ->isRequired() 210 | ->end() 211 | ->scalarNode('host') 212 | ->defaultValue(null) 213 | ->end() 214 | ->scalarNode('profile') 215 | ->defaultValue('default') 216 | ->end() 217 | ->scalarNode('options_storage') 218 | ->defaultValue(SessionStorage::class) 219 | ->info('Service responsible of the options/user entity storage during the ceremony') 220 | ->end() 221 | ->scalarNode('success_handler') 222 | ->defaultValue(DefaultSuccessHandler::class) 223 | ->end() 224 | ->scalarNode('failure_handler') 225 | ->defaultValue(DefaultFailureHandler::class) 226 | ->end() 227 | ->scalarNode('options_handler') 228 | ->defaultValue(DefaultRequestOptionsHandler::class) 229 | ->end() 230 | ->arrayNode('secured_rp_ids') 231 | ->treatFalseLike([]) 232 | ->treatTrueLike([]) 233 | ->treatNullLike([]) 234 | ->useAttributeAsKey('name') 235 | ->scalarPrototype() 236 | ->end() 237 | ->end() 238 | ->end() 239 | ->end() 240 | ->end() 241 | ->end() 242 | ->end() 243 | ->arrayNode('creation_profiles') 244 | ->treatFalseLike($defaultCreationProfiles) 245 | ->treatNullLike($defaultCreationProfiles) 246 | ->treatTrueLike($defaultCreationProfiles) 247 | ->useAttributeAsKey('name') 248 | ->arrayPrototype() 249 | ->addDefaultsIfNotSet() 250 | ->children() 251 | ->arrayNode('rp') 252 | ->isRequired() 253 | ->children() 254 | ->scalarNode('id') 255 | ->defaultNull() 256 | ->end() 257 | ->scalarNode('name') 258 | ->isRequired() 259 | ->end() 260 | ->scalarNode('icon') 261 | ->defaultNull() 262 | ->end() 263 | ->end() 264 | ->end() 265 | ->integerNode('challenge_length') 266 | ->min(16) 267 | ->defaultValue(32) 268 | ->end() 269 | ->integerNode('timeout') 270 | ->min(0) 271 | ->defaultNull() 272 | ->end() 273 | ->arrayNode('authenticator_selection_criteria') 274 | ->addDefaultsIfNotSet() 275 | ->children() 276 | ->scalarNode('attachment_mode') 277 | ->defaultValue(AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE) 278 | ->validate() 279 | ->ifNotInArray([ 280 | AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, 281 | AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM, 282 | AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM, 283 | ]) 284 | ->thenInvalid('Invalid value "%s"') 285 | ->end() 286 | ->end() 287 | ->booleanNode('require_resident_key') 288 | ->defaultFalse() 289 | ->end() 290 | ->scalarNode('user_verification') 291 | ->defaultValue(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED) 292 | ->validate() 293 | ->ifNotInArray([ 294 | AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, 295 | AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, 296 | AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED, 297 | ]) 298 | ->thenInvalid('Invalid value "%s"') 299 | ->end() 300 | ->end() 301 | ->scalarNode('resident_key') 302 | ->defaultValue(AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_PREFERRED) 303 | ->validate() 304 | ->ifNotInArray([ 305 | AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_NONE, 306 | AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_DISCOURAGED, 307 | AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_PREFERRED, 308 | AuthenticatorSelectionCriteria::RESIDENT_KEY_REQUIREMENT_REQUIRED, 309 | ]) 310 | ->thenInvalid('Invalid value "%s"') 311 | ->end() 312 | ->end() 313 | ->end() 314 | ->end() 315 | ->arrayNode('extensions') 316 | ->treatFalseLike([]) 317 | ->treatTrueLike([]) 318 | ->treatNullLike([]) 319 | ->useAttributeAsKey('name') 320 | ->scalarPrototype() 321 | ->end() 322 | ->end() 323 | ->arrayNode('public_key_credential_parameters') 324 | ->integerPrototype() 325 | ->end() 326 | ->requiresAtLeastOneElement() 327 | ->treatNullLike([]) 328 | ->treatFalseLike([]) 329 | ->treatTrueLike([]) 330 | ->defaultValue([ 331 | Algorithms::COSE_ALGORITHM_EdDSA, 332 | Algorithms::COSE_ALGORITHM_ES256, 333 | Algorithms::COSE_ALGORITHM_ES256K, 334 | Algorithms::COSE_ALGORITHM_ES384, 335 | Algorithms::COSE_ALGORITHM_ES512, 336 | Algorithms::COSE_ALGORITHM_RS256, 337 | Algorithms::COSE_ALGORITHM_RS384, 338 | Algorithms::COSE_ALGORITHM_RS512, 339 | Algorithms::COSE_ALGORITHM_PS256, 340 | Algorithms::COSE_ALGORITHM_PS384, 341 | Algorithms::COSE_ALGORITHM_PS512, 342 | ]) 343 | ->end() 344 | ->scalarNode('attestation_conveyance') 345 | ->defaultValue(PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) 346 | ->end() 347 | ->end() 348 | ->end() 349 | ->end() 350 | ->arrayNode('request_profiles') 351 | ->treatFalseLike($defaultRequestProfiles) 352 | ->treatTrueLike($defaultRequestProfiles) 353 | ->treatNullLike($defaultRequestProfiles) 354 | ->useAttributeAsKey('name') 355 | ->arrayPrototype() 356 | ->addDefaultsIfNotSet() 357 | ->children() 358 | ->scalarNode('rp_id') 359 | ->defaultNull() 360 | ->end() 361 | ->integerNode('challenge_length') 362 | ->min(16) 363 | ->defaultValue(32) 364 | ->end() 365 | ->integerNode('timeout') 366 | ->min(0) 367 | ->defaultNull() 368 | ->end() 369 | ->scalarNode('user_verification') 370 | ->defaultValue(AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED)->end() 371 | ->arrayNode('extensions') 372 | ->treatFalseLike([]) 373 | ->treatTrueLike([]) 374 | ->treatNullLike([]) 375 | ->useAttributeAsKey('name') 376 | ->scalarPrototype() 377 | ->end() 378 | ->end() 379 | ->end() 380 | ->end() 381 | ->end() 382 | ->end() 383 | ; 384 | 385 | return $treeBuilder; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/DependencyInjection/Factory/Security/WebauthnFactory.php: -------------------------------------------------------------------------------- 1 | children() 91 | ->scalarNode('user_provider') 92 | ->defaultNull() 93 | ->end() 94 | ->scalarNode('options_storage') 95 | ->defaultValue(self::DEFAULT_SESSION_STORAGE_SERVICE) 96 | ->end() 97 | ->scalarNode('success_handler') 98 | ->defaultValue(self::DEFAULT_SUCCESS_HANDLER_SERVICE) 99 | ->end() 100 | ->scalarNode('failure_handler') 101 | ->defaultValue(self::DEFAULT_FAILURE_HANDLER_SERVICE) 102 | ->end() 103 | ->arrayNode('secured_rp_ids') 104 | ->treatFalseLike([]) 105 | ->treatTrueLike([]) 106 | ->treatNullLike([]) 107 | ->useAttributeAsKey('name') 108 | ->scalarPrototype() 109 | ->end() 110 | ->end() 111 | ->arrayNode('authentication') 112 | ->canBeDisabled() 113 | ->children() 114 | ->scalarNode('profile') 115 | ->defaultValue('default') 116 | ->end() 117 | ->arrayNode('routes') 118 | ->addDefaultsIfNotSet() 119 | ->children() 120 | ->scalarNode('host') 121 | ->defaultNull() 122 | ->end() 123 | ->scalarNode('options_path') 124 | ->defaultValue(self::DEFAULT_LOGIN_OPTIONS_PATH) 125 | ->end() 126 | ->scalarNode('result_path') 127 | ->defaultValue(self::DEFAULT_LOGIN_RESULT_PATH) 128 | ->end() 129 | ->end() 130 | ->end() 131 | ->scalarNode('options_handler') 132 | ->defaultValue(self::DEFAULT_REQUEST_OPTIONS_HANDLER_SERVICE) 133 | ->end() 134 | ->end() 135 | ->end() 136 | ->arrayNode('registration') 137 | ->canBeEnabled() 138 | ->children() 139 | ->scalarNode('profile') 140 | ->defaultValue('default') 141 | ->end() 142 | ->arrayNode('routes') 143 | ->addDefaultsIfNotSet() 144 | ->children() 145 | ->scalarNode('host') 146 | ->defaultNull() 147 | ->end() 148 | ->scalarNode('options_path') 149 | ->defaultValue(self::DEFAULT_REGISTER_OPTIONS_PATH) 150 | ->end() 151 | ->scalarNode('result_path') 152 | ->defaultValue(self::DEFAULT_REGISTER_RESULT_PATH) 153 | ->end() 154 | ->end() 155 | ->end() 156 | ->scalarNode('options_handler') 157 | ->defaultValue(self::DEFAULT_CREATION_OPTIONS_HANDLER_SERVICE) 158 | ->end() 159 | ->end() 160 | ->end() 161 | ->end() 162 | ; 163 | } 164 | 165 | /** 166 | * Creates the authenticator service(s) for the provided configuration. 167 | * 168 | * @return string|string[] The authenticator service ID(s) to be used by the firewall 169 | */ 170 | public function createAuthenticator( 171 | ContainerBuilder $container, 172 | string $firewallName, 173 | array $config, 174 | string $userProviderId 175 | ): string|array { 176 | $firewallConfigId = $this->servicesFactory->createWebauthnFirewallConfig($container, $firewallName, $config); 177 | $successHandlerId = $this->servicesFactory->createSuccessHandler($container, $firewallName, $config,); 178 | $failureHandlerId = $this->servicesFactory->createFailureHandler($container, $firewallName, $config,); 179 | 180 | $this->createAssertionControllersAndRoutes($container, $firewallName, $config); 181 | $this->createAttestationControllersAndRoutes($container, $firewallName, $config); 182 | 183 | return $this->createAuthenticatorService( 184 | $container, 185 | $firewallName, 186 | $userProviderId, 187 | $successHandlerId, 188 | $failureHandlerId, 189 | $firewallConfigId, 190 | $config['options_storage'], 191 | $config['secured_rp_ids'] 192 | ); 193 | } 194 | 195 | /** 196 | * Creates the firewall listener services for the provided configuration. 197 | * 198 | * @return string[] The listener service IDs to be used by the firewall 199 | */ 200 | public function createListeners(ContainerBuilder $container, string $firewallName, array $config): array 201 | { 202 | return []; 203 | } 204 | 205 | /** 206 | * @param string[] $securedRpIds 207 | */ 208 | private function createAuthenticatorService( 209 | ContainerBuilder $container, 210 | string $firewallName, 211 | string $userProviderId, 212 | string $successHandlerId, 213 | string $failureHandlerId, 214 | string $firewallConfigId, 215 | string $optionsStorageId, 216 | array $securedRpIds 217 | ): string { 218 | $authenticatorId = self::AUTHENTICATOR_ID_PREFIX . $firewallName; 219 | $container 220 | ->setDefinition($authenticatorId, new ChildDefinition(self::AUTHENTICATOR_DEFINITION_ID)) 221 | ->replaceArgument(0, new Reference($firewallConfigId)) 222 | ->replaceArgument(1, new Reference($userProviderId)) 223 | ->replaceArgument(2, new Reference($successHandlerId)) 224 | ->replaceArgument(3, new Reference($failureHandlerId)) 225 | ->replaceArgument(4, new Reference($optionsStorageId)) 226 | ->replaceArgument(5, $securedRpIds) 227 | ->addMethodCall('setLogger', [new Reference('webauthn.logger')]) 228 | ; 229 | 230 | return $authenticatorId; 231 | } 232 | 233 | /** 234 | * @param mixed[] $config 235 | */ 236 | private function createAssertionControllersAndRoutes( 237 | ContainerBuilder $container, 238 | string $firewallName, 239 | array $config 240 | ): void { 241 | if ($config['authentication']['enabled'] === false) { 242 | return; 243 | } 244 | 245 | $this->createAssertionRequestControllerAndRoute( 246 | $container, 247 | $firewallName, 248 | $config['authentication']['routes']['options_path'], 249 | $config['authentication']['routes']['host'], 250 | $config['authentication']['profile'], 251 | $config['options_storage'], 252 | $config['authentication']['options_handler'], 253 | $config['failure_handler'], 254 | ); 255 | $this->createResponseControllerAndRoute( 256 | $container, 257 | $firewallName, 258 | 'request', 259 | $config['authentication']['routes']['result_path'], 260 | $config['authentication']['routes']['host'] 261 | ); 262 | } 263 | 264 | /** 265 | * @param mixed[] $config 266 | */ 267 | private function createAttestationControllersAndRoutes( 268 | ContainerBuilder $container, 269 | string $firewallName, 270 | array $config 271 | ): void { 272 | if ($config['registration']['enabled'] === false) { 273 | return; 274 | } 275 | 276 | $this->createAttestationRequestControllerAndRoute( 277 | $container, 278 | $firewallName, 279 | $config['registration']['routes']['options_path'], 280 | $config['registration']['routes']['host'], 281 | $config['registration']['profile'], 282 | $config['options_storage'], 283 | $config['registration']['options_handler'], 284 | $config['failure_handler'], 285 | ); 286 | $this->createResponseControllerAndRoute( 287 | $container, 288 | $firewallName, 289 | 'creation', 290 | $config['registration']['routes']['result_path'], 291 | $config['registration']['routes']['host'] 292 | ); 293 | } 294 | 295 | private function createAssertionRequestControllerAndRoute( 296 | ContainerBuilder $container, 297 | string $firewallName, 298 | string $path, 299 | ?string $host, 300 | string $profile, 301 | string $optionsStorageId, 302 | string $optionsHandlerId, 303 | string $failureHandlerId, 304 | ): void { 305 | $controller = (new Definition(AssertionRequestController::class)) 306 | ->setFactory([new Reference(AssertionControllerFactory::class), 'createAssertionRequestController']) 307 | ->setArguments([ 308 | $profile, 309 | new Reference($optionsStorageId), 310 | new Reference($optionsHandlerId), 311 | new Reference($failureHandlerId), 312 | ]) 313 | ; 314 | $this->createControllerAndRoute($container, $controller, 'request', 'options', $firewallName, $path, $host); 315 | } 316 | 317 | private function createAttestationRequestControllerAndRoute( 318 | ContainerBuilder $container, 319 | string $firewallName, 320 | string $path, 321 | ?string $host, 322 | string $profile, 323 | string $optionsStorageId, 324 | string $optionsHandlerId, 325 | string $failureHandlerId, 326 | ): void { 327 | $controller = (new Definition(AttestationRequestController::class)) 328 | ->setFactory([new Reference(AttestationControllerFactory::class), 'createAttestationRequestController']) 329 | ->setArguments([ 330 | new Reference(RequestBodyUserEntityGuesser::class), 331 | $profile, 332 | new Reference($optionsStorageId), 333 | new Reference($optionsHandlerId), 334 | new Reference($failureHandlerId), 335 | ]) 336 | ; 337 | $this->createControllerAndRoute($container, $controller, 'creation', 'options', $firewallName, $path, $host); 338 | } 339 | 340 | private function createResponseControllerAndRoute( 341 | ContainerBuilder $container, 342 | string $firewallName, 343 | string $action, 344 | string $path, 345 | ?string $host 346 | ): void { 347 | $controller = (new Definition(DummyController::class)) 348 | ->setFactory([new Reference(DummyControllerFactory::class), 'create']) 349 | 350 | ; 351 | $this->createControllerAndRoute($container, $controller, $action, 'result', $firewallName, $path, $host); 352 | } 353 | 354 | private function createControllerAndRoute( 355 | ContainerBuilder $container, 356 | Definition $controller, 357 | string $name, 358 | string $operation, 359 | string $firewallName, 360 | string $path, 361 | ?string $host 362 | ): void { 363 | $controller 364 | ->addTag('controller.service_arguments') 365 | ->addTag(DynamicRouteCompilerPass::TAG, [ 366 | 'path' => $path, 367 | 'host' => $host, 368 | ]) 369 | ->setPublic(true) 370 | ; 371 | 372 | $controllerId = sprintf('webauthn.controller.security.%s.%s.%s', $firewallName, $name, $operation); 373 | 374 | $container->setDefinition($controllerId, $controller); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/DependencyInjection/Factory/Security/WebauthnServicesFactory.php: -------------------------------------------------------------------------------- 1 | $config 19 | */ 20 | public function createWebauthnFirewallConfig( 21 | ContainerBuilder $container, 22 | string $firewallName, 23 | array $config 24 | ): string { 25 | $firewallConfigId = WebauthnFactory::FIREWALL_CONFIG_ID_PREFIX . $firewallName; 26 | $container 27 | ->setDefinition($firewallConfigId, new ChildDefinition(WebauthnFactory::FIREWALL_CONFIG_DEFINITION_ID)) 28 | ->replaceArgument(0, $config) 29 | ->replaceArgument(1, $firewallName) 30 | ; 31 | 32 | return $firewallConfigId; 33 | } 34 | 35 | /** 36 | * @param array $config 37 | */ 38 | public function createSuccessHandler( 39 | ContainerBuilder $container, 40 | string $firewallName, 41 | array $config, 42 | ): string { 43 | $successHandlerId = WebauthnFactory::SUCCESS_HANDLER_ID_PREFIX . $firewallName; 44 | $container 45 | ->setDefinition($successHandlerId, new ChildDefinition($config['success_handler'])) 46 | ; 47 | 48 | return $successHandlerId; 49 | } 50 | 51 | /** 52 | * @param array $config 53 | */ 54 | public function createFailureHandler( 55 | ContainerBuilder $container, 56 | string $firewallName, 57 | array $config, 58 | ): string { 59 | $failureHandlerId = WebauthnFactory::FAILURE_HANDLER_ID_PREFIX . $firewallName; 60 | $container 61 | ->setDefinition($failureHandlerId, new ChildDefinition($config['failure_handler'])) 62 | ; 63 | 64 | return $failureHandlerId; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/DependencyInjection/WebauthnExtension.php: -------------------------------------------------------------------------------- 1 | alias; 55 | } 56 | 57 | public function load(array $configs, ContainerBuilder $container): void 58 | { 59 | $processor = new Processor(); 60 | $config = $processor->processConfiguration( 61 | $this->getConfiguration($configs, $container) ?? new Configuration($this->alias), 62 | $configs 63 | ); 64 | 65 | $container->registerForAutoconfiguration(AttestationStatementSupport::class)->addTag( 66 | AttestationStatementSupportCompilerPass::TAG 67 | ); 68 | $container->registerForAutoconfiguration(ExtensionOutputChecker::class)->addTag( 69 | ExtensionOutputCheckerCompilerPass::TAG 70 | ); 71 | $container->registerForAutoconfiguration(Algorithm::class)->addTag(CoseAlgorithmCompilerPass::TAG); 72 | 73 | $container->setAlias('webauthn.http_message_factory', $config['http_message_factory']); 74 | $container->setAlias('webauthn.request_factory', $config['request_factory']); 75 | $container->setAlias('webauthn.http_client', $config['http_client']); 76 | $container->setAlias('webauthn.logger', $config['logger']); 77 | 78 | $container->setAlias(PublicKeyCredentialSourceRepository::class, $config['credential_repository']); 79 | $container->setAlias(PublicKeyCredentialUserEntityRepository::class, $config['user_repository']); 80 | 81 | $container->setAlias(TokenBindingHandler::class, $config['token_binding_support_handler']); 82 | $container->setAlias(CounterChecker::class, $config['counter_checker']); 83 | 84 | $loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/')); 85 | $this->loadAndroidSafetyNet($container, $loader, $config['android_safetynet']); 86 | $this->loadMetadataServices($container, $loader, $config['metadata']); 87 | $this->loadControllersSupport($container, $loader, $config['controllers']); 88 | 89 | $container->setParameter('webauthn.creation_profiles', $config['creation_profiles']); 90 | $container->setParameter('webauthn.request_profiles', $config['request_profiles']); 91 | 92 | $loader->load('services.php'); 93 | $loader->load('cose.php'); 94 | $loader->load('security.php'); 95 | 96 | if ($container->getParameter('kernel.debug') === true) { 97 | $loader->load('dev_services.php'); 98 | } 99 | } 100 | 101 | public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface 102 | { 103 | return new Configuration($this->alias); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function prepend(ContainerBuilder $container): void 110 | { 111 | $bundles = $container->getParameter('kernel.bundles'); 112 | if (! is_array($bundles) || ! array_key_exists('DoctrineBundle', $bundles)) { 113 | return; 114 | } 115 | $configs = $container->getExtensionConfig('doctrine'); 116 | if (count($configs) === 0) { 117 | return; 118 | } 119 | $config = current($configs); 120 | if (! isset($config['dbal'])) { 121 | $config['dbal'] = []; 122 | } 123 | if (! isset($config['dbal']['types'])) { 124 | $config['dbal']['types'] = []; 125 | } 126 | $config['dbal']['types'] += [ 127 | 'attested_credential_data' => DbalType\AttestedCredentialDataType::class, 128 | 'aaguid' => DbalType\AAGUIDDataType::class, 129 | 'base64' => DbalType\Base64BinaryDataType::class, 130 | 'public_key_credential_descriptor' => DbalType\PublicKeyCredentialDescriptorType::class, 131 | 'public_key_credential_descriptor_collection' => DbalType\PublicKeyCredentialDescriptorCollectionType::class, 132 | 'trust_path' => DbalType\TrustPathDataType::class, 133 | ]; 134 | $container->prependExtensionConfig('doctrine', $config); 135 | } 136 | 137 | /** 138 | * @param mixed[] $config 139 | */ 140 | private function loadControllersSupport(ContainerBuilder $container, FileLoader $loader, array $config): void 141 | { 142 | if ($config['enabled'] === false) { 143 | return; 144 | } 145 | 146 | $loader->load('controller.php'); 147 | $this->loadCreationControllersSupport($container, $config['creation'] ?? []); 148 | $this->loadRequestControllersSupport($container, $config['request'] ?? []); 149 | } 150 | 151 | /** 152 | * @param mixed[] $config 153 | */ 154 | private function loadCreationControllersSupport(ContainerBuilder $container, array $config): void 155 | { 156 | foreach ($config as $name => $creationConfig) { 157 | $attestationRequestControllerId = sprintf('webauthn.controller.creation.request.%s', $name); 158 | $attestationRequestController = (new Definition(AttestationRequestController::class)) 159 | ->setFactory( 160 | [new Reference(AttestationControllerFactory::class), 'createAttestationRequestController'] 161 | ) 162 | ->setArguments([ 163 | new Reference($creationConfig['user_entity_guesser']), 164 | $creationConfig['profile'], 165 | new Reference($creationConfig['options_storage']), 166 | new Reference($creationConfig['options_handler']), 167 | new Reference($creationConfig['failure_handler']), 168 | ]) 169 | ->addTag(DynamicRouteCompilerPass::TAG, [ 170 | 'path' => $creationConfig['options_path'], 171 | 'host' => $creationConfig['host'], 172 | ]) 173 | ->addTag('controller.service_arguments') 174 | ; 175 | $container->setDefinition($attestationRequestControllerId, $attestationRequestController); 176 | 177 | $attestationResponseControllerId = sprintf('webauthn.controller.creation.response.%s', $name); 178 | $attestationResponseController = new Definition(AttestationResponseController::class); 179 | $attestationResponseController->setFactory( 180 | [new Reference(AttestationControllerFactory::class), 'createAttestationResponseController'] 181 | ); 182 | $attestationResponseController->setArguments([ 183 | new Reference($creationConfig['options_storage']), 184 | new Reference($creationConfig['success_handler']), 185 | new Reference($creationConfig['failure_handler']), 186 | $creationConfig['secured_rp_ids'], 187 | ]); 188 | $attestationResponseController->addTag(DynamicRouteCompilerPass::TAG, [ 189 | 'path' => $creationConfig['result_path'], 190 | 'host' => $creationConfig['host'], 191 | ]); 192 | $attestationResponseController->addTag('controller.service_arguments'); 193 | $container->setDefinition($attestationResponseControllerId, $attestationResponseController); 194 | } 195 | } 196 | 197 | /** 198 | * @param mixed[] $config 199 | */ 200 | private function loadRequestControllersSupport(ContainerBuilder $container, array $config): void 201 | { 202 | foreach ($config as $name => $requestConfig) { 203 | $assertionRequestControllerId = sprintf('webauthn.controller.request.request.%s', $name); 204 | $assertionRequestController = (new Definition(AssertionRequestController::class)) 205 | ->setFactory([new Reference(AssertionControllerFactory::class), 'createAssertionRequestController']) 206 | ->setArguments([ 207 | $requestConfig['profile'], 208 | new Reference($requestConfig['options_storage']), 209 | new Reference($requestConfig['options_handler']), 210 | new Reference($requestConfig['failure_handler']), 211 | ]) 212 | ->addTag(DynamicRouteCompilerPass::TAG, [ 213 | 'path' => $requestConfig['options_path'], 214 | 'host' => $requestConfig['host'], 215 | ]) 216 | ->addTag('controller.service_arguments') 217 | ; 218 | $container->setDefinition($assertionRequestControllerId, $assertionRequestController); 219 | 220 | $assertionResponseControllerId = sprintf('webauthn.controller.request.response.%s', $name); 221 | $assertionResponseController = new Definition(AssertionResponseController::class); 222 | $assertionResponseController->setFactory( 223 | [new Reference(AssertionControllerFactory::class), 'createAssertionResponseController'] 224 | ); 225 | $assertionResponseController->setArguments([ 226 | new Reference($requestConfig['options_storage']), 227 | new Reference($requestConfig['success_handler']), 228 | new Reference($requestConfig['failure_handler']), 229 | $requestConfig['secured_rp_ids'], 230 | ]); 231 | $assertionResponseController->addTag(DynamicRouteCompilerPass::TAG, [ 232 | 'path' => $requestConfig['result_path'], 233 | 'host' => $requestConfig['host'], 234 | ]); 235 | $assertionResponseController->addTag('controller.service_arguments'); 236 | $container->setDefinition($assertionResponseControllerId, $assertionResponseController); 237 | } 238 | } 239 | 240 | /** 241 | * @param mixed[] $config 242 | */ 243 | private function loadAndroidSafetyNet(ContainerBuilder $container, FileLoader $loader, array $config): void 244 | { 245 | //Android SafetyNet 246 | $container->setParameter('webauthn.android_safetynet.leeway', $config['leeway']); 247 | $container->setParameter('webauthn.android_safetynet.max_age', $config['max_age']); 248 | $container->setParameter('webauthn.android_safetynet.api_key', $config['api_key']); 249 | $loader->load('android_safetynet.php'); 250 | } 251 | 252 | /** 253 | * @param mixed[] $config 254 | */ 255 | private function loadMetadataServices(ContainerBuilder $container, FileLoader $loader, array $config): void 256 | { 257 | if ($config['enabled'] === false) { 258 | return; 259 | } 260 | $container->setAlias(MetadataStatementRepository::class, $config['mds_repository']); 261 | $container->setAlias(StatusReportRepository::class, $config['status_report_repository']); 262 | $container->setAlias(CertificateChainChecker::class, $config['certificate_chain_checker']); 263 | $loader->load('metadata_statement_supports.php'); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Doctrine/Type/AAGUIDDataType.php: -------------------------------------------------------------------------------- 1 | __toString(); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?AbstractUid 30 | { 31 | if ($value instanceof AbstractUid || mb_strlen((string) $value, '8bit') !== 36) { 32 | return $value; 33 | } 34 | 35 | return Uuid::fromString($value); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getSQLDeclaration(array $column, AbstractPlatform $platform): string 42 | { 43 | return $platform->getClobTypeDeclarationSQL($column); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string 50 | { 51 | return 'aaguid'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 58 | { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Doctrine/Type/AttestedCredentialDataType.php: -------------------------------------------------------------------------------- 1 | getJsonTypeDeclarationSQL($fieldDeclaration); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getName(): string 51 | { 52 | return 'attested_credential_data'; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 59 | { 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Doctrine/Type/Base64BinaryDataType.php: -------------------------------------------------------------------------------- 1 | getClobTypeDeclarationSQL($column); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string 50 | { 51 | return 'base64'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 58 | { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Doctrine/Type/PublicKeyCredentialDescriptorCollectionType.php: -------------------------------------------------------------------------------- 1 | getJsonTypeDeclarationSQL($fieldDeclaration); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string 50 | { 51 | return 'public_key_credential_descriptor_collection'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 58 | { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Doctrine/Type/PublicKeyCredentialDescriptorType.php: -------------------------------------------------------------------------------- 1 | getJsonTypeDeclarationSQL($fieldDeclaration); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function getName(): string 50 | { 51 | return 'public_key_credential_descriptor'; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 58 | { 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Doctrine/Type/TrustPathDataType.php: -------------------------------------------------------------------------------- 1 | getJsonTypeDeclarationSQL($fieldDeclaration); 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getName(): string 52 | { 53 | return 'trust_path'; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function requiresSQLCommentHint(AbstractPlatform $platform): bool 60 | { 61 | return true; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Dto/AdditionalPublicKeyCredentialCreationOptionsRequest.php: -------------------------------------------------------------------------------- 1 | |null 15 | */ 16 | public ?array $authenticatorSelection = null; 17 | 18 | #[Type(type: 'string')] 19 | #[Choice(choices: [ 20 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, 21 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT, 22 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT, 23 | ])] 24 | public string $attestation = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE; 25 | 26 | /** 27 | * @var array|null 28 | */ 29 | public ?array $extensions = null; 30 | } 31 | -------------------------------------------------------------------------------- /src/Dto/ServerPublicKeyCredentialCreationOptionsRequest.php: -------------------------------------------------------------------------------- 1 | |null 24 | */ 25 | public ?array $authenticatorSelection = null; 26 | 27 | #[Type(type: 'string')] 28 | #[Choice(choices: [ 29 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE, 30 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT, 31 | PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT, 32 | ])] 33 | public string $attestation = PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE; 34 | 35 | /** 36 | * @var array|null 37 | */ 38 | public ?array $extensions = null; 39 | } 40 | -------------------------------------------------------------------------------- /src/Dto/ServerPublicKeyCredentialRequestOptionsRequest.php: -------------------------------------------------------------------------------- 1 | |null 29 | */ 30 | public ?array $extensions = null; 31 | } 32 | -------------------------------------------------------------------------------- /src/Event/AuthenticatorAssertionResponseValidationFailedEvent.php: -------------------------------------------------------------------------------- 1 | credentialId; 28 | } 29 | 30 | public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse 31 | { 32 | return $this->authenticatorAssertionResponse; 33 | } 34 | 35 | public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions 36 | { 37 | return $this->publicKeyCredentialRequestOptions; 38 | } 39 | 40 | public function getRequest(): ServerRequestInterface 41 | { 42 | return $this->request; 43 | } 44 | 45 | public function getUserHandle(): ?string 46 | { 47 | return $this->userHandle; 48 | } 49 | 50 | public function getThrowable(): Throwable 51 | { 52 | return $this->throwable; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php: -------------------------------------------------------------------------------- 1 | credentialId; 28 | } 29 | 30 | public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse 31 | { 32 | return $this->authenticatorAssertionResponse; 33 | } 34 | 35 | public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions 36 | { 37 | return $this->publicKeyCredentialRequestOptions; 38 | } 39 | 40 | public function getRequest(): ServerRequestInterface 41 | { 42 | return $this->request; 43 | } 44 | 45 | public function getUserHandle(): ?string 46 | { 47 | return $this->userHandle; 48 | } 49 | 50 | public function getPublicKeyCredentialSource(): PublicKeyCredentialSource 51 | { 52 | return $this->publicKeyCredentialSource; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php: -------------------------------------------------------------------------------- 1 | authenticatorAttestationResponse; 26 | } 27 | 28 | public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions 29 | { 30 | return $this->publicKeyCredentialCreationOptions; 31 | } 32 | 33 | public function getRequest(): ServerRequestInterface 34 | { 35 | return $this->request; 36 | } 37 | 38 | public function getThrowable(): Throwable 39 | { 40 | return $this->throwable; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php: -------------------------------------------------------------------------------- 1 | authenticatorAttestationResponse; 26 | } 27 | 28 | public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions 29 | { 30 | return $this->publicKeyCredentialCreationOptions; 31 | } 32 | 33 | public function getRequest(): ServerRequestInterface 34 | { 35 | return $this->request; 36 | } 37 | 38 | public function getPublicKeyCredentialSource(): PublicKeyCredentialSource 39 | { 40 | return $this->publicKeyCredentialSource; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Event/PublicKeyCredentialCreationOptionsCreatedEvent.php: -------------------------------------------------------------------------------- 1 | publicKeyCredentialCreationOptions; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Event/PublicKeyCredentialRequestOptionsCreatedEvent.php: -------------------------------------------------------------------------------- 1 | publicKeyCredentialRequestOptions; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Repository/DummyPublicKeyCredentialSourceRepository.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 27 | } 28 | 29 | public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void 30 | { 31 | $this->logger->critical( 32 | 'Please change the Public Key Credential Source Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 33 | ); 34 | throw new LogicException( 35 | 'You are using the DummyPublicKeyCredentialSourceRepository service. Please create your own repository' 36 | ); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array 43 | { 44 | $this->logger->critical( 45 | 'Please change the Public Key Credential Source Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 46 | ); 47 | throw new LogicException( 48 | 'You are using the DummyPublicKeyCredentialSourceRepository service. Please create your own repository' 49 | ); 50 | } 51 | 52 | public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource 53 | { 54 | $this->logger->critical( 55 | 'Please change the Public Key Credential Source Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 56 | ); 57 | throw new LogicException( 58 | 'You are using the DummyPublicKeyCredentialSourceRepository service. Please create your own repository' 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Repository/DummyPublicKeyCredentialUserEntityRepository.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 25 | } 26 | 27 | public function findOneByUsername(string $username): ?PublicKeyCredentialUserEntity 28 | { 29 | $this->logger->critical( 30 | 'Please change the Public Key Credential User Entity Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 31 | ); 32 | throw new LogicException( 33 | 'You are using the DummyPublicKeyCredentialUserEntityRepository service. Please create your own repository' 34 | ); 35 | } 36 | 37 | public function findOneByUserHandle(string $userHandle): ?PublicKeyCredentialUserEntity 38 | { 39 | $this->logger->critical( 40 | 'Please change the Public Key Credential User Entity Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 41 | ); 42 | throw new LogicException( 43 | 'You are using the DummyPublicKeyCredentialUserEntityRepository service. Please create your own repository' 44 | ); 45 | } 46 | 47 | public function generateNextUserEntityId(): string 48 | { 49 | $this->logger->critical( 50 | 'Please change the Public Key Credential User Entity Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 51 | ); 52 | throw new LogicException( 53 | 'You are using the DummyPublicKeyCredentialUserEntityRepository service. Please create your own repository' 54 | ); 55 | } 56 | 57 | public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void 58 | { 59 | $this->logger->critical( 60 | 'Please change the Public Key Credential User Entity Repository in the bundle configuration. See https://webauthn-doc.spomky-labs.com/the-webauthn-server/the-symfony-way#repositories-1' 61 | ); 62 | throw new LogicException( 63 | 'You are using the DummyPublicKeyCredentialUserEntityRepository service. Please create your own repository' 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Repository/PublicKeyCredentialSourceRepository.php: -------------------------------------------------------------------------------- 1 | getManagerForClass($class); 28 | Assertion::isInstanceOf($manager, EntityManagerInterface::class, sprintf( 29 | 'Could not find the entity manager for class "%s". Check your Doctrine configuration to make sure it is configured to load this entity’s metadata.', 30 | $class 31 | )); 32 | 33 | $this->class = $class; 34 | $this->manager = $manager; 35 | } 36 | 37 | public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void 38 | { 39 | $this->manager->persist($publicKeyCredentialSource); 40 | $this->manager->flush(); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array 47 | { 48 | $qb = $this->manager->createQueryBuilder(); 49 | 50 | return $qb->select('c') 51 | ->from($this->getClass(), 'c') 52 | ->where('c.userHandle = :userHandle') 53 | ->setParameter(':userHandle', $publicKeyCredentialUserEntity->getId()) 54 | ->getQuery() 55 | ->execute() 56 | ; 57 | } 58 | 59 | public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource 60 | { 61 | $qb = $this->manager->createQueryBuilder(); 62 | 63 | return $qb->select('c') 64 | ->from($this->getClass(), 'c') 65 | ->where('c.publicKeyCredentialId = :publicKeyCredentialId') 66 | ->setParameter(':publicKeyCredentialId', base64_encode($publicKeyCredentialId)) 67 | ->setMaxResults(1) 68 | ->getQuery() 69 | ->getOneOrNullResult() 70 | ; 71 | } 72 | 73 | protected function getClass(): string 74 | { 75 | return $this->class; 76 | } 77 | 78 | protected function getEntityManager(): EntityManagerInterface 79 | { 80 | return $this->manager; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Repository/PublicKeyCredentialUserEntityRepository.php: -------------------------------------------------------------------------------- 1 | services() 12 | ->defaults() 13 | ->private() 14 | ->autoconfigure() 15 | ; 16 | 17 | if (class_exists(JWKFactory::class) && class_exists(RS256::class)) { 18 | $container 19 | ->set(AndroidSafetyNetAttestationStatementSupport::class) 20 | ->call('setMaxAge', ['%webauthn.android_safetynet.max_age%']) 21 | ->call('setLeeway', ['%webauthn.android_safetynet.leeway%']) 22 | ; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/Resources/config/controller.php: -------------------------------------------------------------------------------- 1 | services() 19 | ->defaults() 20 | ->private() 21 | ->autoconfigure() 22 | ; 23 | 24 | $container 25 | ->set(AttestationControllerFactory::class) 26 | ->args([ 27 | service('webauthn.http_message_factory'), 28 | service(SerializerInterface::class), 29 | service(ValidatorInterface::class), 30 | service(PublicKeyCredentialCreationOptionsFactory::class), 31 | service(PublicKeyCredentialLoader::class), 32 | service(AuthenticatorAttestationResponseValidator::class), 33 | service(PublicKeyCredentialSourceRepository::class), 34 | ]) 35 | ; 36 | $container->set(DefaultFailureHandler::class); 37 | $container->set(DefaultSuccessHandler::class); 38 | }; 39 | -------------------------------------------------------------------------------- /src/Resources/config/cose.php: -------------------------------------------------------------------------------- 1 | services() 24 | ->defaults() 25 | ->private() 26 | ->autoconfigure() 27 | ; 28 | 29 | $container 30 | ->set('webauthn.cose.algorithm.manager') 31 | ->class(Manager::class) 32 | ; 33 | 34 | $container 35 | ->set('webauthn.cose.algoritm.RS1') 36 | ->class(RS1::class) 37 | ; 38 | $container 39 | ->set('webauthn.cose.algoritm.RS256') 40 | ->class(RS256::class) 41 | ; 42 | $container 43 | ->set('webauthn.cose.algoritm.RS384') 44 | ->class(RS384::class) 45 | ; 46 | $container 47 | ->set('webauthn.cose.algoritm.RS512') 48 | ->class(RS512::class) 49 | ; 50 | 51 | $container 52 | ->set('webauthn.cose.algoritm.PS256') 53 | ->class(PS256::class) 54 | ; 55 | $container 56 | ->set('webauthn.cose.algoritm.PS384') 57 | ->class(PS384::class) 58 | ; 59 | $container 60 | ->set('webauthn.cose.algoritm.PS512') 61 | ->class(PS512::class) 62 | ; 63 | 64 | $container 65 | ->set('webauthn.cose.algoritm.ES256K') 66 | ->class(ES256K::class) 67 | ; 68 | $container 69 | ->set('webauthn.cose.algoritm.ES256') 70 | ->class(ES256::class) 71 | ; 72 | $container 73 | ->set('webauthn.cose.algoritm.ES384') 74 | ->class(ES384::class) 75 | ; 76 | $container 77 | ->set('webauthn.cose.algoritm.ES512') 78 | ->class(ES512::class) 79 | ; 80 | 81 | $container 82 | ->set('webauthn.cose.algoritm.ED256') 83 | ->class(Ed256::class) 84 | ; 85 | $container 86 | ->set('webauthn.cose.algoritm.ED512') 87 | ->class(Ed512::class) 88 | ; 89 | $container 90 | ->set('webauthn.cose.algoritm.Ed25519ph') 91 | ->class(Ed25519::class) 92 | ; 93 | }; 94 | -------------------------------------------------------------------------------- /src/Resources/config/dev_services.php: -------------------------------------------------------------------------------- 1 | services() 11 | ->defaults() 12 | ->private() 13 | ->autoconfigure() 14 | ; 15 | 16 | $container->set(WebauthnCollector::class) 17 | ->tag('data_collector', [ 18 | 'id' => 'webauthn_collector', 19 | 'template' => '@Webauthn/data_collector/template.html.twig', 20 | ]) 21 | ; 22 | }; 23 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-mapping/PublicKeyCredentialEntity.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-mapping/PublicKeyCredentialSource.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Resources/config/doctrine-mapping/PublicKeyCredentialUserEntity.orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Resources/config/metadata_statement_supports.php: -------------------------------------------------------------------------------- 1 | services() 16 | ->defaults() 17 | ->private() 18 | ->autoconfigure() 19 | ; 20 | 21 | $container 22 | ->set(AppleAttestationStatementSupport::class) 23 | ; 24 | $container 25 | ->set(TPMAttestationStatementSupport::class) 26 | ; 27 | $container 28 | ->set(FidoU2FAttestationStatementSupport::class) 29 | ; 30 | $container 31 | ->set(AndroidKeyAttestationStatementSupport::class) 32 | ; 33 | $container 34 | ->set(PackedAttestationStatementSupport::class) 35 | ->args([service('webauthn.cose.algorithm.manager')]) 36 | ; 37 | 38 | $container 39 | ->set(PhpCertificateChainChecker::class) 40 | ->args([service('webauthn.http_client'), service('webauthn.request_factory')]) 41 | ; 42 | }; 43 | -------------------------------------------------------------------------------- /src/Resources/config/routing.php: -------------------------------------------------------------------------------- 1 | import('.', 'webauthn'); 9 | }; 10 | -------------------------------------------------------------------------------- /src/Resources/config/security.php: -------------------------------------------------------------------------------- 1 | services() 31 | ->defaults() 32 | ->private() 33 | ->autoconfigure() 34 | ; 35 | 36 | $container 37 | ->set(IsUserPresentVoter::class) 38 | ->tag('security.voter') 39 | ; 40 | 41 | $container 42 | ->set(IsUserVerifiedVoter::class) 43 | ->tag('security.voter') 44 | ; 45 | 46 | $container 47 | ->set(DefaultSuccessHandler::class) 48 | ; 49 | 50 | $container 51 | ->set(DefaultFailureHandler::class) 52 | ; 53 | 54 | $container 55 | ->set(SessionStorage::class) 56 | ->args([service('request_stack')]) 57 | ; 58 | 59 | $container 60 | ->set(DefaultCreationOptionsHandler::class) 61 | ; 62 | 63 | $container 64 | ->set(DefaultRequestOptionsHandler::class) 65 | ; 66 | 67 | $container 68 | ->set(WebauthnFactory::AUTHENTICATOR_DEFINITION_ID, WebauthnAuthenticator::class) 69 | ->abstract() 70 | ->args([ 71 | abstract_arg('Firewall config'), 72 | abstract_arg('User provider'), 73 | abstract_arg('Success handler'), 74 | abstract_arg('Failure handler'), 75 | abstract_arg('Options Storage'), 76 | abstract_arg('Secured Relying Party IDs'), 77 | service('webauthn.http_message_factory'), 78 | service(PublicKeyCredentialSourceRepository::class), 79 | service(PublicKeyCredentialUserEntityRepository::class), 80 | service(PublicKeyCredentialLoader::class), 81 | service(AuthenticatorAssertionResponseValidator::class), 82 | service(AuthenticatorAttestationResponseValidator::class), 83 | ]) 84 | ; 85 | 86 | $container 87 | ->set(WebauthnFactory::FIREWALL_CONFIG_DEFINITION_ID, WebauthnFirewallConfig::class) 88 | ->abstract() 89 | ->args([ 90 | [], // Firewall settings 91 | abstract_arg('Firewall name'), 92 | service('security.http_utils'), 93 | ]) 94 | ; 95 | 96 | $container 97 | ->set(CurrentUserEntityGuesser::class) 98 | ->args([service(TokenStorageInterface::class), service(PublicKeyCredentialUserEntityRepository::class)]) 99 | ; 100 | $container 101 | ->set(RequestBodyUserEntityGuesser::class) 102 | ->args([ 103 | service(SerializerInterface::class), 104 | service(ValidatorInterface::class), 105 | service(PublicKeyCredentialUserEntityRepository::class), 106 | ]) 107 | ; 108 | }; 109 | -------------------------------------------------------------------------------- /src/Resources/config/services.php: -------------------------------------------------------------------------------- 1 | services() 45 | ->defaults() 46 | ->private() 47 | ->autoconfigure() 48 | ; 49 | 50 | $container 51 | ->set(BaseAuthenticatorAttestationResponseValidator::class) 52 | ->class(AuthenticatorAttestationResponseValidator::class) 53 | ->args([ 54 | service(AttestationStatementSupportManager::class), 55 | service(PublicKeyCredentialSourceRepository::class), 56 | service(TokenBindingHandler::class), 57 | service(ExtensionOutputCheckerHandler::class), 58 | service(EventDispatcherInterface::class), 59 | ]) 60 | ->public() 61 | ; 62 | $container 63 | ->set(BaseAuthenticatorAssertionResponseValidator::class) 64 | ->class(AuthenticatorAssertionResponseValidator::class) 65 | ->args([ 66 | service(PublicKeyCredentialSourceRepository::class), 67 | service(TokenBindingHandler::class), 68 | service(ExtensionOutputCheckerHandler::class), 69 | service('webauthn.cose.algorithm.manager'), 70 | service(EventDispatcherInterface::class), 71 | ]) 72 | ->public() 73 | ; 74 | $container 75 | ->set(PublicKeyCredentialLoader::class) 76 | ->args([service(AttestationObjectLoader::class)]) 77 | ->public() 78 | ; 79 | $container 80 | ->set(PublicKeyCredentialCreationOptionsFactory::class) 81 | ->args(['%webauthn.creation_profiles%', service(EventDispatcherInterface::class)]) 82 | ->public() 83 | ; 84 | $container 85 | ->set(PublicKeyCredentialRequestOptionsFactory::class) 86 | ->args(['%webauthn.request_profiles%', service(EventDispatcherInterface::class)]) 87 | ->public() 88 | ; 89 | 90 | $container 91 | ->set(ExtensionOutputCheckerHandler::class) 92 | ; 93 | $container 94 | ->set(AttestationObjectLoader::class) 95 | ->args([service(AttestationStatementSupportManager::class)]) 96 | ; 97 | $container 98 | ->set(AttestationStatementSupportManager::class) 99 | ; 100 | $container 101 | ->set(NoneAttestationStatementSupport::class) 102 | ; 103 | 104 | $container 105 | ->set(IgnoreTokenBindingHandler::class) 106 | ; 107 | $container 108 | ->set(TokenBindingNotSupportedHandler::class) 109 | ; 110 | $container 111 | ->set(SecTokenBindingHandler::class) 112 | ; 113 | 114 | $container 115 | ->set(ThrowExceptionIfInvalid::class) 116 | ->autowire(false) 117 | ; 118 | 119 | $container 120 | ->set(Loader::class) 121 | ->tag('routing.loader') 122 | ; 123 | 124 | $container 125 | ->set(AttestationControllerFactory::class) 126 | ->args([ 127 | service('webauthn.http_message_factory'), 128 | service(SerializerInterface::class), 129 | service(ValidatorInterface::class), 130 | service(PublicKeyCredentialCreationOptionsFactory::class), 131 | service(PublicKeyCredentialLoader::class), 132 | service(BaseAuthenticatorAttestationResponseValidator::class), 133 | service(PublicKeyCredentialSourceRepository::class), 134 | ]) 135 | ; 136 | $container 137 | ->set(AssertionControllerFactory::class) 138 | ->args([ 139 | service('webauthn.http_message_factory'), 140 | service(SerializerInterface::class), 141 | service(ValidatorInterface::class), 142 | service(PublicKeyCredentialRequestOptionsFactory::class), 143 | service(PublicKeyCredentialLoader::class), 144 | service(BaseAuthenticatorAssertionResponseValidator::class), 145 | service(PublicKeyCredentialUserEntityRepository::class), 146 | service(PublicKeyCredentialSourceRepository::class), 147 | ]) 148 | ; 149 | 150 | $container 151 | ->set(DummyPublicKeyCredentialSourceRepository::class) 152 | ->autowire(false) 153 | ; 154 | $container 155 | ->set(DummyPublicKeyCredentialUserEntityRepository::class) 156 | ->autowire(false) 157 | ; 158 | 159 | $container 160 | ->set(DummyControllerFactory::class) 161 | ; 162 | 163 | $container 164 | ->set('webauthn.http_message_factory.default') 165 | ->class(PsrHttpFactory::class) 166 | ->args([ 167 | service(ServerRequestFactoryInterface::class), 168 | service(StreamFactoryInterface::class), 169 | service(UploadedFileFactoryInterface::class), 170 | service(ResponseFactoryInterface::class), 171 | ]) 172 | ; 173 | 174 | $container 175 | ->set('webauthn.logger.default') 176 | ->class(NullLogger::class) 177 | ; 178 | 179 | $container 180 | ->alias('webauthn.http_client.default', ClientInterface::class) 181 | ; 182 | 183 | $container 184 | ->alias('webauthn.request_factory.default', RequestFactoryInterface::class) 185 | ; 186 | }; 187 | -------------------------------------------------------------------------------- /src/Resources/views/data_collector/tab/attestation.html.twig: -------------------------------------------------------------------------------- 1 |
2 |

Attestation

3 |
4 |

5 | The following table lists all Public Key Credential Attestation Options generated by the application.
6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% if not collector.getData().publicKeyCredentialCreationOptions is empty %} 15 | {% for id, data in collector.getData().publicKeyCredentialCreationOptions %} 16 | 17 | 26 | 27 | {% endfor %} 28 | {% else %} 29 | 30 | 31 | 32 | {% endif %} 33 | 34 |
Public Key Credential Attestation Options
18 | {{ profiler_dump(data.options) }} 19 |
20 | Show JSON 21 |
22 |
23 |
{{ data.json }}
24 |
25 |
No Public Key Credential Attestation Options have been generated
35 |

36 | The following table lists all Authenticator Attestation validated by the application.
37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% if collector.getData().authenticatorAttestationResponseValidationSucceeded is empty and collector.getData().authenticatorAttestationResponseValidationFailed is empty %} 48 | 49 | 50 | 51 | {% endif %} 52 | {% if not collector.getData().authenticatorAttestationResponseValidationSucceeded is empty %} 53 | {% for id, data in collector.getData().authenticatorAttestationResponseValidationSucceeded %} 54 | 55 | 56 | 65 | 68 | 69 | {% endfor %} 70 | {% endif %} 71 | {% if not collector.getData().authenticatorAttestationResponseValidationFailed is empty %} 72 | {% for id, data in collector.getData().authenticatorAttestationResponseValidationFailed %} 73 | 74 | 75 | 84 | 87 | 88 | {% endfor %} 89 | {% endif %} 90 | 91 |
Attestation ResponseOptionsResult
No Authenticator Attestation have been validated
{{ profiler_dump(data.attestation_response) }} 57 | {{ profiler_dump(data.options) }} 58 |
59 | Show JSON 60 |
61 |
62 |
{{ data.options_json }}
63 |
64 |
66 | {{ profiler_dump(data.credential_source) }} 67 |
{{ profiler_dump(data.attestation_response) }} 76 | {{ profiler_dump(data.options) }} 77 |
78 | Show JSON 79 |
80 |
81 |
{{ data.options_json }}
82 |
83 |
85 | {{ profiler_dump(data.exception) }} 86 |
92 |
93 |
94 | -------------------------------------------------------------------------------- /src/Resources/views/data_collector/tab/request.html.twig: -------------------------------------------------------------------------------- 1 |
2 |

Request

3 |
4 |

5 | The following table lists all Public Key Credential Request Options generated by the application.
6 |

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% if not collector.getData().publicKeyCredentialRequestOptions is empty %} 15 | {% for id, data in collector.getData().publicKeyCredentialRequestOptions %} 16 | 17 | 26 | 27 | {% endfor %} 28 | {% else %} 29 | 30 | 31 | 32 | {% endif %} 33 | 34 |
Public Key Credential Request Options
18 | {{ profiler_dump(data.options) }} 19 |
20 | Show JSON 21 |
22 |
23 |
{{ data.json }}
24 |
25 |
No Public Key Credential Request Options have been generated
35 |

36 | The following table lists all Authenticator Requests validated by the application.
37 |

38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | {% if collector.getData().authenticatorAssertionResponseValidationSucceeded is empty and collector.getData().authenticatorAssertionResponseValidationFailed is empty %} 50 | 51 | 52 | 53 | {% endif %} 54 | {% if not collector.getData().authenticatorAssertionResponseValidationSucceeded is empty %} 55 | {% for id, data in collector.getData().authenticatorAssertionResponseValidationSucceeded %} 56 | 57 | 58 | 59 | 60 | 69 | 72 | 73 | {% endfor %} 74 | {% endif %} 75 | {% if not collector.getData().authenticatorAssertionResponseValidationFailed is empty %} 76 | {% for id, data in collector.getData().authenticatorAssertionResponseValidationFailed %} 77 | 78 | 79 | 80 | 81 | 90 | 93 | 94 | {% endfor %} 95 | {% endif %} 96 | 97 |
User Handle (ID)Credential IDAssertion ResponseOptionsResult
No Authenticator Assertion have been validated
{{ profiler_dump(data.user_handle) }}{{ profiler_dump(data.credential_id) }}{{ profiler_dump(data.assertion_response) }} 61 | {{ profiler_dump(data.options) }} 62 |
63 | Show JSON 64 |
65 |
66 |
{{ data.options_json }}
67 |
68 |
70 | {{ profiler_dump(data.credential_source) }} 71 |
{{ profiler_dump(data.user_handle) }}{{ profiler_dump(data.credential_id) }}{{ profiler_dump(data.assertion_response) }} 82 | {{ profiler_dump(data.options) }} 83 |
84 | Show JSON 85 |
86 |
87 |
{{ data.options_json }}
88 |
89 |
91 | {{ profiler_dump(data.exception) }} 92 |
98 |
99 |
100 | -------------------------------------------------------------------------------- /src/Routing/Loader.php: -------------------------------------------------------------------------------- 1 | routes = new RouteCollection(); 20 | } 21 | 22 | public function add(string $pattern, ?string $host, string $name): void 23 | { 24 | $controllerId = sprintf('%s', $name); 25 | $defaults = [ 26 | '_controller' => $controllerId, 27 | ]; 28 | $route = new Route($pattern, $defaults, [], [], $host, [], [Request::METHOD_POST]); 29 | $this->routes->add($name, $route); 30 | } 31 | 32 | public function load($resource, string $type = null) 33 | { 34 | return $this->routes; 35 | } 36 | 37 | public function supports(mixed $resource, string $type = null): bool 38 | { 39 | return $type === 'webauthn'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Security/Authentication/Exception/WebauthnAuthenticationEvent.php: -------------------------------------------------------------------------------- 1 | 40 | */ 41 | public function __serialize(): array 42 | { 43 | return [ 44 | json_encode($this->publicKeyCredentialUserEntity, JSON_THROW_ON_ERROR), 45 | json_encode($this->publicKeyCredentialDescriptor, JSON_THROW_ON_ERROR), 46 | $this->publicKeyCredentialOptions::class, 47 | json_encode($this->publicKeyCredentialOptions, JSON_THROW_ON_ERROR), 48 | $this->isUserPresent, 49 | $this->isUserVerified, 50 | $this->reservedForFutureUse1, 51 | $this->reservedForFutureUse2, 52 | $this->signCount, 53 | $this->extensions, 54 | $this->firewallName, 55 | parent::__serialize(), 56 | ]; 57 | } 58 | 59 | /** 60 | * @param array $serialized 61 | */ 62 | public function __unserialize(array $serialized): void 63 | { 64 | [ 65 | $publicKeyCredentialUserEntity, 66 | $publicKeyCredentialDescriptor, 67 | $publicKeyCredentialOptionsClass, 68 | $publicKeyCredentialOptions, 69 | $this->isUserPresent, 70 | $this->isUserVerified, 71 | $this->reservedForFutureUse1, 72 | $this->reservedForFutureUse2, 73 | $this->signCount, 74 | $extensions, 75 | $this->firewallName, 76 | $parentData 77 | ] = $serialized; 78 | Assertion::subclassOf( 79 | $publicKeyCredentialOptionsClass, 80 | PublicKeyCredentialOptions::class, 81 | 'Invalid PublicKeyCredentialOptions class' 82 | ); 83 | $this->publicKeyCredentialUserEntity = PublicKeyCredentialUserEntity::createFromString( 84 | $publicKeyCredentialUserEntity 85 | ); 86 | $this->publicKeyCredentialDescriptor = PublicKeyCredentialDescriptor::createFromString( 87 | $publicKeyCredentialDescriptor 88 | ); 89 | $this->publicKeyCredentialOptions = $publicKeyCredentialOptionsClass::createFromString( 90 | $publicKeyCredentialOptions 91 | ); 92 | 93 | $this->extensions = null; 94 | if ($extensions !== null) { 95 | $this->extensions = AuthenticationExtensionsClientOutputs::createFromString($extensions); 96 | } 97 | parent::__unserialize($parentData); 98 | } 99 | 100 | public function getCredentials(): PublicKeyCredentialDescriptor 101 | { 102 | return $this->getPublicKeyCredentialDescriptor(); 103 | } 104 | 105 | public function getPublicKeyCredentialUserEntity(): PublicKeyCredentialUserEntity 106 | { 107 | return $this->publicKeyCredentialUserEntity; 108 | } 109 | 110 | public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor 111 | { 112 | return $this->publicKeyCredentialDescriptor; 113 | } 114 | 115 | public function getPublicKeyCredentialOptions(): PublicKeyCredentialOptions 116 | { 117 | return $this->publicKeyCredentialOptions; 118 | } 119 | 120 | public function isUserPresent(): bool 121 | { 122 | return $this->isUserPresent; 123 | } 124 | 125 | public function isUserVerified(): bool 126 | { 127 | return $this->isUserVerified; 128 | } 129 | 130 | public function getReservedForFutureUse1(): int 131 | { 132 | return $this->reservedForFutureUse1; 133 | } 134 | 135 | public function getReservedForFutureUse2(): int 136 | { 137 | return $this->reservedForFutureUse2; 138 | } 139 | 140 | public function getSignCount(): int 141 | { 142 | return $this->signCount; 143 | } 144 | 145 | public function getExtensions(): ?AuthenticationExtensionsClientOutputs 146 | { 147 | return $this->extensions; 148 | } 149 | 150 | public function getFirewallName(): string 151 | { 152 | return $this->firewallName; 153 | } 154 | 155 | /** 156 | * @return string[] 157 | */ 158 | public function getAttributes(): array 159 | { 160 | $attributes = parent::getAttributes(); 161 | if ($this->isUserVerified) { 162 | $attributes[] = IsUserVerifiedVoter::IS_USER_VERIFIED; 163 | } 164 | if ($this->isUserPresent) { 165 | $attributes[] = IsUserPresentVoter::IS_USER_PRESENT; 166 | } 167 | 168 | return $attributes; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Security/Authentication/Token/WebauthnTokenInterface.php: -------------------------------------------------------------------------------- 1 | isUserPresent() ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED; 31 | } 32 | 33 | return $result; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Security/Authorization/Voter/IsUserVerifiedVoter.php: -------------------------------------------------------------------------------- 1 | isUserVerified() ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED; 31 | } 32 | 33 | return $result; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Security/Guesser/CurrentUserEntityGuesser.php: -------------------------------------------------------------------------------- 1 | tokenStorage->getToken()?->getUser(); 24 | Assertion::notNull($user, 'Unable to find the user entity'); 25 | $userEntity = $this->userEntityRepository->findOneByUserHandle($user->getUserIdentifier()); 26 | Assertion::notNull($userEntity, 'Unable to find the user entity'); 27 | 28 | return $userEntity; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Security/Guesser/RequestBodyUserEntityGuesser.php: -------------------------------------------------------------------------------- 1 | getContentType(), 'Only JSON content type allowed'); 29 | $content = $request->getContent(); 30 | Assertion::string($content, 'Invalid data'); 31 | 32 | /** @var ServerPublicKeyCredentialCreationOptionsRequest $dto */ 33 | $dto = $this->serializer->deserialize($content, ServerPublicKeyCredentialCreationOptionsRequest::class, 'json'); 34 | $errors = $this->validator->validate($dto); 35 | 36 | if (count($errors) > 0) { 37 | $messages = []; 38 | foreach ($errors as $error) { 39 | $messages[] = $error->getPropertyPath() . ': ' . $error->getMessage(); 40 | } 41 | throw new RuntimeException(implode("\n", $messages)); 42 | } 43 | 44 | $existingUserEntity = $this->userEntityRepository->findOneByUsername($dto->username); 45 | 46 | return $existingUserEntity ?? PublicKeyCredentialUserEntity::create( 47 | $dto->username, 48 | $this->userEntityRepository->generateNextUserEntityId(), 49 | $dto->displayName 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Security/Guesser/UserEntityGuesser.php: -------------------------------------------------------------------------------- 1 | jsonSerialize(); 19 | $data['status'] = 'ok'; 20 | $data['errorMessage'] = ''; 21 | 22 | return new JsonResponse($data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Security/Handler/DefaultFailureHandler.php: -------------------------------------------------------------------------------- 1 | 'error', 20 | 'errorMessage' => $exception === null ? 'Authentication failed' : $exception->getMessage(), 21 | ]; 22 | 23 | return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); 24 | } 25 | 26 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response 27 | { 28 | return $this->onFailure($request, $exception); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Security/Handler/DefaultRequestOptionsHandler.php: -------------------------------------------------------------------------------- 1 | jsonSerialize(); 19 | $data['status'] = 'ok'; 20 | $data['errorMessage'] = ''; 21 | 22 | return new JsonResponse($data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Security/Handler/DefaultSuccessHandler.php: -------------------------------------------------------------------------------- 1 | 'ok', 19 | 'errorMessage' => '', 20 | ]; 21 | 22 | return new JsonResponse($data, JsonResponse::HTTP_OK); 23 | } 24 | 25 | public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response 26 | { 27 | return $this->onSuccess($request); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Security/Handler/FailureHandler.php: -------------------------------------------------------------------------------- 1 | authenticatorResponse; 30 | } 31 | 32 | public function getPublicKeyCredentialOptions(): PublicKeyCredentialOptions 33 | { 34 | return $this->publicKeyCredentialOptions; 35 | } 36 | 37 | public function getPublicKeyCredentialUserEntity(): ?PublicKeyCredentialUserEntity 38 | { 39 | return $this->publicKeyCredentialUserEntity; 40 | } 41 | 42 | public function getPublicKeyCredentialSource(): PublicKeyCredentialSource 43 | { 44 | return $this->publicKeyCredentialSource; 45 | } 46 | 47 | public function getFirewallName(): string 48 | { 49 | return $this->firewallName; 50 | } 51 | 52 | public function isResolved(): bool 53 | { 54 | return true; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Security/Http/Authenticator/WebauthnAuthenticator.php: -------------------------------------------------------------------------------- 1 | logger = new NullLogger(); 61 | } 62 | 63 | public function setLogger(LoggerInterface $logger): void 64 | { 65 | $this->logger = $logger; 66 | } 67 | 68 | public function supports(Request $request): ?bool 69 | { 70 | if ($request->getMethod() !== Request::METHOD_POST) { 71 | return false; 72 | } 73 | 74 | if ($this->firewallConfig->isAuthenticationEnabled() && $this->firewallConfig->isAuthenticationResultPathRequest( 75 | $request 76 | )) { 77 | return true; 78 | } 79 | if ($this->firewallConfig->isRegistrationEnabled() && $this->firewallConfig->isRegistrationResultPathRequest( 80 | $request 81 | )) { 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | public function authenticate(Request $request): Passport 89 | { 90 | if ($this->firewallConfig->isAuthenticationResultPathRequest($request)) { 91 | return $this->processWithAssertion($request); 92 | } 93 | 94 | return $this->processWithAttestation($request); 95 | } 96 | 97 | public function createToken(Passport $passport, string $firewallName): TokenInterface 98 | { 99 | $credentialsBadge = $passport->getBadge(WebauthnCredentials::class); 100 | Assertion::isInstanceOf($credentialsBadge, WebauthnCredentials::class, 'Invalid credentials'); 101 | 102 | $userBadge = $passport->getBadge(UserBadge::class); 103 | Assertion::isInstanceOf($userBadge, UserBadge::class, 'Invalid user'); 104 | 105 | /** @var AuthenticatorAttestationResponse|AuthenticatorAssertionResponse $response */ 106 | $response = $credentialsBadge->getAuthenticatorResponse(); 107 | if ($response instanceof AuthenticatorAssertionResponse) { 108 | $authData = $response->getAuthenticatorData(); 109 | } else { 110 | $authData = $response->getAttestationObject() 111 | ->getAuthData() 112 | ; 113 | } 114 | $userEntity = $credentialsBadge->getPublicKeyCredentialUserEntity(); 115 | Assertion::notNull($userEntity, 'The user entity is missing'); 116 | 117 | $token = new WebauthnToken( 118 | $userEntity, 119 | $credentialsBadge->getPublicKeyCredentialOptions(), 120 | $credentialsBadge->getPublicKeyCredentialSource() 121 | ->getPublicKeyCredentialDescriptor(), 122 | $authData->isUserPresent(), 123 | $authData->isUserVerified(), 124 | $authData->getReservedForFutureUse1(), 125 | $authData->getReservedForFutureUse2(), 126 | $authData->getSignCount(), 127 | $authData->getExtensions(), 128 | $credentialsBadge->getFirewallName(), 129 | $userBadge->getUser() 130 | ->getRoles() 131 | ); 132 | $token->setUser($userBadge->getUser()); 133 | 134 | return $token; 135 | } 136 | 137 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response 138 | { 139 | $this->logger->info('User has been authenticated successfully with Webauthn.', [ 140 | 'request' => $request, 141 | 'firewallName' => $firewallName, 142 | 'identifier' => $token->getUserIdentifier(), 143 | ]); 144 | 145 | return $this->successHandler->onAuthenticationSuccess($request, $token); 146 | } 147 | 148 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response 149 | { 150 | $this->logger->info('Webauthn authentication request failed.', [ 151 | 'request' => $request, 152 | 'exception' => $exception, 153 | ]); 154 | 155 | return $this->failureHandler->onAuthenticationFailure($request, $exception); 156 | } 157 | 158 | public function isInteractive(): bool 159 | { 160 | return true; 161 | } 162 | 163 | private function processWithAssertion(Request $request): Passport 164 | { 165 | try { 166 | Assertion::eq('json', $request->getContentType(), 'Only JSON content type allowed'); 167 | $content = $request->getContent(); 168 | Assertion::string($content, 'Invalid data'); 169 | $publicKeyCredential = $this->publicKeyCredentialLoader->load($content); 170 | $response = $publicKeyCredential->getResponse(); 171 | Assertion::isInstanceOf($response, AuthenticatorAssertionResponse::class, 'Invalid response'); 172 | 173 | $data = $this->optionsStorage->get(); 174 | $publicKeyCredentialRequestOptions = $data->getPublicKeyCredentialOptions(); 175 | Assertion::isInstanceOf( 176 | $publicKeyCredentialRequestOptions, 177 | PublicKeyCredentialRequestOptions::class, 178 | 'Invalid data' 179 | ); 180 | 181 | $userEntity = $data->getPublicKeyCredentialUserEntity(); 182 | $psr7Request = $this->httpMessageFactory->createRequest($request); 183 | $source = $this->assertionResponseValidator->check( 184 | $publicKeyCredential->getRawId(), 185 | $response, 186 | $publicKeyCredentialRequestOptions, 187 | $psr7Request, 188 | $userEntity?->getId(), 189 | $this->securedRelyingPartyIds 190 | ); 191 | 192 | $userEntity = $this->credentialUserEntityRepository->findOneByUserHandle($source->getUserHandle()); 193 | Assertion::isInstanceOf($userEntity, PublicKeyCredentialUserEntity::class, 'Invalid user entity'); 194 | 195 | $credentials = new WebauthnCredentials( 196 | $response, 197 | $publicKeyCredentialRequestOptions, 198 | $userEntity, 199 | $source, 200 | $this->firewallConfig->getFirewallName() 201 | ); 202 | $userBadge = new UserBadge($source->getUserHandle(), $this->userProvider->loadUserByIdentifier(...)); 203 | 204 | return new Passport($userBadge, $credentials, []); 205 | } catch (Throwable $e) { 206 | throw new AuthenticationException($e->getMessage(), $e->getCode(), $e); 207 | } 208 | } 209 | 210 | private function processWithAttestation(Request $request): Passport 211 | { 212 | try { 213 | Assertion::eq('json', $request->getContentType(), 'Only JSON content type allowed'); 214 | $content = $request->getContent(); 215 | Assertion::string($content, 'Invalid data'); 216 | $publicKeyCredential = $this->publicKeyCredentialLoader->load($content); 217 | $response = $publicKeyCredential->getResponse(); 218 | Assertion::isInstanceOf($response, AuthenticatorAttestationResponse::class, 'Invalid response'); 219 | 220 | $storedData = $this->optionsStorage->get(); 221 | $publicKeyCredentialCreationOptions = $storedData->getPublicKeyCredentialOptions(); 222 | Assertion::isInstanceOf( 223 | $publicKeyCredentialCreationOptions, 224 | PublicKeyCredentialCreationOptions::class, 225 | 'Unable to find the public key credential creation options' 226 | ); 227 | $userEntity = $storedData->getPublicKeyCredentialUserEntity(); 228 | Assertion::notNull($userEntity, 'Unable to find the public key credential user entity'); 229 | 230 | $psr7Request = $this->httpMessageFactory->createRequest($request); 231 | $credentialSource = $this->attestationResponseValidator->check( 232 | $response, 233 | $publicKeyCredentialCreationOptions, 234 | $psr7Request, 235 | $this->securedRelyingPartyIds 236 | ); 237 | if ($this->credentialUserEntityRepository->findOneByUsername($userEntity->getName()) !== null) { 238 | throw new InvalidArgumentException('The username already exists'); 239 | } 240 | if ($this->credentialSourceRepository->findOneByCredentialId( 241 | $credentialSource->getPublicKeyCredentialId() 242 | ) !== null) { 243 | throw new InvalidArgumentException('The credentials already exists'); 244 | } 245 | $this->credentialUserEntityRepository->saveUserEntity($userEntity); 246 | $this->credentialSourceRepository->saveCredentialSource($credentialSource); 247 | 248 | $credentials = new WebauthnCredentials( 249 | $response, 250 | $publicKeyCredentialCreationOptions, 251 | $userEntity, 252 | $credentialSource, 253 | $this->firewallConfig->getFirewallName() 254 | ); 255 | $userBadge = new UserBadge($credentialSource->getUserHandle(), $this->userProvider->loadUserByIdentifier( 256 | ... 257 | )); 258 | 259 | return new Passport($userBadge, $credentials, []); 260 | } catch (Throwable $e) { 261 | throw new AuthenticationException($e->getMessage(), $e->getCode(), $e); 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/Security/Storage/Item.php: -------------------------------------------------------------------------------- 1 | publicKeyCredentialOptions; 28 | } 29 | 30 | public function getPublicKeyCredentialUserEntity(): ?PublicKeyCredentialUserEntity 31 | { 32 | return $this->publicKeyCredentialUserEntity; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Security/Storage/OptionsStorage.php: -------------------------------------------------------------------------------- 1 | requestStack->getSession(); 26 | $session->set(self::SESSION_PARAMETER, [ 27 | 'options' => $item->getPublicKeyCredentialOptions(), 28 | 'userEntity' => $item->getPublicKeyCredentialUserEntity(), 29 | ]); 30 | } 31 | 32 | public function get(): Item 33 | { 34 | $session = $this->requestStack->getSession(); 35 | $sessionValue = $session->remove(self::SESSION_PARAMETER); 36 | if (! is_array($sessionValue) || ! array_key_exists('options', $sessionValue) || ! array_key_exists( 37 | 'userEntity', 38 | $sessionValue 39 | )) { 40 | throw new BadRequestHttpException('No public key credential options available for this session.'); 41 | } 42 | 43 | $publicKeyCredentialRequestOptions = $sessionValue['options']; 44 | $userEntity = $sessionValue['userEntity']; 45 | 46 | if (! $publicKeyCredentialRequestOptions instanceof PublicKeyCredentialOptions) { 47 | throw new BadRequestHttpException('No public key credential options available for this session.'); 48 | } 49 | if ($userEntity !== null && ! $userEntity instanceof PublicKeyCredentialUserEntity) { 50 | throw new BadRequestHttpException('No user entity available for this session.'); 51 | } 52 | 53 | return Item::create($publicKeyCredentialRequestOptions, $userEntity); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Security/WebauthnFirewallConfig.php: -------------------------------------------------------------------------------- 1 | $options 15 | */ 16 | public function __construct( 17 | private readonly array $options, 18 | private readonly string $firewallName, 19 | private readonly HttpUtils $httpUtils, 20 | ) { 21 | } 22 | 23 | public function getFirewallName(): string 24 | { 25 | return $this->firewallName; 26 | } 27 | 28 | public function getUserProvider(): ?string 29 | { 30 | return $this->options['user_provider'] ?? null; 31 | } 32 | 33 | public function getOptionsStorage(): string 34 | { 35 | return $this->options['options_storage'] ?? WebauthnFactory::DEFAULT_SESSION_STORAGE_SERVICE; 36 | } 37 | 38 | public function getSuccessHandler(): string 39 | { 40 | return $this->options['success_handler'] ?? WebauthnFactory::DEFAULT_SUCCESS_HANDLER_SERVICE; 41 | } 42 | 43 | public function getFailureHandler(): string 44 | { 45 | return $this->options['success_handler'] ?? WebauthnFactory::DEFAULT_FAILURE_HANDLER_SERVICE; 46 | } 47 | 48 | public function isAuthenticationEnabled(): bool 49 | { 50 | return $this->options['authentication']['enabled'] ?? true; 51 | } 52 | 53 | public function getAuthenticationProfile(): string 54 | { 55 | return $this->options['authentication']['profile'] ?? 'default'; 56 | } 57 | 58 | public function getAuthenticationOptionsHandler(): string 59 | { 60 | return $this->options['authentication']['options_handler'] ?? WebauthnFactory::DEFAULT_REQUEST_OPTIONS_HANDLER_SERVICE; 61 | } 62 | 63 | public function getAuthenticationHost(): ?string 64 | { 65 | return $this->options['authentication']['routes']['host'] ?? null; 66 | } 67 | 68 | public function getAuthenticationOptionsPath(): string 69 | { 70 | return $this->options['authentication']['routes']['options_path'] ?? WebauthnFactory::DEFAULT_LOGIN_OPTIONS_PATH; 71 | } 72 | 73 | public function getAuthenticationResultPath(): string 74 | { 75 | return $this->options['authentication']['routes']['result_path'] ?? WebauthnFactory::DEFAULT_LOGIN_RESULT_PATH; 76 | } 77 | 78 | public function isRegistrationEnabled(): bool 79 | { 80 | return $this->options['registration']['enabled'] ?? true; 81 | } 82 | 83 | public function getRegistrationProfile(): string 84 | { 85 | return $this->options['registration']['profile'] ?? 'default'; 86 | } 87 | 88 | public function getRegistrationOptionsHandler(): string 89 | { 90 | return $this->options['registration']['options_handler'] ?? WebauthnFactory::DEFAULT_REQUEST_OPTIONS_HANDLER_SERVICE; 91 | } 92 | 93 | public function getRegistrationHost(): ?string 94 | { 95 | return $this->options['registration']['routes']['host'] ?? null; 96 | } 97 | 98 | public function getRegistrationOptionsPath(): string 99 | { 100 | return $this->options['registration']['routes']['options_path'] ?? WebauthnFactory::DEFAULT_LOGIN_OPTIONS_PATH; 101 | } 102 | 103 | public function getRegistrationResultPath(): string 104 | { 105 | return $this->options['registration']['routes']['result_path'] ?? WebauthnFactory::DEFAULT_LOGIN_RESULT_PATH; 106 | } 107 | 108 | /** 109 | * @return string[] 110 | */ 111 | public function getSecuredRpIds(): array 112 | { 113 | return $this->options['secured_rp_ids'] ?? []; 114 | } 115 | 116 | public function isAuthenticationOptionsPathRequest(Request $request): bool 117 | { 118 | return $this->httpUtils->checkRequestPath($request, $this->getAuthenticationOptionsPath()); 119 | } 120 | 121 | public function isAuthenticationResultPathRequest(Request $request): bool 122 | { 123 | return $this->httpUtils->checkRequestPath($request, $this->getAuthenticationResultPath()); 124 | } 125 | 126 | public function isRegistrationOptionsPathRequest(Request $request): bool 127 | { 128 | return $this->httpUtils->checkRequestPath($request, $this->getRegistrationOptionsPath()); 129 | } 130 | 131 | public function isRegistrationResultPathRequest(Request $request): bool 132 | { 133 | return $this->httpUtils->checkRequestPath($request, $this->getRegistrationResultPath()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Service/AuthenticatorAssertionResponseValidator.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch(new AuthenticatorAssertionResponseValidationSucceededEvent( 54 | $credentialId, 55 | $authenticatorAssertionResponse, 56 | $publicKeyCredentialRequestOptions, 57 | $request, 58 | $userHandle, 59 | $result 60 | )); 61 | 62 | return $result; 63 | } catch (Throwable $throwable) { 64 | $this->eventDispatcher->dispatch(new AuthenticatorAssertionResponseValidationFailedEvent( 65 | $credentialId, 66 | $authenticatorAssertionResponse, 67 | $publicKeyCredentialRequestOptions, 68 | $request, 69 | $userHandle, 70 | $throwable 71 | )); 72 | 73 | throw $throwable; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Service/AuthenticatorAttestationResponseValidator.php: -------------------------------------------------------------------------------- 1 | eventDispatcher->dispatch(new AuthenticatorAttestationResponseValidationSucceededEvent( 47 | $authenticatorAttestationResponse, 48 | $publicKeyCredentialCreationOptions, 49 | $request, 50 | $result 51 | )); 52 | 53 | return $result; 54 | } catch (Throwable $throwable) { 55 | $this->eventDispatcher->dispatch(new AuthenticatorAttestationResponseValidationFailedEvent( 56 | $authenticatorAttestationResponse, 57 | $publicKeyCredentialCreationOptions, 58 | $request, 59 | $throwable 60 | )); 61 | 62 | throw $throwable; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Service/DefaultFailureHandler.php: -------------------------------------------------------------------------------- 1 | 'error', 19 | 'errorMessage' => $exception === null ? 'An unexpected error occurred' : $exception->getMessage(), 20 | ]; 21 | 22 | return new JsonResponse($data, Response::HTTP_BAD_REQUEST); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Service/DefaultSuccessHandler.php: -------------------------------------------------------------------------------- 1 | 'ok', 18 | 'errorMessage' => '', 19 | ]; 20 | 21 | return new JsonResponse($data, JsonResponse::HTTP_CREATED); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Service/PublicKeyCredentialCreationOptionsFactory.php: -------------------------------------------------------------------------------- 1 | profiles, $key, sprintf('The profile with key "%s" does not exist.', $key)); 42 | $profile = $this->profiles[$key]; 43 | 44 | $options = PublicKeyCredentialCreationOptions 45 | ::create( 46 | $this->createRpEntity($profile), 47 | $userEntity, 48 | random_bytes($profile['challenge_length']), 49 | $this->createCredentialParameters($profile) 50 | ) 51 | ->excludeCredentials(...$excludeCredentials) 52 | ->setAuthenticatorSelection( 53 | $authenticatorSelection ?? $this->createAuthenticatorSelectionCriteria($profile) 54 | ) 55 | ->setAttestation($attestationConveyance ?? $profile['attestation_conveyance']) 56 | ->setExtensions($authenticationExtensionsClientInputs ?? $this->createExtensions($profile)) 57 | ->setTimeout($profile['timeout']) 58 | ; 59 | $this->eventDispatcher->dispatch(new PublicKeyCredentialCreationOptionsCreatedEvent($options)); 60 | 61 | return $options; 62 | } 63 | 64 | /** 65 | * @param mixed[] $profile 66 | */ 67 | private function createExtensions(array $profile): AuthenticationExtensionsClientInputs 68 | { 69 | $extensions = new AuthenticationExtensionsClientInputs(); 70 | foreach ($profile['extensions'] as $k => $v) { 71 | $extensions->add(AuthenticationExtension::create($k, $v)); 72 | } 73 | 74 | return $extensions; 75 | } 76 | 77 | /** 78 | * @param mixed[] $profile 79 | */ 80 | private function createAuthenticatorSelectionCriteria(array $profile): AuthenticatorSelectionCriteria 81 | { 82 | return AuthenticatorSelectionCriteria::create() 83 | ->setAuthenticatorAttachment($profile['authenticator_selection_criteria']['attachment_mode']) 84 | ->setRequireResidentKey($profile['authenticator_selection_criteria']['require_resident_key']) 85 | ->setUserVerification($profile['authenticator_selection_criteria']['user_verification']) 86 | ->setResidentKey($profile['authenticator_selection_criteria']['resident_key']) 87 | ; 88 | } 89 | 90 | /** 91 | * @param mixed[] $profile 92 | */ 93 | private function createRpEntity(array $profile): PublicKeyCredentialRpEntity 94 | { 95 | return new PublicKeyCredentialRpEntity($profile['rp']['name'], $profile['rp']['id'], $profile['rp']['icon']); 96 | } 97 | 98 | /** 99 | * @param mixed[] $profile 100 | * 101 | * @return PublicKeyCredentialParameters[] 102 | */ 103 | private function createCredentialParameters(array $profile): array 104 | { 105 | $callback = static fn ($alg): PublicKeyCredentialParameters => new PublicKeyCredentialParameters( 106 | PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, 107 | $alg 108 | ); 109 | 110 | return array_map($callback, $profile['public_key_credential_parameters']); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Service/PublicKeyCredentialRequestOptionsFactory.php: -------------------------------------------------------------------------------- 1 | profiles, $key, sprintf('The profile with key "%s" does not exist.', $key)); 36 | $profile = $this->profiles[$key]; 37 | 38 | $options = PublicKeyCredentialRequestOptions 39 | ::create(random_bytes($profile['challenge_length'])) 40 | ->setRpId($profile['rp_id']) 41 | ->setUserVerification($userVerification ?? $profile['user_verification']) 42 | ->allowCredentials(...$allowCredentials) 43 | ->setExtensions($authenticationExtensionsClientInputs ?? $this->createExtensions($profile)) 44 | ->setTimeout($profile['timeout']) 45 | ; 46 | $this->eventDispatcher->dispatch(new PublicKeyCredentialRequestOptionsCreatedEvent($options)); 47 | 48 | return $options; 49 | } 50 | 51 | /** 52 | * @param mixed[] $profile 53 | */ 54 | private function createExtensions(array $profile): AuthenticationExtensionsClientInputs 55 | { 56 | $extensions = new AuthenticationExtensionsClientInputs(); 57 | foreach ($profile['extensions'] as $k => $v) { 58 | $extensions->add(AuthenticationExtension::create($k, $v)); 59 | } 60 | 61 | return $extensions; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/WebauthnBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass( 42 | new AttestationStatementSupportCompilerPass(), 43 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 44 | 0 45 | ); 46 | $container->addCompilerPass( 47 | new ExtensionOutputCheckerCompilerPass(), 48 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 49 | 0 50 | ); 51 | $container->addCompilerPass(new CoseAlgorithmCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); 52 | $container->addCompilerPass(new DynamicRouteCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); 53 | $container->addCompilerPass( 54 | new EnforcedSafetyNetApiKeyVerificationCompilerPass(), 55 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 56 | 0 57 | ); 58 | $container->addCompilerPass(new LoggerSetterCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0); 59 | $container->addCompilerPass( 60 | new CounterCheckerSetterCompilerPass(), 61 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 62 | 0 63 | ); 64 | $container->addCompilerPass( 65 | new CertificateChainCheckerSetterCompilerPass(), 66 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 67 | 0 68 | ); 69 | $container->addCompilerPass( 70 | new MetadataStatementSupportCompilerPass(), 71 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 72 | 0 73 | ); 74 | 75 | $this->registerMappings($container); 76 | 77 | if ($container->hasExtension('security')) { 78 | $extension = $container->getExtension('security'); 79 | Assertion::isInstanceOf( 80 | $extension, 81 | SecurityExtension::class, 82 | 'The security extension is missing or invalid' 83 | ); 84 | $extension->addAuthenticatorFactory(new WebauthnFactory(new WebauthnServicesFactory())); 85 | } 86 | } 87 | 88 | private function registerMappings(ContainerBuilder $container): void 89 | { 90 | $realPath = realpath(__DIR__ . '/Resources/config/doctrine-mapping'); 91 | $mappings = [ 92 | $realPath => 'Webauthn', 93 | ]; 94 | if (class_exists(DoctrineOrmMappingsPass::class)) { 95 | $container->addCompilerPass( 96 | DoctrineOrmMappingsPass::createXmlMappingDriver($mappings, []), 97 | PassConfig::TYPE_BEFORE_OPTIMIZATION, 98 | 0 99 | ); 100 | } 101 | } 102 | } 103 | --------------------------------------------------------------------------------