Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,7 @@ export const webAuthnAuthConditionalMetaCallback = {
_allowCredentials: [],
timeout: 60000,
userVerification: 'preferred',
conditionalWebAuthn: true,
mediation: 'conditional',
relyingPartyId: '',
_relyingPartyId: 'example.com',
extensions: {},
Expand Down
17 changes: 5 additions & 12 deletions packages/javascript-sdk/src/fr-webauthn/fr-webauthn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
96 changes: 62 additions & 34 deletions packages/javascript-sdk/src/fr-webauthn/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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,
);
Expand Down Expand Up @@ -349,37 +372,23 @@ 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<PublicKeyCredential | null> {
// Feature check before we attempt authenticating
if (!window.PublicKeyCredential) {
const e = new Error('PublicKeyCredential not supported by this browser');
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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -608,4 +636,4 @@ export type {
WebAuthnCallbacks,
WebAuthnRegistrationMetadata,
};
export { WebAuthnOutcome, WebAuthnStepType };
export { WebAuthnOutcome, WebAuthnOutcomeType, WebAuthnStepType };
2 changes: 1 addition & 1 deletion packages/javascript-sdk/src/fr-webauthn/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ interface WebAuthnAuthenticationMetadata {
_relyingPartyId?: string;
timeout: number;
userVerification: UserVerificationType;
conditional?: string;
mediation?: string;
extensions?: Record<string, unknown>;
_type?: 'WebAuthn';
supportsJsonResponse?: boolean;
Expand Down
3 changes: 2 additions & 1 deletion packages/javascript-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -160,5 +160,6 @@ export {
ValidatedCreatePasswordCallback,
ValidatedCreateUsernameCallback,
WebAuthnOutcome,
WebAuthnOutcomeType,
WebAuthnStepType,
};