@@ -48,6 +48,12 @@ type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMet
4848type WebAuthnTextOutput = WebAuthnTextOutputRegistration ;
4949const TWO_SECOND = 2000 ;
5050
51+ declare global {
52+ interface Window {
53+ PingWebAuthnAbortController : AbortController ;
54+ }
55+ }
56+
5157/**
5258 * Utility for integrating a web browser's WebAuthn API.
5359 *
@@ -65,19 +71,19 @@ const TWO_SECOND = 2000;
6571 * }
6672 * ```
6773 *
68- * Conditional UI (Autofill) Support:
74+ * Conditional mediation (Autofill) Support:
6975 *
7076 * ```js
71- * // Check if browser supports conditional UI
77+ * // Check if browser supports conditional mediation
7278 * const supportsConditionalUI = await FRWebAuthn.isConditionalUISupported();
7379 *
7480 * if (supportsConditionalUI) {
75- * // The authenticate() method automatically handles conditional UI
81+ * // The authenticate() method automatically handles conditional mediation
7682 * // when the server indicates support via conditionalWebAuthn: true
7783 * // in the metadata. No additional code changes needed.
7884 * await FRWebAuthn.authenticate(step);
7985 *
80- * // For conditional UI to work in the browser, add autocomplete="webauthn"
86+ * // For conditional mediation to work in the browser, add autocomplete="webauthn"
8187 * // to your username input field:
8288 * // <input type="text" name="username" autocomplete="webauthn" />
8389 * }
@@ -117,12 +123,21 @@ abstract class FRWebAuthn {
117123 }
118124
119125 /**
120- * 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.
121136 *
122137 * @return Promise<boolean> indicating if conditional mediation is available
123138 */
124- public static async isConditionalUISupported ( ) : Promise < boolean > {
125- if ( ! window . PublicKeyCredential ) {
139+ public static async isConditionalMediationSupported ( ) : Promise < boolean > {
140+ if ( ! this . isWebAuthnSupported ( ) ) {
126141 return false ;
127142 }
128143
@@ -138,41 +153,49 @@ abstract class FRWebAuthn {
138153
139154 /**
140155 * Populates the step with the necessary authentication outcome.
141- * Automatically handles conditional UI if indicated by the server metadata.
156+ * Automatically handles conditional mediation if indicated by the server metadata.
142157 *
143158 * @param step The step that contains WebAuthn authentication data
159+ * @param optionsTransformer Augments the derived options with custom behaviour
144160 * @return The populated step
145161 */
146- 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 > {
147168 const { hiddenCallback, metadataCallback, textOutputCallback } = this . getCallbacks ( step ) ;
148169 if ( hiddenCallback && ( metadataCallback || textOutputCallback ) ) {
149- let outcome : ReturnType < typeof this . getAuthenticationOutcome > ;
150- let credential : PublicKeyCredential | null = null ;
170+ const options : CredentialRequestOptions = { } ;
151171
152172 try {
153- let publicKey : PublicKeyCredentialRequestOptions ;
154- let useConditionalUI = false ;
155-
156173 if ( metadataCallback ) {
157174 const meta = metadataCallback . getOutputValue ( 'data' ) as WebAuthnAuthenticationMetadata ;
158-
159- // Check if server indicates conditional UI should be used
160- useConditionalUI = meta . conditional === 'true' ;
161- publicKey = this . createAuthenticationPublicKey ( meta ) ;
162-
163- credential = await this . getAuthenticationCredential (
164- publicKey as PublicKeyCredentialRequestOptions ,
165- useConditionalUI ,
166- ) ;
167- outcome = this . getAuthenticationOutcome ( credential ) ;
175+ const mediation = meta . mediation as CredentialMediationRequirement ;
176+
177+ if ( mediation === 'conditional' ) {
178+ const isConditionalMediationSupported = await this . isConditionalMediationSupported ( ) ;
179+ if ( ! isConditionalMediationSupported ) {
180+ const e = new Error (
181+ 'Conditional mediation was requested, but is not supported by this browser.' ,
182+ ) ;
183+ e . name = WebAuthnOutcomeType . NotSupportedError ;
184+ throw e ;
185+ }
186+ }
187+
188+ options . publicKey = this . createAuthenticationPublicKey ( meta ) ;
189+ options . mediation = mediation ;
168190 } else if ( textOutputCallback ) {
169- publicKey = parseWebAuthnAuthenticateText ( textOutputCallback . getMessage ( ) ) ;
170-
171- credential = await this . getAuthenticationCredential (
172- publicKey as PublicKeyCredentialRequestOptions ,
173- false , // Script-based callbacks don't support conditional UI
174- ) ;
175- outcome = this . getAuthenticationOutcome ( credential ) ;
191+ const metadata = this . extractMetadata ( textOutputCallback . getMessage ( ) ) ;
192+ if ( metadata ) {
193+ options . publicKey = this . createAuthenticationPublicKey (
194+ metadata as WebAuthnAuthenticationMetadata ,
195+ ) ;
196+ } else {
197+ options . publicKey = parseWebAuthnAuthenticateText ( textOutputCallback . getMessage ( ) ) ;
198+ }
176199 } else {
177200 throw new Error ( 'No Credential found from Public Key' ) ;
178201 }
@@ -187,6 +210,12 @@ abstract class FRWebAuthn {
187210 throw error ;
188211 }
189212
213+ const credential : PublicKeyCredential | null = await this . getAuthenticationCredential (
214+ optionsTransformer ( options ) ,
215+ ) ;
216+ const outcome : ReturnType < typeof this . getAuthenticationOutcome > =
217+ this . getAuthenticationOutcome ( credential ) ;
218+
190219 if ( metadataCallback ) {
191220 const meta = metadataCallback . getOutputValue ( 'data' ) as WebAuthnAuthenticationMetadata ;
192221 if ( meta ?. supportsJsonResponse && credential && 'authenticatorAttachment' in credential ) {
@@ -236,7 +265,13 @@ abstract class FRWebAuthn {
236265 ) ;
237266 outcome = this . getRegistrationOutcome ( credential ) ;
238267 } else if ( textOutputCallback ) {
239- publicKey = parseWebAuthnRegisterText ( textOutputCallback . getMessage ( ) ) ;
268+ const metadata = this . extractMetadata ( textOutputCallback . getMessage ( ) ) ;
269+
270+ if ( metadata ) {
271+ publicKey = this . createRegistrationPublicKey ( metadata as WebAuthnRegistrationMetadata ) ;
272+ } else {
273+ publicKey = parseWebAuthnRegisterText ( textOutputCallback . getMessage ( ) ) ;
274+ }
240275 credential = await this . getRegistrationCredential (
241276 publicKey as PublicKeyCredentialCreationOptions ,
242277 ) ;
@@ -349,37 +384,23 @@ abstract class FRWebAuthn {
349384 /**
350385 * Retrieves the credential from the browser Web Authentication API.
351386 *
352- * @param options The public key options associated with the request
353- * @param useConditionalUI Whether to use conditional UI (autofill)
387+ * @param options The options associated with the request
354388 * @return The credential
355389 */
356390 public static async getAuthenticationCredential (
357- options : PublicKeyCredentialRequestOptions ,
358- useConditionalUI = false ,
391+ options : CredentialRequestOptions ,
359392 ) : Promise < PublicKeyCredential | null > {
360393 // Feature check before we attempt authenticating
361- if ( ! window . PublicKeyCredential ) {
394+ if ( ! this . isWebAuthnSupported ( ) ) {
362395 const e = new Error ( 'PublicKeyCredential not supported by this browser' ) ;
363396 e . name = WebAuthnOutcomeType . NotSupportedError ;
364397 throw e ;
365398 }
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- }
381399
382- const credential = await navigator . credentials . get ( credentialRequestOptions ) ;
400+ const credential = await navigator . credentials . get ( {
401+ ...options ,
402+ signal : this . createAbortController ( ) . signal ,
403+ } ) ;
383404 return credential as PublicKeyCredential ;
384405 }
385406
@@ -448,7 +469,7 @@ abstract class FRWebAuthn {
448469 options : PublicKeyCredentialCreationOptions ,
449470 ) : Promise < PublicKeyCredential | null > {
450471 // Feature check before we attempt registering a device
451- if ( ! window . PublicKeyCredential ) {
472+ if ( this . isWebAuthnSupported ( ) ) {
452473 const e = new Error ( 'PublicKeyCredential not supported by this browser' ) ;
453474 e . name = WebAuthnOutcomeType . NotSupportedError ;
454475 throw e ;
@@ -525,7 +546,7 @@ abstract class FRWebAuthn {
525546 challenge : Uint8Array . from ( atob ( challenge ) , ( c ) => c . charCodeAt ( 0 ) ) . buffer ,
526547 timeout,
527548 } ;
528- // For conditional UI , allowCredentials can be omitted.
549+ // For conditional mediation , allowCredentials can be omitted.
529550 // For standard WebAuthn, it may or may not be present.
530551 // Only add the property if the array is not empty.
531552 if ( allowCredentialsValue && allowCredentialsValue . length > 0 ) {
@@ -599,6 +620,25 @@ abstract class FRWebAuthn {
599620 } ,
600621 } ;
601622 }
623+
624+ private static createAbortController ( ) {
625+ window . PingWebAuthnAbortController ?. abort ( ) ;
626+
627+ const abortController = new AbortController ( ) ;
628+ window . PingWebAuthnAbortController = abortController ;
629+ return abortController ;
630+ }
631+
632+ private static extractMetadata ( message : string ) : object | null {
633+ const contextMatch = message . match ( / ^ v a r s c r i p t C o n t e x t = ( .* ) ; * $ / m) ;
634+ const jsonString = contextMatch ?. [ 1 ] ;
635+
636+ if ( jsonString ) {
637+ return JSON . parse ( jsonString ) ;
638+ }
639+
640+ return null ;
641+ }
602642}
603643
604644export default FRWebAuthn ;
@@ -608,4 +648,4 @@ export type {
608648 WebAuthnCallbacks ,
609649 WebAuthnRegistrationMetadata ,
610650} ;
611- export { WebAuthnOutcome , WebAuthnStepType } ;
651+ export { WebAuthnOutcome , WebAuthnOutcomeType , WebAuthnStepType } ;
0 commit comments