Skip to content

Commit a60c44f

Browse files
committed
Add Webauthn badge-based authentication support
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.
1 parent 98088fa commit a60c44f

File tree

15 files changed

+402
-65
lines changed

15 files changed

+402
-65
lines changed

src/stimulus/assets/dist/controller.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export default class extends Controller {
99
type: StringConstructor;
1010
default: string;
1111
};
12+
requestResultField: {
13+
type: StringConstructor;
14+
default: null;
15+
};
1216
requestSuccessRedirectUri: StringConstructor;
1317
creationResultUrl: {
1418
type: StringConstructor;
@@ -59,6 +63,7 @@ export default class extends Controller {
5963
};
6064
readonly requestResultUrlValue: string;
6165
readonly requestOptionsUrlValue: string;
66+
readonly requestResultFieldValue?: string;
6267
readonly requestSuccessRedirectUriValue?: string;
6368
readonly creationResultUrlValue: string;
6469
readonly creationOptionsUrlValue: string;

src/stimulus/assets/dist/controller.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ class default_1 extends Controller {
55
constructor() {
66
super(...arguments);
77
this.connect = async () => {
8-
var _a, _b;
8+
var _a, _b, _c;
99
const options = {
1010
requestResultUrl: this.requestResultUrlValue,
1111
requestOptionsUrl: this.requestOptionsUrlValue,
1212
requestSuccessRedirectUri: (_a = this.requestSuccessRedirectUriValue) !== null && _a !== undefined ? _a : null,
13+
requestResultField: (_a = this.requestResultFieldValue) !== null && _a !== undefined ? _a : null,
14+
requestSuccessRedirectUri: (_b = this.requestSuccessRedirectUriValue) !== null && _b !== undefined ? _b : null,
1315
creationResultUrl: this.creationResultUrlValue,
1416
creationOptionsUrl: this.creationOptionsUrlValue,
1517
creationSuccessRedirectUri: (_b = this.creationSuccessRedirectUriValue) !== null && _b !== undefined ? _b : null,
18+
creationSuccessRedirectUri: (_c = this.creationSuccessRedirectUriValue) !== null && _c !== undefined ? _c : null,
1619
};
1720
this._dispatchEvent('webauthn:connect', { options });
1821
const supportAutofill = await browserSupportsWebAuthnAutofill();
@@ -38,9 +41,16 @@ class default_1 extends Controller {
3841
this._processSignin(optionsResponseJson, false);
3942
}
4043
async _processSignin(optionsResponseJson, useBrowserAutofill) {
44+
var _a;
4145
try {
4246
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
4347
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
48+
console.log(authenticatorResponse, this.requestResultFieldValue, this.element instanceof HTMLFormElement);
49+
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
50+
(_a = this.element.querySelector(this.requestResultFieldValue)) === null || _a === void 0 ? void 0 : _a.setAttribute('value', JSON.stringify(authenticatorResponse));
51+
this.element.submit();
52+
return;
53+
}
4454
const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
4555
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {
4656
window.location.replace(this.requestSuccessRedirectUriValue);
@@ -151,6 +161,7 @@ class default_1 extends Controller {
151161
default_1.values = {
152162
requestResultUrl: { type: String, default: '/request' },
153163
requestOptionsUrl: { type: String, default: '/request/options' },
164+
requestResultField: { type: String, default: null },
154165
requestSuccessRedirectUri: String,
155166
creationResultUrl: { type: String, default: '/creation' },
156167
creationOptionsUrl: { type: String, default: '/creation/options' },

src/stimulus/assets/src/controller.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export default class extends Controller {
1111
static values = {
1212
requestResultUrl: { type: String, default: '/request' },
1313
requestOptionsUrl: { type: String, default: '/request/options' },
14+
requestResultField: { type: String, default: null },
1415
requestSuccessRedirectUri: String,
1516
creationResultUrl: { type: String, default: '/creation' },
1617
creationOptionsUrl: { type: String, default: '/creation/options' },
@@ -32,6 +33,7 @@ export default class extends Controller {
3233

3334
declare readonly requestResultUrlValue: string;
3435
declare readonly requestOptionsUrlValue: string;
36+
declare readonly requestResultFieldValue?: string;
3537
declare readonly requestSuccessRedirectUriValue?: string;
3638
declare readonly creationResultUrlValue: string;
3739
declare readonly creationOptionsUrlValue: string;
@@ -49,6 +51,7 @@ export default class extends Controller {
4951
const options = {
5052
requestResultUrl: this.requestResultUrlValue,
5153
requestOptionsUrl: this.requestOptionsUrlValue,
54+
requestResultField: this.requestResultFieldValue ?? null,
5255
requestSuccessRedirectUri: this.requestSuccessRedirectUriValue ?? null,
5356
creationResultUrl: this.creationResultUrlValue,
5457
creationOptionsUrl: this.creationOptionsUrlValue,
@@ -85,6 +88,16 @@ export default class extends Controller {
8588
// @ts-ignore
8689
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
8790
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
91+
console.log(
92+
authenticatorResponse,
93+
this.requestResultFieldValue,
94+
this.element instanceof HTMLFormElement,
95+
);
96+
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
97+
this.element.querySelector(this.requestResultFieldValue)?.setAttribute('value', JSON.stringify(authenticatorResponse));
98+
this.element.submit();
99+
return;
100+
}
88101

89102
const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
90103
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {

src/symfony/src/Controller/AssertionControllerFactory.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class AssertionControllerFactory implements CanLogData
2323

2424
public function __construct(
2525
private readonly SerializerInterface $serializer,
26+
private readonly OptionsStorage $optionStorage,
2627
private readonly AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator,
2728
private readonly PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository,
2829
) {
@@ -36,21 +37,19 @@ public function setLogger(LoggerInterface $logger): void
3637

3738
public function createRequestController(
3839
PublicKeyCredentialRequestOptionsBuilder $optionsBuilder,
39-
OptionsStorage $optionStorage,
4040
RequestOptionsHandler $optionsHandler,
4141
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler
4242
): AssertionRequestController {
4343
return new AssertionRequestController(
4444
$optionsBuilder,
45-
$optionStorage,
45+
$this->optionStorage,
4646
$optionsHandler,
4747
$failureHandler,
4848
$this->logger,
4949
);
5050
}
5151

5252
public function createResponseController(
53-
OptionsStorage $optionStorage,
5453
SuccessHandler $successHandler,
5554
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
5655
null|AuthenticatorAssertionResponseValidator $authenticatorAssertionResponseValidator = null,
@@ -59,7 +58,7 @@ public function createResponseController(
5958
$this->serializer,
6059
$authenticatorAssertionResponseValidator ?? $this->authenticatorAssertionResponseValidator,
6160
$this->logger,
62-
$optionStorage,
61+
$this->optionStorage,
6362
$successHandler,
6463
$failureHandler,
6564
$this->publicKeyCredentialSourceRepository

src/symfony/src/Controller/AttestationControllerFactory.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
final readonly class AttestationControllerFactory
1919
{
2020
public function __construct(
21+
private OptionsStorage $optionStorage,
2122
private SerializerInterface $serializer,
2223
private AuthenticatorAttestationResponseValidator $attestationResponseValidator,
2324
private PublicKeyCredentialSourceRepositoryInterface $publicKeyCredentialSourceRepository
@@ -27,23 +28,21 @@ public function __construct(
2728
public function createRequestController(
2829
PublicKeyCredentialCreationOptionsBuilder $optionsBuilder,
2930
UserEntityGuesser $userEntityGuesser,
30-
OptionsStorage $optionStorage,
3131
CreationOptionsHandler $creationOptionsHandler,
3232
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
3333
bool $hideExistingExcludedCredentials = false
3434
): AttestationRequestController {
3535
return new AttestationRequestController(
3636
$optionsBuilder,
3737
$userEntityGuesser,
38-
$optionStorage,
38+
$this->optionStorage,
3939
$creationOptionsHandler,
4040
$failureHandler,
4141
$hideExistingExcludedCredentials
4242
);
4343
}
4444

4545
public function createResponseController(
46-
OptionsStorage $optionStorage,
4746
SuccessHandler $successHandler,
4847
FailureHandler|AuthenticationFailureHandlerInterface $failureHandler,
4948
null|AuthenticatorAttestationResponseValidator $attestationResponseValidator = null,
@@ -52,7 +51,7 @@ public function createResponseController(
5251
$this->serializer,
5352
$attestationResponseValidator ?? $this->attestationResponseValidator,
5453
$this->publicKeyCredentialSourceRepository,
55-
$optionStorage,
54+
$this->optionStorage,
5655
$successHandler,
5756
$failureHandler,
5857
);

src/symfony/src/DependencyInjection/Configuration.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public function getConfigTreeBuilder(): TreeBuilder
6464
->defaultValue('webauthn.clock.default')
6565
->info('PSR-20 Clock service.')
6666
->end()
67+
->scalarNode('options_storage')
68+
->defaultValue(SessionStorage::class)
69+
->info('Service responsible of the options/user entity storage during the ceremony')
70+
->end()
6771
->scalarNode('event_dispatcher')
6872
->defaultValue(EventDispatcherInterface::class)
6973
->info('PSR-14 Event Dispatcher service.')
@@ -330,7 +334,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
330334
->defaultValue(Request::METHOD_POST)
331335
->end()
332336
->scalarNode('result_path')
333-
->isRequired()
337+
->defaultNull()
334338
->end()
335339
->scalarNode('host')
336340
->defaultValue(null)
@@ -354,6 +358,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
354358
->defaultFalse()
355359
->end()
356360
->scalarNode('options_storage')
361+
->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.')
357362
->defaultValue(SessionStorage::class)
358363
->info('Service responsible of the options/user entity storage during the ceremony')
359364
->end()
@@ -411,7 +416,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
411416
->defaultValue(Request::METHOD_POST)
412417
->end()
413418
->scalarNode('result_path')
414-
->isRequired()
419+
->defaultNull()
415420
->end()
416421
->scalarNode('host')
417422
->defaultValue(null)
@@ -426,6 +431,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
426431
->defaultNull()
427432
->end()
428433
->scalarNode('options_storage')
434+
->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.')
429435
->defaultValue(SessionStorage::class)
430436
->info('Service responsible of the options/user entity storage during the ceremony')
431437
->end()

src/symfony/src/DependencyInjection/Factory/Security/WebauthnFactory.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
private const PRIORITY = 0;
8989

9090
public function __construct(
91-
private WebauthnServicesFactory $servicesFactory
91+
private WebauthnServicesFactory $servicesFactory,
9292
) {
9393
}
9494

@@ -112,6 +112,7 @@ public function addConfiguration(NodeDefinition $builder): void
112112
->defaultNull()
113113
->end()
114114
->scalarNode('options_storage')
115+
->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.')
115116
->defaultValue(self::DEFAULT_SESSION_STORAGE_SERVICE)
116117
->end()
117118
->scalarNode('success_handler')
@@ -239,7 +240,6 @@ public function createAuthenticator(
239240
$config['success_handler'],
240241
$config['failure_handler'],
241242
$firewallConfigId,
242-
$config['options_storage'],
243243
$authenticatorAssertionResponseValidatorId,
244244
$authenticatorAttestationResponseValidatorId
245245
);
@@ -263,7 +263,6 @@ private function createAuthenticatorService(
263263
string $successHandlerId,
264264
string $failureHandlerId,
265265
string $firewallConfigId,
266-
string $optionsStorageId,
267266
string $authenticatorAssertionResponseValidatorId,
268267
string $authenticatorAttestationResponseValidatorId
269268
): string {
@@ -274,7 +273,6 @@ private function createAuthenticatorService(
274273
->replaceArgument(1, new Reference($userProviderId))
275274
->replaceArgument(2, new Reference($successHandlerId))
276275
->replaceArgument(3, new Reference($failureHandlerId))
277-
->replaceArgument(4, new Reference($optionsStorageId))
278276
->replaceArgument(8, new Reference($authenticatorAssertionResponseValidatorId))
279277
->replaceArgument(9, new Reference($authenticatorAttestationResponseValidatorId))
280278
->addMethodCall('setLogger', [new Reference('webauthn.logger')]);
@@ -302,10 +300,10 @@ private function createAssertionControllersAndRoutes(
302300
$config['authentication']['routes']['options_path'],
303301
$config['authentication']['routes']['host'],
304302
$optionsBuilderId,
305-
$config['options_storage'],
306303
$config['authentication']['options_handler'],
307304
$config['failure_handler'],
308305
);
306+
if ($config['authentication']['routes']['result_path'] !== null) {
309307
$this->createResponseControllerAndRoute(
310308
$container,
311309
$firewallName,
@@ -314,6 +312,7 @@ private function createAssertionControllersAndRoutes(
314312
$config['authentication']['routes']['result_path'],
315313
$config['authentication']['routes']['host']
316314
);
315+
}
317316
}
318317

319318
/**
@@ -336,10 +335,10 @@ private function createAttestationControllersAndRoutes(
336335
$config['registration']['routes']['options_path'],
337336
$config['registration']['routes']['host'],
338337
$optionsBuilderId,
339-
$config['options_storage'],
340338
$config['registration']['options_handler'],
341339
$config['failure_handler'],
342340
);
341+
if ($config['registration']['routes']['result_path'] !== null) {
343342
$this->createResponseControllerAndRoute(
344343
$container,
345344
$firewallName,
@@ -348,6 +347,7 @@ private function createAttestationControllersAndRoutes(
348347
$config['registration']['routes']['result_path'],
349348
$config['registration']['routes']['host']
350349
);
350+
}
351351
}
352352

353353
private function createAssertionRequestControllerAndRoute(
@@ -357,15 +357,13 @@ private function createAssertionRequestControllerAndRoute(
357357
string $path,
358358
?string $host,
359359
string $optionsBuilderId,
360-
string $optionsStorageId,
361360
string $optionsHandlerId,
362361
string $failureHandlerId,
363362
): void {
364363
$controller = (new Definition(AssertionRequestController::class))
365364
->setFactory([new Reference(AssertionControllerFactory::class), 'createRequestController'])
366365
->setArguments([
367366
new Reference($optionsBuilderId),
368-
new Reference($optionsStorageId),
369367
new Reference($optionsHandlerId),
370368
new Reference($failureHandlerId),
371369
]);
@@ -388,7 +386,6 @@ private function createAttestationRequestControllerAndRoute(
388386
string $path,
389387
?string $host,
390388
string $optionsBuilderId,
391-
string $optionsStorageId,
392389
string $optionsHandlerId,
393390
string $failureHandlerId,
394391
): void {
@@ -397,7 +394,6 @@ private function createAttestationRequestControllerAndRoute(
397394
->setArguments([
398395
new Reference($optionsBuilderId),
399396
new Reference(RequestBodyUserEntityGuesser::class),
400-
new Reference($optionsStorageId),
401397
new Reference($optionsHandlerId),
402398
new Reference($failureHandlerId),
403399
true,

0 commit comments

Comments
 (0)