Skip to content

Commit 18a698b

Browse files
committed
Add WebAuthn extensions and integrate PRF handling logic
Introduced multiple WebAuthn extensions, including AppId, CredentialProperties, LargeBlob, and PRF extensions, alongside their corresponding builders and methods. Updated the controller to process WebAuthn extension inputs and outputs, specifically adding support for PRF data transformation. Ensured seamless integration with the WebAuthn API by implementing base64-to-buffer conversions and vice versa.
1 parent a05dc1a commit 18a698b

11 files changed

+324
-12
lines changed

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 CredentialPropropertiesInputExtension 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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
final class LargeBlobInputExtension extends AuthenticationExtension
8+
{
9+
public const REQUIRED = 'required';
10+
public const PREFERRED = 'preferred';
11+
12+
public static function support(string $support): AuthenticationExtension
13+
{
14+
assert(in_array($support, [self::REQUIRED, self::PREFERRED], true), 'Invalid support value.');
15+
16+
return self::create('largeBlob', ['support' => $support]);
17+
}
18+
19+
public static function read(): AuthenticationExtension
20+
{
21+
return self::create('largeBlob', ['read' => true]);
22+
}
23+
24+
public static function write(string $value): AuthenticationExtension
25+
{
26+
return self::create('largeBlob', ['write' => $value]);
27+
}
28+
}
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+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Webauthn\AuthenticationExtensions;
6+
7+
use ParagonIE\ConstantTime\Base64UrlSafe;
8+
9+
final class PseudoRandomFunctionInputExtensionBuilder
10+
{
11+
/**
12+
* @var array{eval?: array{first: string, second?: string}, evalByCredential?: array<string, array{first: string, second?: string}>
13+
*/
14+
private array $values = [];
15+
16+
private function __construct()
17+
{
18+
}
19+
20+
public static function create(): self
21+
{
22+
return new self();
23+
}
24+
25+
public function withInputs(string $first, null|string $second = null): self
26+
{
27+
$eval = ['first' => Base64UrlSafe::encodeUnpadded($first)];
28+
if ($second !== null) {
29+
$eval['second'] = Base64UrlSafe::encodeUnpadded($second);
30+
}
31+
$this->values['eval'] = $eval;
32+
33+
return $this;
34+
}
35+
36+
public function withCredentialInputs(string $credentialId, string $first, null|string $second = null): self
37+
{
38+
$eval = ['first' => Base64UrlSafe::encodeUnpadded($first)];
39+
if ($second !== null) {
40+
$eval['second'] = Base64UrlSafe::encodeUnpadded($second);
41+
}
42+
if (!array_key_exists('evalByCredential', $this->values)) {
43+
$this->values['evalByCredential'] = [];
44+
}
45+
$this->values['evalByCredential'][$credentialId] = $eval;
46+
47+
return $this;
48+
}
49+
50+
public function build(): PseudoRandomFunctionInputExtension
51+
{
52+
return new PseudoRandomFunctionInputExtension('prf', $this->values);
53+
}
54+
}

0 commit comments

Comments
 (0)