Skip to content

Commit 053097e

Browse files
committed
Add WebAuthn authentication extensions support
Implemented multiple WebAuthn extensions, including AppId, Uvm, CredentialProperties, LargeBlob, and PseudoRandomFunction, to enhance authentication capabilities. Updated Stimulus controller to handle input/output processing of these extensions, enabling seamless integration with WebAuthn flows. These changes improve flexibility and support for advanced use cases.
1 parent 45a05b2 commit 053097e

12 files changed

+357
-12
lines changed

phpstan-baseline.neon

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,24 @@ parameters:
15211521
count: 1
15221522
path: src/webauthn/src/AuthenticationExtensions/AuthenticationExtensions.php
15231523

1524+
-
1525+
message: '#^Cannot access offset string on mixed\.$#'
1526+
identifier: offsetAccess.nonOffsetAccessible
1527+
count: 1
1528+
path: src/webauthn/src/AuthenticationExtensions/PseudoRandomFunctionInputExtensionBuilder.php
1529+
1530+
-
1531+
message: '#^PHPDoc tag @var has invalid value \(array\{eval\?\: array\{first\: string, second\?\: string\}, evalByCredential\?\: array\<string, array\{first\: string, second\?\: string\}\>\)\: Unexpected token "\*/", expected ''\}'' at offset 145 on line 3$#'
1532+
identifier: phpDoc.parseError
1533+
count: 1
1534+
path: src/webauthn/src/AuthenticationExtensions/PseudoRandomFunctionInputExtensionBuilder.php
1535+
1536+
-
1537+
message: '#^Property Webauthn\\AuthenticationExtensions\\PseudoRandomFunctionInputExtensionBuilder\:\:\$values type has no value type specified in iterable type array\.$#'
1538+
identifier: missingType.iterableValue
1539+
count: 1
1540+
path: src/webauthn/src/AuthenticationExtensions/PseudoRandomFunctionInputExtensionBuilder.php
1541+
15241542
-
15251543
message: '#^Cannot access offset 1 on array\|false\.$#'
15261544
identifier: offsetAccess.nonOffsetAccessible

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,10 @@ export default class extends Controller {
9393
private _getAttestationResponse;
9494
private _getAssertionResponse;
9595
private _getResult;
96+
private _processExtensionsInput;
97+
private _processPrfInput;
98+
private _importPrfValues;
99+
private _processExtensionsOutput;
100+
private _processPrfOutput;
101+
private _exportPrfValues;
96102
}

