diff --git a/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.mock.data.ts b/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.mock.data.ts index 092910b3..1d210c04 100644 --- a/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.mock.data.ts +++ b/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.mock.data.ts @@ -505,7 +505,7 @@ export const webAuthnAuthConditionalMetaCallback = { _allowCredentials: [], timeout: 60000, userVerification: 'preferred', - conditionalWebAuthn: true, + mediation: 'conditional', relyingPartyId: '', _relyingPartyId: 'example.com', extensions: {}, diff --git a/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts b/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts index 9d08af87..2ea8806e 100644 --- a/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts +++ b/packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts @@ -149,7 +149,7 @@ describe('Test FRWebAuthn class with Conditional UI', () => { _allowCredentials: [], timeout: 60000, userVerification: 'preferred', - conditionalWebAuthn: true, + mediation: 'conditional', relyingPartyId: '', _relyingPartyId: 'example.com', extensions: {}, @@ -180,19 +180,10 @@ describe('Test FRWebAuthn class with Conditional UI', () => { vi.spyOn(window.PublicKeyCredential, 'isConditionalMediationAvailable').mockResolvedValue( false, ); - // FIX APPLIED HERE: Added block comment to empty function - const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { - /* empty */ - }); const getSpy = vi.spyOn(navigator.credentials, 'get'); // Attempt to authenticate with conditional UI requested - await FRWebAuthn.getAuthenticationCredential({}, true); - - // Expect a warning to be logged - expect(consoleSpy).toHaveBeenCalledWith( - 'Conditional UI was requested, but is not supported by this browser.', - ); + await FRWebAuthn.getAuthenticationCredential({}); // Expect the call to navigator.credentials.get to NOT have the mediation property expect(getSpy).toHaveBeenCalledWith( @@ -208,7 +199,9 @@ describe('Test FRWebAuthn class with Conditional UI', () => { const getSpy = vi.spyOn(navigator.credentials, 'get'); // Attempt to authenticate with conditional UI requested - await FRWebAuthn.getAuthenticationCredential({}, true); + await FRWebAuthn.getAuthenticationCredential({ + mediation: 'conditional', + }); // Expect the call to navigator.credentials.get to have the mediation property expect(getSpy).toHaveBeenCalledWith( diff --git a/packages/javascript-sdk/src/fr-webauthn/index.ts b/packages/javascript-sdk/src/fr-webauthn/index.ts index 293366f8..140e4048 100644 --- a/packages/javascript-sdk/src/fr-webauthn/index.ts +++ b/packages/javascript-sdk/src/fr-webauthn/index.ts @@ -48,6 +48,12 @@ type WebAuthnMetadata = WebAuthnAuthenticationMetadata | WebAuthnRegistrationMet type WebAuthnTextOutput = WebAuthnTextOutputRegistration; const TWO_SECOND = 2000; +declare global { + interface Window { + PingWebAuthnAbortController: AbortController; + } +} + /** * Utility for integrating a web browser's WebAuthn API. * @@ -151,27 +157,38 @@ abstract class FRWebAuthn { try { let publicKey: PublicKeyCredentialRequestOptions; - let useConditionalUI = false; if (metadataCallback) { const meta = metadataCallback.getOutputValue('data') as WebAuthnAuthenticationMetadata; + const mediation = meta.mediation as CredentialMediationRequirement; + + if (mediation === 'conditional') { + const isConditionalSupported = await this.isConditionalUISupported(); + if (!isConditionalSupported) { + const e = new Error( + 'Conditional UI was requested, but is not supported by this browser.', + ); + e.name = WebAuthnOutcomeType.NotSupportedError; + throw e; + } + } - // Check if server indicates conditional UI should be used - useConditionalUI = meta.conditional === 'true'; publicKey = this.createAuthenticationPublicKey(meta); - credential = await this.getAuthenticationCredential( - publicKey as PublicKeyCredentialRequestOptions, - useConditionalUI, - ); + credential = await this.getAuthenticationCredential({ publicKey, mediation }); outcome = this.getAuthenticationOutcome(credential); } else if (textOutputCallback) { - publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage()); + const metadata = this.extractMetadata(textOutputCallback.getMessage()); - credential = await this.getAuthenticationCredential( - publicKey as PublicKeyCredentialRequestOptions, - false, // Script-based callbacks don't support conditional UI - ); + if (metadata) { + publicKey = this.createAuthenticationPublicKey( + metadata as WebAuthnAuthenticationMetadata, + ); + } else { + publicKey = parseWebAuthnAuthenticateText(textOutputCallback.getMessage()); + } + + credential = await this.getAuthenticationCredential({ publicKey }); outcome = this.getAuthenticationOutcome(credential); } else { throw new Error('No Credential found from Public Key'); @@ -236,7 +253,13 @@ abstract class FRWebAuthn { ); outcome = this.getRegistrationOutcome(credential); } else if (textOutputCallback) { - publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage()); + const metadata = this.extractMetadata(textOutputCallback.getMessage()); + + if (metadata) { + publicKey = this.createRegistrationPublicKey(metadata as WebAuthnRegistrationMetadata); + } else { + publicKey = parseWebAuthnRegisterText(textOutputCallback.getMessage()); + } credential = await this.getRegistrationCredential( publicKey as PublicKeyCredentialCreationOptions, ); @@ -349,13 +372,11 @@ abstract class FRWebAuthn { /** * Retrieves the credential from the browser Web Authentication API. * - * @param options The public key options associated with the request - * @param useConditionalUI Whether to use conditional UI (autofill) + * @param options The options associated with the request * @return The credential */ public static async getAuthenticationCredential( - options: PublicKeyCredentialRequestOptions, - useConditionalUI = false, + options: CredentialRequestOptions, ): Promise { // Feature check before we attempt authenticating if (!window.PublicKeyCredential) { @@ -363,23 +384,11 @@ abstract class FRWebAuthn { e.name = WebAuthnOutcomeType.NotSupportedError; throw e; } - // Build the credential request options - const credentialRequestOptions: CredentialRequestOptions = { - publicKey: options, - }; - - // Add conditional mediation if requested and supported - if (useConditionalUI) { - const isConditionalSupported = await this.isConditionalUISupported(); - if (isConditionalSupported) { - credentialRequestOptions.mediation = 'conditional' as CredentialMediationRequirement; - } else { - // eslint-disable-next-line no-console - FRLogger.warn('Conditional UI was requested, but is not supported by this browser.'); - } - } - const credential = await navigator.credentials.get(credentialRequestOptions); + const credential = await navigator.credentials.get({ + ...options, + signal: this.createAbortController().signal, + }); return credential as PublicKeyCredential; } @@ -599,6 +608,25 @@ abstract class FRWebAuthn { }, }; } + + private static createAbortController() { + window.PingWebAuthnAbortController?.abort(); + + const abortController = new AbortController(); + window.PingWebAuthnAbortController = abortController; + return abortController; + } + + private static extractMetadata(message: string): object | null { + const contextMatch = message.match(/^var scriptContext = (.*)$/); + const jsonString = contextMatch?.[1]; + + if (jsonString) { + return JSON.parse(jsonString); + } + + return null; + } } export default FRWebAuthn; @@ -608,4 +636,4 @@ export type { WebAuthnCallbacks, WebAuthnRegistrationMetadata, }; -export { WebAuthnOutcome, WebAuthnStepType }; +export { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType }; diff --git a/packages/javascript-sdk/src/fr-webauthn/interfaces.ts b/packages/javascript-sdk/src/fr-webauthn/interfaces.ts index 9990868a..87da0a9f 100644 --- a/packages/javascript-sdk/src/fr-webauthn/interfaces.ts +++ b/packages/javascript-sdk/src/fr-webauthn/interfaces.ts @@ -86,7 +86,7 @@ interface WebAuthnAuthenticationMetadata { _relyingPartyId?: string; timeout: number; userVerification: UserVerificationType; - conditional?: string; + mediation?: string; extensions?: Record; _type?: 'WebAuthn'; supportsJsonResponse?: boolean; diff --git a/packages/javascript-sdk/src/index.ts b/packages/javascript-sdk/src/index.ts index 4added1e..0b7df6fc 100644 --- a/packages/javascript-sdk/src/index.ts +++ b/packages/javascript-sdk/src/index.ts @@ -61,7 +61,7 @@ import type { WebAuthnCallbacks, WebAuthnRegistrationMetadata, } from './fr-webauthn'; -import FRWebAuthn, { WebAuthnOutcome, WebAuthnStepType } from './fr-webauthn'; +import FRWebAuthn, { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType } from './fr-webauthn'; import HttpClient from './http-client'; import type { GetAuthorizationUrlOptions, @@ -160,5 +160,6 @@ export { ValidatedCreatePasswordCallback, ValidatedCreateUsernameCallback, WebAuthnOutcome, + WebAuthnOutcomeType, WebAuthnStepType, };