Skip to content

Commit a432c50

Browse files
committed
Add Webauthn authentication support
Introduce WebauthnBadge, WebauthnPassport, and WebauthnAuthenticator implementations to support Webauthn-based authentication. Update services and configuration to handle Webauthn-specific dependencies and options, and add support for customizable storage and routing configurations. This implementation enhances security options by enabling passwordless authentication.
1 parent 88b9c85 commit a432c50

File tree

15 files changed

+443
-108
lines changed

15 files changed

+443
-108
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 & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ 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,
12-
requestSuccessRedirectUri: (_a = this.requestSuccessRedirectUriValue) !== null && _a !== void 0 ? _a : null,
12+
requestResultField: (_a = this.requestResultFieldValue) !== null && _a !== undefined ? _a : null,
13+
requestSuccessRedirectUri: (_b = this.requestSuccessRedirectUriValue) !== null && _b !== undefined ? _b : null,
1314
creationResultUrl: this.creationResultUrlValue,
1415
creationOptionsUrl: this.creationOptionsUrlValue,
15-
creationSuccessRedirectUri: (_b = this.creationSuccessRedirectUriValue) !== null && _b !== void 0 ? _b : null,
16+
creationSuccessRedirectUri: (_c = this.creationSuccessRedirectUriValue) !== null && _c !== undefined ? _c : null,
1617
};
1718
this._dispatchEvent('webauthn:connect', { options });
1819
const supportAutofill = await browserSupportsWebAuthnAutofill();
@@ -38,9 +39,16 @@ class default_1 extends Controller {
3839
this._processSignin(optionsResponseJson, false);
3940
}
4041
async _processSignin(optionsResponseJson, useBrowserAutofill) {
42+
var _a;
4143
try {
4244
const authenticatorResponse = await startAuthentication(optionsResponseJson, useBrowserAutofill);
4345
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
46+
console.log(authenticatorResponse, this.requestResultFieldValue, this.element instanceof HTMLFormElement);
47+
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
48+
(_a = this.element.querySelector(this.requestResultFieldValue)) === null || _a === void 0 ? void 0 : _a.setAttribute('value', JSON.stringify(authenticatorResponse));
49+
this.element.submit();
50+
return;
51+
}
4452
const assertionResponse = await this._getAssertionResponse(authenticatorResponse);
4553
if (assertionResponse !== false && this.requestSuccessRedirectUriValue) {
4654
window.location.replace(this.requestSuccessRedirectUriValue);
@@ -151,6 +159,7 @@ class default_1 extends Controller {
151159
default_1.values = {
152160
requestResultUrl: { type: String, default: '/request' },
153161
requestOptionsUrl: { type: String, default: '/request/options' },
162+
requestResultField: { type: String, default: null },
154163
requestSuccessRedirectUri: String,
155164
creationResultUrl: { type: String, default: '/creation' },
156165
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(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.')
@@ -314,7 +318,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
314318
->defaultValue(Request::METHOD_POST)
315319
->end()
316320
->scalarNode('result_path')
317-
->isRequired()
321+
->defaultNull()
318322
->end()
319323
->scalarNode('host')
320324
->defaultValue(null)
@@ -338,6 +342,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
338342
->defaultFalse()
339343
->end()
340344
->scalarNode('options_storage')
345+
->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.')
341346
->defaultValue(SessionStorage::class)
342347
->info('Service responsible of the options/user entity storage during the ceremony')
343348
->end()
@@ -379,7 +384,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
379384
->defaultValue(Request::METHOD_POST)
380385
->end()
381386
->scalarNode('result_path')
382-
->isRequired()
387+
->defaultNull()
383388
->end()
384389
->scalarNode('host')
385390
->defaultValue(null)
@@ -394,6 +399,7 @@ private function addControllersConfig(ArrayNodeDefinition $rootNode): void
394399
->defaultNull()
395400
->end()
396401
->scalarNode('options_storage')
402+
->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.')
397403
->defaultValue(SessionStorage::class)
398404
->info('Service responsible of the options/user entity storage during the ceremony')
399405
->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')
@@ -232,7 +233,6 @@ public function createAuthenticator(
232233
$config['success_handler'],
233234
$config['failure_handler'],
234235
$firewallConfigId,
235-
$config['options_storage'],
236236
$authenticatorAssertionResponseValidatorId,
237237
$authenticatorAttestationResponseValidatorId
238238
);
@@ -256,7 +256,6 @@ private function createAuthenticatorService(
256256
string $successHandlerId,
257257
string $failureHandlerId,
258258
string $firewallConfigId,
259-
string $optionsStorageId,
260259
string $authenticatorAssertionResponseValidatorId,
261260
string $authenticatorAttestationResponseValidatorId
262261
): string {
@@ -267,7 +266,6 @@ private function createAuthenticatorService(
267266
->replaceArgument(1, new Reference($userProviderId))
268267
->replaceArgument(2, new Reference($successHandlerId))
269268
->replaceArgument(3, new Reference($failureHandlerId))
270-
->replaceArgument(4, new Reference($optionsStorageId))
271269
->replaceArgument(8, new Reference($authenticatorAssertionResponseValidatorId))
272270
->replaceArgument(9, new Reference($authenticatorAttestationResponseValidatorId))
273271
->addMethodCall('setLogger', [new Reference('webauthn.logger')]);
@@ -295,10 +293,10 @@ private function createAssertionControllersAndRoutes(
295293
$config['authentication']['routes']['options_path'],
296294
$config['authentication']['routes']['host'],
297295
$optionsBuilderId,
298-
$config['options_storage'],
299296
$config['authentication']['options_handler'],
300297
$config['failure_handler'],
301298
);
299+
if ($config['authentication']['routes']['result_path'] !== null) {
302300
$this->createResponseControllerAndRoute(
303301
$container,
304302
$firewallName,
@@ -307,6 +305,7 @@ private function createAssertionControllersAndRoutes(
307305
$config['authentication']['routes']['result_path'],
308306
$config['authentication']['routes']['host']
309307
);
308+
}
310309
}
311310

312311
/**
@@ -329,10 +328,10 @@ private function createAttestationControllersAndRoutes(
329328
$config['registration']['routes']['options_path'],
330329
$config['registration']['routes']['host'],
331330
$optionsBuilderId,
332-
$config['options_storage'],
333331
$config['registration']['options_handler'],
334332
$config['failure_handler'],
335333
);
334+
if ($config['registration']['routes']['result_path'] !== null) {
336335
$this->createResponseControllerAndRoute(
337336
$container,
338337
$firewallName,
@@ -341,6 +340,7 @@ private function createAttestationControllersAndRoutes(
341340
$config['registration']['routes']['result_path'],
342341
$config['registration']['routes']['host']
343342
);
343+
}
344344
}
345345

346346
private function createAssertionRequestControllerAndRoute(
@@ -350,15 +350,13 @@ private function createAssertionRequestControllerAndRoute(
350350
string $path,
351351
?string $host,
352352
string $optionsBuilderId,
353-
string $optionsStorageId,
354353
string $optionsHandlerId,
355354
string $failureHandlerId,
356355
): void {
357356
$controller = (new Definition(AssertionRequestController::class))
358357
->setFactory([new Reference(AssertionControllerFactory::class), 'createRequestController'])
359358
->setArguments([
360359
new Reference($optionsBuilderId),
361-
new Reference($optionsStorageId),
362360
new Reference($optionsHandlerId),
363361
new Reference($failureHandlerId),
364362
]);
@@ -381,7 +379,6 @@ private function createAttestationRequestControllerAndRoute(
381379
string $path,
382380
?string $host,
383381
string $optionsBuilderId,
384-
string $optionsStorageId,
385382
string $optionsHandlerId,
386383
string $failureHandlerId,
387384
): void {
@@ -390,7 +387,6 @@ private function createAttestationRequestControllerAndRoute(
390387
->setArguments([
391388
new Reference($optionsBuilderId),
392389
new Reference(RequestBodyUserEntityGuesser::class),
393-
new Reference($optionsStorageId),
394390
new Reference($optionsHandlerId),
395391
new Reference($failureHandlerId),
396392
true,

0 commit comments

Comments
 (0)