src/stimulus/assets/dist/controller.js

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Controller } from '@hotwired/stimulus';
2-
import { browserSupportsWebAuthnAutofill, browserSupportsWebAuthn, startAuthentication, startRegistration } from '@simplewebauthn/browser';
2+
import { browserSupportsWebAuthnAutofill, browserSupportsWebAuthn, startAuthentication, startRegistration, base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser';
33

44
class default_1 extends Controller {
55
constructor() {
@@ -42,7 +42,9 @@ class default_1 extends Controller {
4242
async _processSignin(optionsResponseJson, useBrowserAutofill) {
4343
var _a;
4444
try {
45-
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
45+
optionsResponseJson = this._processExtensionsInput(optionsResponseJson);
46+
let authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
47+
authenticatorResponse = this._processExtensionsOutput(authenticatorResponse);
4648
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
4749
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
4850
(_a = this.element.querySelector(this.requestResultFieldValue)) === null || _a === void 0 ? void 0 : _a.setAttribute('value', JSON.stringify(authenticatorResponse));
@@ -67,11 +69,13 @@ class default_1 extends Controller {
6769
return;
6870
}
6971
event.preventDefault();
70-
const optionsResponseJson = await this._getPublicKeyCredentialCreationOptions(null);
72+
let optionsResponseJson = await this._getPublicKeyCredentialCreationOptions(null);
7173
if (!optionsResponseJson) {
7274
return;
7375
}
74-
const authenticatorResponse = await startRegistration({ optionsJSON: optionsResponseJson });
76+
optionsResponseJson = this._processExtensionsInput(optionsResponseJson);
77+
let authenticatorResponse = await startRegistration({ optionsJSON: optionsResponseJson });
78+
authenticatorResponse = this._processExtensionsOutput(authenticatorResponse);
7579
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
7680
if (this.creationResultFieldValue && this.element instanceof HTMLFormElement) {
7781
(_a = this.element.querySelector(this.creationResultFieldValue)) === null || _a === void 0 ? void 0 : _a.setAttribute('value', JSON.stringify(authenticatorResponse));
@@ -161,6 +165,56 @@ class default_1 extends Controller {
161165
this._dispatchEvent(eventPrefix + 'success', { data: attestationResponseJSON });
162166
return attestationResponseJSON;
163167
}
168+
_processExtensionsInput(options) {
169+
if (!options || !options.extensions) {
170+
return options;
171+
}
172+
if (options.extensions.prf) {
173+
options.extensions.prf = this._processPrfInput(options.extensions.prf);
174+
}
175+
return options;
176+
}
177+
_processPrfInput(prf) {
178+
if (prf.eval) {
179+
prf.eval = this._importPrfValues(eval);
180+
}
181+
if (prf.evalByCredential) {
182+
Object.keys(prf.evalByCredential).forEach((key) => {
183+
prf.evalByCredential[key] = this._importPrfValues(prf.evalByCredential[key]);
184+
});
185+
}
186+
return prf;
187+
}
188+
_importPrfValues(values) {
189+
values.first = base64URLStringToBuffer(values.first);
190+
if (values.second) {
191+
values.second = base64URLStringToBuffer(values.second);
192+
}
193+
return values;
194+
}
195+
_processExtensionsOutput(options) {
196+
if (!options || !options.extensions) {
197+
return options;
198+
}
199+
if (options.extensions.prf) {
200+
options.extensions.prf = this._processPrfOutput(options.extensions.prf);
201+
}
202+
return options;
203+
}
204+
_processPrfOutput(prf) {
205+
if (!prf.result) {
206+
return prf;
207+
}
208+
prf.result = this._exportPrfValues(prf.result);
209+
return prf;
210+
}
211+
_exportPrfValues(values) {
212+
values.first = bufferToBase64URLString(values.first);
213+
if (values.second) {
214+
values.second = bufferToBase64URLString(values.second);
215+
}
216+
return values;
217+
}
164218
}
165219
default_1.values = {
166220
requestResultUrl: { type: String, default: '/request' },

src/stimulus/assets/src/controller.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import { Controller } from '@hotwired/stimulus';
44
import {
55
AuthenticationResponseJSON,
6-
RegistrationResponseJSON
6+
RegistrationResponseJSON,
7+
PublicKeyCredentialRequestOptionsJSON,
8+
PublicKeyCredentialCreationOptionsJSON
79
} from '@simplewebauthn/types';
8-
import { browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, startAuthentication, startRegistration } from '@simplewebauthn/browser';
10+
import { browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, startAuthentication, startRegistration, base64URLStringToBuffer, bufferToBase64URLString } from '@simplewebauthn/browser';
911

1012
export default class extends Controller {
1113
static values = {
@@ -89,7 +91,11 @@ export default class extends Controller {
8991
private async _processSignin(optionsResponseJson: Object, useBrowserAutofill: boolean): Promise<void> {
9092
try {
9193
// @ts-ignore
92-
const authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
94+
optionsResponseJson = this._processExtensionsInput(optionsResponseJson);
95+
// @ts-ignore
96+
let authenticatorResponse = await startAuthentication({ optionsJSON: optionsResponseJson, useBrowserAutofill });
97+
// @ts-ignore
98+
authenticatorResponse = this._processExtensionsOutput(authenticatorResponse);
9399
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
94100
if (this.requestResultFieldValue && this.element instanceof HTMLFormElement) {
95101
this.element.querySelector(this.requestResultFieldValue)?.setAttribute('value', JSON.stringify(authenticatorResponse));
@@ -114,13 +120,16 @@ export default class extends Controller {
114120
return;
115121
}
116122
event.preventDefault();
117-
const optionsResponseJson = await this._getPublicKeyCredentialCreationOptions(null);
123+
let optionsResponseJson = await this._getPublicKeyCredentialCreationOptions(null);
118124
if (!optionsResponseJson) {
119125
return;
120126
}
121127

128+
optionsResponseJson = this._processExtensionsInput(optionsResponseJson);
129+
// @ts-ignore
130+
let authenticatorResponse = await startRegistration({ optionsJSON: optionsResponseJson });
122131
// @ts-ignore
123-
const authenticatorResponse = await startRegistration({ optionsJSON: optionsResponseJson });
132+
authenticatorResponse = this._processExtensionsOutput(authenticatorResponse);
124133
this._dispatchEvent('webauthn:authenticator:response', { response: authenticatorResponse });
125134
if (this.creationResultFieldValue && this.element instanceof HTMLFormElement) {
126135
this.element.querySelector(this.creationResultFieldValue)?.setAttribute('value', JSON.stringify(authenticatorResponse));
@@ -228,4 +237,89 @@ export default class extends Controller {
228237

229238
return attestationResponseJSON;
230239
}
240+
241+
private _processExtensionsInput(options: Object|PublicKeyCredentialRequestOptionsJSON|PublicKeyCredentialCreationOptionsJSON): Object|PublicKeyCredentialRequestOptionsJSON|PublicKeyCredentialCreationOptionsJSON {
242+
// @ts-ignore
243+
if (!options || !options.extensions) {
244+
return options;
245+
}
246+
247+
// @ts-ignore
248+
if (options.extensions.prf) {
249+
// @ts-ignore
250+
options.extensions.prf = this._processPrfInput(options.extensions.prf);
251+
}
252+
253+
return options;
254+
}
255+
256+
private _processPrfInput(prf: Object): Object {
257+
// @ts-ignore
258+
if (prf.eval) {
259+
// @ts-ignore
260+
prf.eval = this._importPrfValues(eval);
261+
}
262+
263+
// @ts-ignore
264+
if (prf.evalByCredential) {
265+
// @ts-ignore
266+
Object.keys(prf.evalByCredential).forEach((key) => {
267+
// @ts-ignore
268+
prf.evalByCredential[key] = this._importPrfValues(prf.evalByCredential[key]);
269+
});
270+
}
271+
272+
return prf;
273+
}
274+
275+
private _importPrfValues(values: Object): Object {
276+
// @ts-ignore
277+
values.first = base64URLStringToBuffer(values.first);
278+
// @ts-ignore
279+
if (values.second) {
280+
// @ts-ignore
281+
values.second = base64URLStringToBuffer(values.second);
282+
}
283+
284+
return values;
285+
}
286+
287+
private _processExtensionsOutput(options: Object|AuthenticationResponseJSON|RegistrationResponseJSON): Object|PublicKeyCredentialRequestOptionsJSON|PublicKeyCredentialCreationOptionsJSON {
288+
// @ts-ignore
289+
if (!options || !options.extensions) {
290+
return options;
291+
}
292+
293+
// @ts-ignore
294+
if (options.extensions.prf) {
295+
// @ts-ignore
296+
options.extensions.prf = this._processPrfOutput(options.extensions.prf);
297+
}
298+
299+
return options;
300+
}
301+
302+
private _processPrfOutput(prf: Object): Object {
303+
// @ts-ignore
304+
if (!prf.result) {
305+
return prf
306+
}
307+
308+
// @ts-ignore
309+
prf.result = this._exportPrfValues(prf.result);
310+
311+
return prf;
312+
}
313+
314+
private _exportPrfValues(values: Object): Object {
315+
// @ts-ignore
316+
values.first = bufferToBase64URLString(values.first);
317+
// @ts-ignore
318+
if (values.second) {
319+
// @ts-ignore
320+
values.second = bufferToBase64URLString(values.second);
321+
}
322+
323+
return values;
324+
}
231325
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
final class AppIdExcludeInputExtension extends AuthenticationExtension
8+
{
9+
public static function enable(string $value): AuthenticationExtension
10+
{
11+
return self::create('appidExclude', $value);
12+
}
13+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
final class AppIdInputExtension extends AuthenticationExtension
8+
{
9+
public static function enable(): AuthenticationExtension
10+
{
11+
return self::create('appid', true);
12+
}
13+
14+
public static function disable(): AuthenticationExtension
15+
{
16+
return self::create('appid', false);
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
final class CredentialPropertiesInputExtension extends AuthenticationExtension
8+
{
9+
public static function enable(): AuthenticationExtension
10+
{
11+
return self::create('credProps', true);
12+
}
13+
14+
public static function disable(): AuthenticationExtension
15+
{
16+
return self::create('credProps', false);
17+
}
18+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
use function assert;
8+
use function in_array;
9+
10+
final class LargeBlobInputExtension extends AuthenticationExtension
11+
{
12+
public const REQUIRED = 'required';
13+
14+
public const PREFERRED = 'preferred';
15+
16+
public static function support(string $support): AuthenticationExtension
17+
{
18+
assert(in_array($support, [self::REQUIRED, self::PREFERRED], true), 'Invalid support value.');
19+
20+
return self::create('largeBlob', [
21+
'support' => $support,
22+
]);
23+
}
24+
25+
public static function read(): AuthenticationExtension
26+
{
27+
return self::create('largeBlob', [
28+
'read' => true,
29+
]);
30+
}
31+
32+
public static function write(string $value): AuthenticationExtension
33+
{
34+
return self::create('largeBlob', [
35+
'write' => $value,
36+
]);
37+
}
38+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
final class PseudoRandomFunctionInputExtension extends AuthenticationExtension
8+
{
9+
}

0 commit comments

Comments
 (0)