Skip to content

Commit 41389bc

Browse files
ForgeRockEmmathomas-schofield-fr
authored andcommitted
feat: ability to override WebAuthn options for authentication AME-33781
1 parent 1fb1e57 commit 41389bc

File tree

3 files changed

+48
-34
lines changed

3 files changed

+48
-34
lines changed

.changeset/thirty-rules-film.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
2-
"@forgerock/javascript-sdk": patch
2+
'@forgerock/javascript-sdk': patch
33
---
44

55
WebAuthn improvements
6-
* Fix parsing of WebAuthn scripts when `asScript` is true
7-
* Improve handling when conditional mediation is not supported
8-
* Enable re-invocation of WebAuthn requests
6+
7+
- Fix parsing of WebAuthn scripts when `asScript` is true
8+
- Improve handling when conditional mediation is not supported
9+
- Enable re-invocation of WebAuthn requests
10+
- Enable modification of options passed to navigator.credentials.get()

packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ describe('Test FRWebAuthn class with Conditional UI', () => {
131131

132132
it('should detect if conditional UI is supported', async () => {
133133
vi.spyOn(window.PublicKeyCredential, 'isConditionalMediationAvailable').mockResolvedValue(true);
134-
const isSupported = await FRWebAuthn.isConditionalUISupported();
134+
const isSupported = await FRWebAuthn.isConditionalMediationSupported();
135135
expect(isSupported).toBe(true);
136136
});
137137

packages/javascript-sdk/src/fr-webauthn/index.ts

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,19 @@ declare global {
7171
* }
7272
* ```
7373
*
74-
* Conditional UI (Autofill) Support:
74+
* Conditional mediation (Autofill) Support:
7575
*
7676
* ```js
77-
* // Check if browser supports conditional UI
77+
* // Check if browser supports conditional mediation
7878
* const supportsConditionalUI = await FRWebAuthn.isConditionalUISupported();
7979
*
8080
* if (supportsConditionalUI) {
81-
* // The authenticate() method automatically handles conditional UI
81+
* // The authenticate() method automatically handles conditional mediation
8282
* // when the server indicates support via conditionalWebAuthn: true
8383
* // in the metadata. No additional code changes needed.
8484
* await FRWebAuthn.authenticate(step);
8585
*
86-
* // For conditional UI to work in the browser, add autocomplete="webauthn"
86+
* // For conditional mediation to work in the browser, add autocomplete="webauthn"
8787
* // to your username input field:
8888
* // <input type="text" name="username" autocomplete="webauthn" />
8989
* }
@@ -123,12 +123,21 @@ abstract class FRWebAuthn {
123123
}
124124

125125
/**
126-
* Checks if the browser supports conditional UI (autofill) for WebAuthn.
126+
* Checks if the browser supports WebAuthn.
127+
*
128+
* @return boolean indicating if WebAuthn is available
129+
*/
130+
public static isWebAuthnSupported(): boolean {
131+
return !!window.PublicKeyCredential;
132+
}
133+
134+
/**
135+
* Checks if the browser supports conditional mediation (autofill) for WebAuthn.
127136
*
128137
* @return Promise<boolean> indicating if conditional mediation is available
129138
*/
130-
public static async isConditionalUISupported(): Promise<boolean> {
131-
if (!window.PublicKeyCredential) {
139+
public static async isConditionalMediationSupported(): Promise<boolean> {
140+
if (!this.isWebAuthnSupported()) {
132141
return false;
133142
}
134143

@@ -144,52 +153,49 @@ abstract class FRWebAuthn {
144153

145154
/**
146155
* Populates the step with the necessary authentication outcome.
147-
* Automatically handles conditional UI if indicated by the server metadata.
156+
* Automatically handles conditional mediation if indicated by the server metadata.
148157
*
149158
* @param step The step that contains WebAuthn authentication data
159+
* @param optionsTransformer Augments the derived options with custom behaviour
150160
* @return The populated step
151161
*/
152-
public static async authenticate(step: FRStep): Promise<FRStep> {
162+
public static async authenticate(
163+
step: FRStep,
164+
optionsTransformer: (options: CredentialRequestOptions) => CredentialRequestOptions = (
165+
options,
166+
) => options,
167+
): Promise<FRStep> {
153168
const { hiddenCallback, metadataCallback, textOutputCallback } = this.getCallbacks(step);
154169
if (hiddenCallback && (metadataCallback || textOutputCallback)) {
155-
let outcome: ReturnType<typeof this.getAuthenticationOutcome>;
156-
let credential: PublicKeyCredential | null = null;
170+
const options: CredentialRequestOptions = {};
157171

158172
try {
159-
let publicKey: PublicKeyCredentialRequestOptions;
160-
161173
if (metadataCallback) {
162174
const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
163175
const mediation = meta.mediation as CredentialMediationRequirement;
164176

165177
if (mediation === 'conditional') {
166-
const isConditionalSupported = await this.isConditionalUISupported();
167-
if (!isConditionalSupported) {
178+
const isConditionalMediationSupported = await this.isConditionalMediationSupported();
179+
if (!isConditionalMediationSupported) {
168180
const e = new Error(
169-
'Conditional UI was requested, but is not supported by this browser.',
181+
'Conditional mediation was requested, but is not supported by this browser.',
170182
);
171183
e.name = WebAuthnOutcomeType.NotSupportedError;
172184
throw e;
173185
}
174186
}
175187

176-
publicKey = this.createAuthenticationPublicKey(meta);
177-
178-
credential = await this.getAuthenticationCredential({ publicKey, mediation });
179-
outcome = this.getAuthenticationOutcome(credential);
188+
options.publicKey = this.createAuthenticationPublicKey(meta);
189+
options.mediation = mediation;
180190
} else if (textOutputCallback) {
181191
const metadata = this.extractMetadata(textOutputCallback.getMessage());
182-
183192
if (metadata) {
184-
publicKey = this.createAuthenticationPublicKey(
193+
options.publicKey = this.createAuthenticationPublicKey(
185194
metadata as WebAuthnAuthenticationMetadata,
186195
);
187196
} else {
188-
publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
197+
options.publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage());
189198
}
190-
191-
credential = await this.getAuthenticationCredential({ publicKey });
192-
outcome = this.getAuthenticationOutcome(credential);
193199
} else {
194200
throw new Error('No Credential found from Public Key');
195201
}
@@ -204,6 +210,12 @@ abstract class FRWebAuthn {
204210
throw error;
205211
}
206212

213+
const credential: PublicKeyCredential | null = await this.getAuthenticationCredential(
214+
optionsTransformer(options),
215+
);
216+
const outcome: ReturnType<typeof this.getAuthenticationOutcome> =
217+
this.getAuthenticationOutcome(credential);
218+
207219
if (metadataCallback) {
208220
const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata;
209221
if (meta?.supportsJsonResponse && credential && 'authenticatorAttachment' in credential) {
@@ -379,7 +391,7 @@ abstract class FRWebAuthn {
379391
options: CredentialRequestOptions,
380392
): Promise<PublicKeyCredential | null> {
381393
// Feature check before we attempt authenticating
382-
if (!window.PublicKeyCredential) {
394+
if (!this.isWebAuthnSupported()) {
383395
const e = new Error('PublicKeyCredential not supported by this browser');
384396
e.name = WebAuthnOutcomeType.NotSupportedError;
385397
throw e;
@@ -457,7 +469,7 @@ abstract class FRWebAuthn {
457469
options: PublicKeyCredentialCreationOptions,
458470
): Promise<PublicKeyCredential | null> {
459471
// Feature check before we attempt registering a device
460-
if (!window.PublicKeyCredential) {
472+
if (this.isWebAuthnSupported()) {
461473
const e = new Error('PublicKeyCredential not supported by this browser');
462474
e.name = WebAuthnOutcomeType.NotSupportedError;
463475
throw e;
@@ -534,7 +546,7 @@ abstract class FRWebAuthn {
534546
challenge: Uint8Array.from(atob(challenge), (c) => c.charCodeAt(0)).buffer,
535547
timeout,
536548
};
537-
// For conditional UI, allowCredentials can be omitted.
549+
// For conditional mediation, allowCredentials can be omitted.
538550
// For standard WebAuthn, it may or may not be present.
539551
// Only add the property if the array is not empty.
540552
if (allowCredentialsValue && allowCredentialsValue.length > 0) {

0 commit comments

Comments
 (0)