Skip to content

Commit

Permalink
Add Webauthn badge-based authentication support
Browse files Browse the repository at this point in the history
Introduces Webauthn-based authentication classes, including `WebauthnAuthenticator`, `WebauthnBadge`, `WebauthnBadgeListener`, and `WebauthnPassport`. These components enable secure, passwordless login using public key credentials, integrating seamlessly into Symfony's security system.
  • Loading branch information
Spomky committed Feb 16, 2025
1 parent 98088fa commit a60c44f
Show file tree
Hide file tree
Showing 15 changed files with 402 additions and 65 deletions.
5 changes: 5 additions & 0 deletions src/stimulus/assets/dist/controller.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default class extends Controller {
type: StringConstructor;
default: string;
};
requestResultField: {
type: StringConstructor;
default: null;
};
requestSuccessRedirectUri: StringConstructor;
creationResultUrl: {
type: StringConstructor;
Expand Down Expand Up @@ -59,6 +63,7 @@ export default class extends Controller {
};
readonly requestResultUrlValue: string;
readonly requestOptionsUrlValue: string;
readonly requestResultFieldValue?: string;
readonly requestSuccessRedirectUriValue?: string;
readonly creationResultUrlValue: string;
readonly creationOptionsUrlValue: string;
Expand Down
13 changes: 12 additions & 1 deletion src/stimulus/assets/dist/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ class default_1 extends Controller {
constructor() {
super(...arguments);
this.connect = async () => {
var _a, _b;
var _a, _b, _c;
const options = {
requestResultUrl: this.requestResultUrlValue,
requestOptionsUrl: this.requestOptionsUrlValue,
requestSuccessRedirectUri: (_a = this.requestSuccessRedirectUriValue) !== null && _a !== undefined ? _a : null,
requestResultField: (_a = this.requestResultFieldValue) !== null && _a !== undefined ? _a : null,
requestSuccessRedirectUri: (_b = this.requestSuccessRedirectUriValue) !== null && _b !== undefined ? _b : null,
creationResultUrl: this.creationResultUrlValue,
creationOptionsUrl: this.creationOptionsUrlValue,
creationSuccessRedirectUri: (_b = this.creationSuccessRedirectUriValue) !== null && _b !== undefined ? _b : null,
creationSuccessRedirectUri: (_c = this.creationSuccessRedirectUriValue) !== null && _c !== undefined ? _c : null,
};
this._dispatchEvent('webauthn:connect', { options });
const supportAutofill = await browserSupportsWebAuthnAutofill();
Expand All @@ -38,9 +41,16 @@ class default_1 extends Controller {
this._processSignin(optionsResponseJson, false);
}
async _processSignin(optionsResponseJson, useBrowserAutofill) {
var _a;
try {
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
console.log(authenticatorResponse, this.requestResultFieldValue, this.element instanceof HTMLFormElement);
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
(_a = this.element.querySelector(this.requestResultFieldValue)) === null || _a === void 0 ? void 0 : _a.setAttribute('value', JSON.stringify(authenticatorResponse));
this.element.submit();
return;
}
const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {
window.location.replace(this.requestSuccessRedirectUriValue);
Expand Down Expand Up @@ -151,6 +161,7 @@ class default_1 extends Controller {
default_1.values = {
requestResultUrl: { type: String, default: '/request' },
requestOptionsUrl: { type: String, default: '/request/options' },
requestResultField: { type: String, default: null },
requestSuccessRedirectUri: String,
creationResultUrl: { type: String, default: '/creation' },
creationOptionsUrl: { type: String, default: '/creation/options' },
Expand Down
13 changes: 13 additions & 0 deletions src/stimulus/assets/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default class extends Controller {
static values = {
requestResultUrl: { type: String, default: '/request' },
requestOptionsUrl: { type: String, default: '/request/options' },
requestResultField: { type: String, default: null },
requestSuccessRedirectUri: String,
creationResultUrl: { type: String, default: '/creation' },
creationOptionsUrl: { type: String, default: '/creation/options' },
Expand All @@ -32,6 +33,7 @@ export default class extends Controller {

declare readonly requestResultUrlValue: string;
declare readonly requestOptionsUrlValue: string;
declare readonly requestResultFieldValue?: string;
declare readonly requestSuccessRedirectUriValue?: string;
declare readonly creationResultUrlValue: string;
declare readonly creationOptionsUrlValue: string;
Expand All @@ -49,6 +51,7 @@ export default class extends Controller {
const options = {
requestResultUrl: this.requestResultUrlValue,
requestOptionsUrl: this.requestOptionsUrlValue,
requestResultField: this.requestResultFieldValue ?? null,
requestSuccessRedirectUri: this.requestSuccessRedirectUriValue ?? null,
creationResultUrl: this.creationResultUrlValue,
creationOptionsUrl: this.creationOptionsUrlValue,
Expand Down Expand Up @@ -85,6 +88,16 @@ export default class extends Controller {
// @ts-ignore
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
console.log(
authenticatorResponse,
this.requestResultFieldValue,
this.element instanceof HTMLFormElement,
);
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
this.element.querySelector(this.requestResultFieldValue)?.setAttribute('value', JSON.stringify(authenticatorResponse));
this.element.submit();
return;
}

const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {
Expand Down
7 changes: 3 additions & 4 deletions src/symfony/src/Controller/AssertionControllerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class AssertionControllerFactory implements CanLogData

public function __construct(
private readonly SerializerInterface $serializer,
private readonly OptionsStorage $optionStorage,
private readonly AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator,
private readonly PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository,
) {
Expand All @@ -36,21 +37,19 @@ public function setLogger(LoggerInterface $logger): void

public function createRequestController(
PublicKeyCredentialRequestOptionsBuilder $optionsBuilder,
OptionsStorage $optionStorage,
RequestOptionsHandler $optionsHandler,
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler
): AssertionRequestController {
return new AssertionRequestController(
$optionsBuilder,
$optionStorage,
$this->optionStorage,
$optionsHandler,
$failureHandler,
$this->logger,
);
}

public function createResponseController(
OptionsStorage $optionStorage,
SuccessHandler $successHandler,
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
null|AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator = null,
Expand All @@ -59,7 +58,7 @@ public function createResponseController(
$this->serializer,
$authenticatorAssertionResponseValidator ?? $this->authenticatorAssertionResponseValidator,
$this->logger,
$optionStorage,
$this->optionStorage,
$successHandler,
$failureHandler,
$this->publicKeyCredentialSourceRepository
Expand Down
7 changes: 3 additions & 4 deletions src/symfony/src/Controller/AttestationControllerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
final readonly class AttestationControllerFactory
{
public function __construct(
private OptionsStorage $optionStorage,
private SerializerInterface $serializer,
private AuthenticatorAttestationResponseValidator $attestationResponseValidator,
private PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository
Expand All @@ -27,23 +28,21 @@ public function __construct(
public function createRequestController(
PublicKeyCredentialCreationOptionsBuilder $optionsBuilder,
UserEntityGuesser $userEntityGuesser,
OptionsStorage $optionStorage,
CreationOptionsHandler $creationOptionsHandler,
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
bool $hideExistingExcludedCredentials = false
): AttestationRequestController {
return new AttestationRequestController(
$optionsBuilder,
$userEntityGuesser,
$optionStorage,
$this->optionStorage,
$creationOptionsHandler,
$failureHandler,
$hideExistingExcludedCredentials
);
}

public function createResponseController(
OptionsStorage $optionStorage,
SuccessHandler $successHandler,
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
null|AuthenticatorAttestationResponseValidator $attestationResponseValidator = null,
Expand All @@ -52,7 +51,7 @@ public function createResponseController(
$this->serializer,
$attestationResponseValidator ?? $this->attestationResponseValidator,
$this->publicKeyCredentialSourceRepository,
$optionStorage,
$this->optionStorage,
$successHandler,
$failureHandler,
);
Expand Down
10 changes: 8 additions & 2 deletions src/symfony/src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->defaultValue('webauthn.clock.default')
->info('PSR-20 Clock service.')
->end()
->scalarNode('options_storage')
->defaultValue(SessionStorage::class)
->info('Service responsible of the options/user entity storage during the ceremony')
->end()
->scalarNode('event_dispatcher')
->defaultValue(EventDispatcherInterface::class)
->info('PSR-14 Event Dispatcher service.')
Expand Down Expand Up @@ -330,7 +334,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
->defaultValue(Request::METHOD_POST)
->end()
->scalarNode('result_path')
->isRequired()
->defaultNull()
->end()
->scalarNode('host')
->defaultValue(null)
Expand All @@ -354,6 +358,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
->defaultFalse()
->end()
->scalarNode('options_storage')
->setDeprecated('web-auth/webauthn-symfony-bundle', '5.2.0', 'The child node "%node%" at path "%path%" is deprecated. Please use the root option "options_storage" instead.')
->defaultValue(SessionStorage::class)
->info('Service responsible of the options/user entity storage during the ceremony')
->end()
Expand Down Expand Up @@ -411,7 +416,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
->defaultValue(Request::METHOD_POST)
->end()
->scalarNode('result_path')
->isRequired()
->defaultNull()
->end()
->scalarNode('host')
->defaultValue(null)
Expand All @@ -426,6 +431,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
->defaultNull()
->end()
->scalarNode('options_storage')
->setDeprecated('web-auth/webauthn-symfony-bundle', '5.2.0', 'The child node "%node%" at path "%path%" is deprecated. Please use the root option "options_storage" instead.')
->defaultValue(SessionStorage::class)
->info('Service responsible of the options/user entity storage during the ceremony')
->end()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
private const PRIORITY = 0;

public function __construct(
private WebauthnServicesFactory $servicesFactory
private WebauthnServicesFactory $servicesFactory,
) {
}

Expand All @@ -112,6 +112,7 @@ public function addConfiguration(NodeDefinition $builder): void
->defaultNull()
->end()
->scalarNode('options_storage')
->setDeprecated('web-auth/webauthn-symfony-bundle', '5.2.0', 'The child node "%node%" at path "%path%" is deprecated. Please use the root option "options_storage" instead.')
->defaultValue(self::DEFAULT_SESSION_STORAGE_SERVICE)
->end()
->scalarNode('success_handler')
Expand Down Expand Up @@ -239,7 +240,6 @@ public function createAuthenticator(
$config['success_handler'],
$config['failure_handler'],
$firewallConfigId,
$config['options_storage'],
$authenticatorAssertionResponseValidatorId,
$authenticatorAttestationResponseValidatorId
);
Expand All @@ -263,7 +263,6 @@ private function createAuthenticatorService(
string $successHandlerId,
string $failureHandlerId,
string $firewallConfigId,
string $optionsStorageId,
string $authenticatorAssertionResponseValidatorId,
string $authenticatorAttestationResponseValidatorId
): string {
Expand All @@ -274,7 +273,6 @@ private function createAuthenticatorService(
->replaceArgument(1, new Reference($userProviderId))
->replaceArgument(2, new Reference($successHandlerId))
->replaceArgument(3, new Reference($failureHandlerId))
->replaceArgument(4, new Reference($optionsStorageId))
->replaceArgument(8, new Reference($authenticatorAssertionResponseValidatorId))
->replaceArgument(9, new Reference($authenticatorAttestationResponseValidatorId))
->addMethodCall('setLogger', [new Reference('webauthn.logger')]);
Expand Down Expand Up @@ -302,10 +300,10 @@ private function createAssertionControllersAndRoutes(
$config['authentication']['routes']['options_path'],
$config['authentication']['routes']['host'],
$optionsBuilderId,
$config['options_storage'],
$config['authentication']['options_handler'],
$config['failure_handler'],
);
if ($config['authentication']['routes']['result_path'] !== null) {
$this->createResponseControllerAndRoute(
$container,
$firewallName,
Expand All @@ -314,6 +312,7 @@ private function createAssertionControllersAndRoutes(
$config['authentication']['routes']['result_path'],
$config['authentication']['routes']['host']
);
}
}

/**
Expand All @@ -336,10 +335,10 @@ private function createAttestationControllersAndRoutes(
$config['registration']['routes']['options_path'],
$config['registration']['routes']['host'],
$optionsBuilderId,
$config['options_storage'],
$config['registration']['options_handler'],
$config['failure_handler'],
);
if ($config['registration']['routes']['result_path'] !== null) {
$this->createResponseControllerAndRoute(
$container,
$firewallName,
Expand All @@ -348,6 +347,7 @@ private function createAttestationControllersAndRoutes(
$config['registration']['routes']['result_path'],
$config['registration']['routes']['host']
);
}
}

private function createAssertionRequestControllerAndRoute(
Expand All @@ -357,15 +357,13 @@ private function createAssertionRequestControllerAndRoute(
string $path,
?string $host,
string $optionsBuilderId,
string $optionsStorageId,
string $optionsHandlerId,
string $failureHandlerId,
): void {
$controller = (new Definition(AssertionRequestController::class))
->setFactory([new Reference(AssertionControllerFactory::class), 'createRequestController'])
->setArguments([
new Reference($optionsBuilderId),
new Reference($optionsStorageId),
new Reference($optionsHandlerId),
new Reference($failureHandlerId),
]);
Expand All @@ -388,7 +386,6 @@ private function createAttestationRequestControllerAndRoute(
string $path,
?string $host,
string $optionsBuilderId,
string $optionsStorageId,
string $optionsHandlerId,
string $failureHandlerId,
): void {
Expand All @@ -397,7 +394,6 @@ private function createAttestationRequestControllerAndRoute(
->setArguments([
new Reference($optionsBuilderId),
new Reference(RequestBodyUserEntityGuesser::class),
new Reference($optionsStorageId),
new Reference($optionsHandlerId),
new Reference($failureHandlerId),
true,
Expand Down
Loading

0 comments on commit a60c44f

Please sign in to comment.