@@ -13,6 +13,7 @@ import { CallbackType } from '../auth/enums';
1313import type HiddenValueCallback from '../fr-auth/callbacks/hidden-value-callback' ;
1414import type MetadataCallback from '../fr-auth/callbacks/metadata-callback' ;
1515import type FRStep from '../fr-auth/fr-step' ;
16+ import { FRLogger } from '../util/logger' ;
1617import { WebAuthnOutcome , WebAuthnOutcomeType , WebAuthnStepType } from './enums' ;
1718import {
1819 arrayBufferToString ,
@@ -30,6 +31,7 @@ import type {
3031} from './interfaces' ;
3132import type TextOutputCallback from '../fr-auth/callbacks/text-output-callback' ;
3233import { parseWebAuthnAuthenticateText , parseWebAuthnRegisterText } from './script-parser' ;
34+ import { withTimeout } from '../util/timeout' ;
3335
3436// <clientdata>::<attestation>::<publickeyCredential>::<DeviceName>
3537type OutcomeWithName <
@@ -44,6 +46,8 @@ type OutcomeWithName<
4446type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMetadata ;
4547// Script-based WebAuthn
4648type WebAuthnTextOutput = WebAuthnTextOutputRegistration ;
49+ const TWO_SECOND = 2000 ;
50+
4751/**
4852 * Utility for integrating a web browser's WebAuthn API.
4953 *
@@ -60,6 +64,24 @@ type WebAuthnTextOutput = WebAuthnTextOutputRegistration;
6064 * await FRWebAuthn.authenticate(step);
6165 * }
6266 * ```
67+ *
68+ * Conditional UI (Autofill) Support:
69+ *
70+ * ```js
71+ * // Check if browser supports conditional UI
72+ * const supportsConditionalUI = await FRWebAuthn.isConditionalUISupported();
73+ *
74+ * if (supportsConditionalUI) {
75+ * // The authenticate() method automatically handles conditional UI
76+ * // when the server indicates support via conditionalWebAuthn: true
77+ * // in the metadata. No additional code changes needed.
78+ * await FRWebAuthn.authenticate(step);
79+ *
80+ * // For conditional UI to work in the browser, add autocomplete="webauthn"
81+ * // to your username input field:
82+ * // <input type="text" name="username" autocomplete="webauthn" />
83+ * }
84+ * ```
6385 */
6486abstract class FRWebAuthn {
6587 /**
@@ -94,8 +116,29 @@ abstract class FRWebAuthn {
94116 }
95117 }
96118
119+ /**
120+ * Checks if the browser supports conditional UI (autofill) for WebAuthn.
121+ *
122+ * @return Promise<boolean> indicating if conditional mediation is available
123+ */
124+ public static async isConditionalUISupported ( ) : Promise < boolean > {
125+ if ( ! window . PublicKeyCredential ) {
126+ return false ;
127+ }
128+
129+ // Check if the browser supports conditional mediation
130+ try {
131+ return withTimeout ( PublicKeyCredential . isConditionalMediationAvailable ( ) , TWO_SECOND ) ;
132+ } catch {
133+ FRLogger . warn ( 'Conditional mediation check timed out' ) ;
134+ }
135+
136+ return false ;
137+ }
138+
97139 /**
98140 * Populates the step with the necessary authentication outcome.
141+ * Automatically handles conditional UI if indicated by the server metadata.
99142 *
100143 * @param step The step that contains WebAuthn authentication data
101144 * @return The populated step
@@ -108,19 +151,26 @@ abstract class FRWebAuthn {
108151
109152 try {
110153 let publicKey : PublicKeyCredentialRequestOptions ;
154+ let useConditionalUI = false ;
155+
111156 if ( metadataCallback ) {
112157 const meta = metadataCallback . getOutputValue ( 'data' ) as WebAuthnAuthenticationMetadata ;
158+
159+ // Check if server indicates conditional UI should be used
160+ useConditionalUI = meta . conditional === 'true' ;
113161 publicKey = this . createAuthenticationPublicKey ( meta ) ;
114162
115163 credential = await this . getAuthenticationCredential (
116164 publicKey as PublicKeyCredentialRequestOptions ,
165+ useConditionalUI ,
117166 ) ;
118167 outcome = this . getAuthenticationOutcome ( credential ) ;
119168 } else if ( textOutputCallback ) {
120169 publicKey = parseWebAuthnAuthenticateText ( textOutputCallback . getMessage ( ) ) ;
121170
122171 credential = await this . getAuthenticationCredential (
123172 publicKey as PublicKeyCredentialRequestOptions ,
173+ false , // Script-based callbacks don't support conditional UI
124174 ) ;
125175 outcome = this . getAuthenticationOutcome ( credential ) ;
126176 } else {
@@ -300,18 +350,36 @@ abstract class FRWebAuthn {
300350 * Retrieves the credential from the browser Web Authentication API.
301351 *
302352 * @param options The public key options associated with the request
353+ * @param useConditionalUI Whether to use conditional UI (autofill)
303354 * @return The credential
304355 */
305356 public static async getAuthenticationCredential (
306357 options : PublicKeyCredentialRequestOptions ,
358+ useConditionalUI = false ,
307359 ) : Promise < PublicKeyCredential | null > {
308- // Feature check before we attempt registering a device
360+ // Feature check before we attempt authenticating
309361 if ( ! window . PublicKeyCredential ) {
310362 const e = new Error ( 'PublicKeyCredential not supported by this browser' ) ;
311363 e . name = WebAuthnOutcomeType . NotSupportedError ;
312364 throw e ;
313365 }
314- const credential = await navigator . credentials . get ( { publicKey : options } ) ;
366+ // Build the credential request options
367+ const credentialRequestOptions : CredentialRequestOptions = {
368+ publicKey : options ,
369+ } ;
370+
371+ // Add conditional mediation if requested and supported
372+ if ( useConditionalUI ) {
373+ const isConditionalSupported = await this . isConditionalUISupported ( ) ;
374+ if ( isConditionalSupported ) {
375+ credentialRequestOptions . mediation = 'conditional' as CredentialMediationRequirement ;
376+ } else {
377+ // eslint-disable-next-line no-console
378+ FRLogger . warn ( 'Conditional UI was requested, but is not supported by this browser.' ) ;
379+ }
380+ }
381+
382+ const credential = await navigator . credentials . get ( credentialRequestOptions ) ;
315383 return credential as PublicKeyCredential ;
316384 }
317385
@@ -433,22 +501,51 @@ abstract class FRWebAuthn {
433501 const {
434502 acceptableCredentials,
435503 allowCredentials,
504+ _allowCredentials,
436505 challenge,
437506 relyingPartyId,
507+ _relyingPartyId,
438508 timeout,
439509 userVerification,
510+ extensions,
440511 } = metadata ;
441- const rpId = parseRelyingPartyId ( relyingPartyId ) ;
442- const allowCredentialsValue = parseCredentials ( allowCredentials || acceptableCredentials || '' ) ;
443512
444- return {
513+ // Use the structured _allowCredentials if available, otherwise parse the string format
514+ let allowCredentialsValue : PublicKeyCredentialDescriptor [ ] | undefined ;
515+ if ( _allowCredentials && Array . isArray ( _allowCredentials ) ) {
516+ allowCredentialsValue = _allowCredentials ;
517+ } else {
518+ allowCredentialsValue = parseCredentials ( allowCredentials || acceptableCredentials || '' ) ;
519+ }
520+
521+ // Use _relyingPartyId if available, otherwise parse the old format
522+ const rpId = _relyingPartyId || parseRelyingPartyId ( relyingPartyId ) ;
523+
524+ const options : PublicKeyCredentialRequestOptions = {
445525 challenge : Uint8Array . from ( atob ( challenge ) , ( c ) => c . charCodeAt ( 0 ) ) . buffer ,
446526 timeout,
447- // only add key-value pair if proper value is provided
448- ...( allowCredentialsValue && { allowCredentials : allowCredentialsValue } ) ,
449- ...( userVerification && { userVerification } ) ,
450- ...( rpId && { rpId } ) ,
451527 } ;
528+ // For conditional UI, allowCredentials can be omitted.
529+ // For standard WebAuthn, it may or may not be present.
530+ // Only add the property if the array is not empty.
531+ if ( allowCredentialsValue && allowCredentialsValue . length > 0 ) {
532+ options . allowCredentials = allowCredentialsValue ;
533+ }
534+
535+ // Add optional properties only if they have values
536+ if ( userVerification ) {
537+ options . userVerification = userVerification ;
538+ }
539+
540+ if ( rpId ) {
541+ options . rpId = rpId ;
542+ }
543+
544+ if ( extensions && Object . keys ( extensions ) . length > 0 ) {
545+ options . extensions = extensions ;
546+ }
547+
548+ return options ;
452549 }
453550
454551 /**
0 commit comments