Skip to content

Commit a9fa15d

Browse files
authored
Merge pull request #704 from web-auth/features/defined-extensions
Add WebAuthn extensions and integrate PRF handling logic
2 parents 45a05b2 + 053097e commit a9fa15d

12 files changed

+357
-12
lines changed

phpstan-baseline.neon

+18
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

+6
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

+58-4
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

+99-5
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
}
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+
}
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+
}
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+
}
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+
}
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)