Download node_modules/@supabase/auth-js/src/lib/webauthn.ts from metarm/QMetaRam: direct link, hf CLI and curl.
- Browser
- Download file 36.2 kB
-
https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40supabase/auth-js/src/lib/webauthn.ts
- Command line
-
hf download hf://metarm/QMetaRam/node_modules/@supabase/auth-js/src/lib/webauthn.ts
-
curl -L -o webauthn.ts https://huggingface.co/metarm/QMetaRam/resolve/main/node_modules/%40supabase/auth-js/src/lib/webauthn.ts
36.2 kB
| import GoTrueClient from '../GoTrueClient' | |
| import { base64UrlToUint8Array, bytesToBase64URL } from './base64url' | |
| import { AuthError, AuthUnknownError, isAuthError } from './errors' | |
| import { | |
| AuthMFAEnrollWebauthnResponse, | |
| AuthMFAVerifyResponse, | |
| AuthMFAVerifyResponseData, | |
| MFAChallengeWebauthnParams, | |
| MFAEnrollWebauthnParams, | |
| MFAVerifyWebauthnParamFields, | |
| MFAVerifyWebauthnParams, | |
| RequestResult, | |
| StrictOmit, | |
| } from './types' | |
| import { isBrowser } from './helpers' | |
| import type { | |
| AuthenticationCredential, | |
| AuthenticationResponseJSON, | |
| AuthenticatorAttachment, | |
| PublicKeyCredentialCreationOptionsFuture, | |
| PublicKeyCredentialCreationOptionsJSON, | |
| PublicKeyCredentialFuture, | |
| PublicKeyCredentialRequestOptionsFuture, | |
| PublicKeyCredentialRequestOptionsJSON, | |
| RegistrationCredential, | |
| RegistrationResponseJSON, | |
| } from './webauthn.dom' | |
| import { | |
| identifyAuthenticationError, | |
| identifyRegistrationError, | |
| isWebAuthnError, | |
| WebAuthnError, | |
| WebAuthnUnknownError, | |
| } from './webauthn.errors' | |
| export { WebAuthnError, isWebAuthnError, identifyRegistrationError, identifyAuthenticationError } | |
| // Re-export the JSON types for use in other files | |
| export type { RegistrationResponseJSON, AuthenticationResponseJSON } | |
| /** | |
| * WebAuthn abort service to manage ceremony cancellation. | |
| * Ensures only one WebAuthn ceremony is active at a time to prevent "operation already in progress" errors. | |
| * | |
| * @experimental This class is experimental and may change in future releases | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-automation-webdriver-capability W3C WebAuthn Spec - Aborting Ceremonies} | |
| */ | |
| export class WebAuthnAbortService { | |
| private controller: AbortController | undefined | |
| /** | |
| * Create an abort signal for a new WebAuthn operation. | |
| * Automatically cancels any existing operation. | |
| * | |
| * @returns {AbortSignal} Signal to pass to navigator.credentials.create() or .get() | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal MDN - AbortSignal} | |
| */ | |
| createNewAbortSignal(): AbortSignal { | |
| // Abort any existing calls to navigator.credentials.create() or navigator.credentials.get() | |
| if (this.controller) { | |
| const abortError = new Error('Cancelling existing WebAuthn API call for new one') | |
| abortError.name = 'AbortError' | |
| this.controller.abort(abortError) | |
| } | |
| const newController = new AbortController() | |
| this.controller = newController | |
| return newController.signal | |
| } | |
| /** | |
| * Manually cancel the current WebAuthn operation. | |
| * Useful for cleaning up when user cancels or navigates away. | |
| * | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort MDN - AbortController.abort} | |
| */ | |
| cancelCeremony(): void { | |
| if (this.controller) { | |
| const abortError = new Error('Manually cancelling existing WebAuthn API call') | |
| abortError.name = 'AbortError' | |
| this.controller.abort(abortError) | |
| this.controller = undefined | |
| } | |
| } | |
| } | |
| /** | |
| * Singleton instance to ensure only one WebAuthn ceremony is active at a time. | |
| * This prevents "operation already in progress" errors when retrying WebAuthn operations. | |
| * | |
| * @experimental This instance is experimental and may change in future releases | |
| */ | |
| export const webAuthnAbortService = new WebAuthnAbortService() | |
| /** | |
| * Server response format for WebAuthn credential creation options. | |
| * Uses W3C standard JSON format with base64url-encoded binary fields. | |
| */ | |
| export type ServerCredentialCreationOptions = PublicKeyCredentialCreationOptionsJSON | |
| /** | |
| * Server response format for WebAuthn credential request options. | |
| * Uses W3C standard JSON format with base64url-encoded binary fields. | |
| */ | |
| export type ServerCredentialRequestOptions = PublicKeyCredentialRequestOptionsJSON | |
| /** | |
| * Convert base64url encoded strings in WebAuthn credential creation options to ArrayBuffers | |
| * as required by the WebAuthn browser API. | |
| * Supports both native WebAuthn Level 3 parseCreationOptionsFromJSON and manual fallback. | |
| * | |
| * @param {ServerCredentialCreationOptions} options - JSON options from server with base64url encoded fields | |
| * @returns {PublicKeyCredentialCreationOptionsFuture} Options ready for navigator.credentials.create() | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-parseCreationOptionsFromJSON W3C WebAuthn Spec - parseCreationOptionsFromJSON} | |
| */ | |
| export function deserializeCredentialCreationOptions( | |
| options: ServerCredentialCreationOptions | |
| ): PublicKeyCredentialCreationOptionsFuture { | |
| if (!options) { | |
| throw new Error('Credential creation options are required') | |
| } | |
| // Check if the native parseCreationOptionsFromJSON method is available | |
| if ( | |
| typeof PublicKeyCredential !== 'undefined' && | |
| 'parseCreationOptionsFromJSON' in PublicKeyCredential && | |
| typeof (PublicKeyCredential as unknown as PublicKeyCredentialFuture) | |
| .parseCreationOptionsFromJSON === 'function' | |
| ) { | |
| // Use the native WebAuthn Level 3 method | |
| return ( | |
| PublicKeyCredential as unknown as PublicKeyCredentialFuture | |
| ).parseCreationOptionsFromJSON( | |
| /** we assert the options here as typescript still doesn't know about future webauthn types */ | |
| options as any | |
| ) as PublicKeyCredentialCreationOptionsFuture | |
| } | |
| // Fallback to manual parsing for browsers that don't support the native method | |
| // Destructure to separate fields that need transformation | |
| const { challenge: challengeStr, user: userOpts, excludeCredentials, ...restOptions } = options | |
| // Convert challenge from base64url to ArrayBuffer | |
| const challenge = base64UrlToUint8Array(challengeStr).buffer as ArrayBuffer | |
| // Convert user.id from base64url to ArrayBuffer | |
| const user: PublicKeyCredentialUserEntity = { | |
| ...userOpts, | |
| id: base64UrlToUint8Array(userOpts.id).buffer as ArrayBuffer, | |
| } | |
| // Build the result object | |
| const result: PublicKeyCredentialCreationOptionsFuture = { | |
| ...restOptions, | |
| challenge, | |
| user, | |
| } | |
| // Only add excludeCredentials if it exists | |
| if (excludeCredentials && excludeCredentials.length > 0) { | |
| result.excludeCredentials = new Array(excludeCredentials.length) | |
| for (let i = 0; i < excludeCredentials.length; i++) { | |
| const cred = excludeCredentials[i] | |
| result.excludeCredentials[i] = { | |
| ...cred, | |
| id: base64UrlToUint8Array(cred.id).buffer, | |
| type: cred.type || 'public-key', | |
| // Cast transports to handle future transport types like "cable" | |
| transports: cred.transports, | |
| } | |
| } | |
| } | |
| return result | |
| } | |
| /** | |
| * Convert base64url encoded strings in WebAuthn credential request options to ArrayBuffers | |
| * as required by the WebAuthn browser API. | |
| * Supports both native WebAuthn Level 3 parseRequestOptionsFromJSON and manual fallback. | |
| * | |
| * @param {ServerCredentialRequestOptions} options - JSON options from server with base64url encoded fields | |
| * @returns {PublicKeyCredentialRequestOptionsFuture} Options ready for navigator.credentials.get() | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON W3C WebAuthn Spec - parseRequestOptionsFromJSON} | |
| */ | |
| export function deserializeCredentialRequestOptions( | |
| options: ServerCredentialRequestOptions | |
| ): PublicKeyCredentialRequestOptionsFuture { | |
| if (!options) { | |
| throw new Error('Credential request options are required') | |
| } | |
| // Check if the native parseRequestOptionsFromJSON method is available | |
| if ( | |
| typeof PublicKeyCredential !== 'undefined' && | |
| 'parseRequestOptionsFromJSON' in PublicKeyCredential && | |
| typeof (PublicKeyCredential as unknown as PublicKeyCredentialFuture) | |
| .parseRequestOptionsFromJSON === 'function' | |
| ) { | |
| // Use the native WebAuthn Level 3 method | |
| return ( | |
| PublicKeyCredential as unknown as PublicKeyCredentialFuture | |
| ).parseRequestOptionsFromJSON(options) as PublicKeyCredentialRequestOptionsFuture | |
| } | |
| // Fallback to manual parsing for browsers that don't support the native method | |
| // Destructure to separate fields that need transformation | |
| const { challenge: challengeStr, allowCredentials, ...restOptions } = options | |
| // Convert challenge from base64url to ArrayBuffer | |
| const challenge = base64UrlToUint8Array(challengeStr).buffer as ArrayBuffer | |
| // Build the result object | |
| const result: PublicKeyCredentialRequestOptionsFuture = { | |
| ...restOptions, | |
| challenge, | |
| } | |
| // Only add allowCredentials if it exists | |
| if (allowCredentials && allowCredentials.length > 0) { | |
| result.allowCredentials = new Array(allowCredentials.length) | |
| for (let i = 0; i < allowCredentials.length; i++) { | |
| const cred = allowCredentials[i] | |
| result.allowCredentials[i] = { | |
| ...cred, | |
| id: base64UrlToUint8Array(cred.id).buffer, | |
| type: cred.type || 'public-key', | |
| // Cast transports to handle future transport types like "cable" | |
| transports: cred.transports, | |
| } | |
| } | |
| } | |
| return result | |
| } | |
| /** | |
| * Server format for credential response with base64url-encoded binary fields | |
| * Can be either a registration or authentication response | |
| */ | |
| export type ServerCredentialResponse = RegistrationResponseJSON | AuthenticationResponseJSON | |
| /** | |
| * Convert a registration/enrollment credential response to server format. | |
| * Serializes binary fields to base64url for JSON transmission. | |
| * Supports both native WebAuthn Level 3 toJSON and manual fallback. | |
| * | |
| * @param {RegistrationCredential} credential - Credential from navigator.credentials.create() | |
| * @returns {RegistrationResponseJSON} JSON-serializable credential for server | |
| * @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-tojson W3C WebAuthn Spec - toJSON} | |
| */ | |
| export function serializeCredentialCreationResponse( | |
| credential: RegistrationCredential | |
| ): RegistrationResponseJSON { | |
| // Check if the credential instance has the toJSON method | |
| if ('toJSON' in credential && typeof credential.toJSON === 'function') { | |
| // Use the native WebAuthn Level 3 method | |
| return (credential as RegistrationCredential).toJSON() | |
| } | |
| const credentialWithAttachment = credential as PublicKeyCredential & { | |
| response: AuthenticatorAttestationResponse | |
| authenticatorAttachment?: string | null | |
| } | |
| return { | |
| id: credential.id, | |
| rawId: credential.id, | |
| response: { | |
| attestationObject: bytesToBase64URL(new Uint8Array(credential.response.attestationObject)), | |
| clientDataJSON: bytesToBase64URL(new Uint8Array(credential.response.clientDataJSON)), | |
| }, | |
| type: 'public-key', | |
| clientExtensionResults: credential.getClientExtensionResults(), | |
| // Convert null to undefined and cast to AuthenticatorAttachment type | |
| authenticatorAttachment: (credentialWithAttachment.authenticatorAttachment ?? undefined) as | |
| | AuthenticatorAttachment | |
| | undefined, | |
| } | |
| } | |
| /** | |
| * Convert an authentication/verification credential response to server format. | |
| * Serializes binary fields to base64url for JSON transmission. | |
| * Supports both native WebAuthn Level 3 toJSON and manual fallback. | |
| * | |
| * @param {AuthenticationCredential} credential - Credential from navigator.credentials.get() | |
| * @returns {AuthenticationResponseJSON} JSON-serializable credential for server | |
| * @see {@link https://w3c.github.io/webauthn/#dom-publickeycredential-tojson W3C WebAuthn Spec - toJSON} | |
| */ | |
| export function serializeCredentialRequestResponse( | |
| credential: AuthenticationCredential | |
| ): AuthenticationResponseJSON { | |
| // Check if the credential instance has the toJSON method | |
| if ('toJSON' in credential && typeof credential.toJSON === 'function') { | |
| // Use the native WebAuthn Level 3 method | |
| return (credential as AuthenticationCredential).toJSON() | |
| } | |
| // Fallback to manual conversion for browsers that don't support toJSON | |
| // Access authenticatorAttachment via type assertion to handle TypeScript version differences | |
| // @simplewebauthn/types includes this property but base TypeScript 4.7.4 doesn't | |
| const credentialWithAttachment = credential as PublicKeyCredential & { | |
| response: AuthenticatorAssertionResponse | |
| authenticatorAttachment?: string | null | |
| } | |
| const clientExtensionResults = credential.getClientExtensionResults() | |
| const assertionResponse = credential.response | |
| return { | |
| id: credential.id, | |
| rawId: credential.id, // W3C spec expects rawId to match id for JSON format | |
| response: { | |
| authenticatorData: bytesToBase64URL(new Uint8Array(assertionResponse.authenticatorData)), | |
| clientDataJSON: bytesToBase64URL(new Uint8Array(assertionResponse.clientDataJSON)), | |
| signature: bytesToBase64URL(new Uint8Array(assertionResponse.signature)), | |
| userHandle: assertionResponse.userHandle | |
| ? bytesToBase64URL(new Uint8Array(assertionResponse.userHandle)) | |
| : undefined, | |
| }, | |
| type: 'public-key', | |
| clientExtensionResults, | |
| // Convert null to undefined and cast to AuthenticatorAttachment type | |
| authenticatorAttachment: (credentialWithAttachment.authenticatorAttachment ?? undefined) as | |
| | AuthenticatorAttachment | |
| | undefined, | |
| } | |
| } | |
| /** | |
| * A simple test to determine if a hostname is a properly-formatted domain name. | |
| * Considers localhost valid for development environments. | |
| * | |
| * A "valid domain" is defined here: https://url.spec.whatwg.org/#valid-domain | |
| * | |
| * Regex sourced from here: | |
| * https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s15.html | |
| * | |
| * @param {string} hostname - The hostname to validate | |
| * @returns {boolean} True if valid domain or localhost | |
| * @see {@link https://url.spec.whatwg.org/#valid-domain WHATWG URL Spec - Valid Domain} | |
| */ | |
| export function isValidDomain(hostname: string): boolean { | |
| return ( | |
| // Consider localhost valid as well since it's okay wrt Secure Contexts | |
| hostname === 'localhost' || /^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/i.test(hostname) | |
| ) | |
| } | |
| /** | |
| * Determine if the browser is capable of WebAuthn. | |
| * Checks for necessary Web APIs: PublicKeyCredential and Credential Management. | |
| * | |
| * @returns {boolean} True if browser supports WebAuthn | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential#browser_compatibility MDN - PublicKeyCredential Browser Compatibility} | |
| */ | |
| export function browserSupportsWebAuthn(): boolean { | |
| return !!( | |
| isBrowser() && | |
| 'PublicKeyCredential' in window && | |
| window.PublicKeyCredential && | |
| 'credentials' in navigator && | |
| typeof navigator?.credentials?.create === 'function' && | |
| typeof navigator?.credentials?.get === 'function' | |
| ) | |
| } | |
| /** | |
| * Create a WebAuthn credential using the browser's credentials API. | |
| * Wraps navigator.credentials.create() with error handling. | |
| * | |
| * @param {CredentialCreationOptions} options - Options including publicKey parameters | |
| * @returns {Promise<RequestResult<RegistrationCredential, WebAuthnError>>} Created credential or error | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-createCredential W3C WebAuthn Spec - Create Credential} | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/create MDN - credentials.create} | |
| */ | |
| export async function createCredential( | |
| options: StrictOmit<CredentialCreationOptions, 'publicKey'> & { | |
| publicKey: PublicKeyCredentialCreationOptionsFuture | |
| } | |
| ): Promise<RequestResult<RegistrationCredential, WebAuthnError>> { | |
| try { | |
| const response = await navigator.credentials.create( | |
| /** we assert the type here until typescript types are updated */ | |
| options as Parameters<typeof navigator.credentials.create>[0] | |
| ) | |
| if (!response) { | |
| return { | |
| data: null, | |
| error: new WebAuthnUnknownError('Empty credential response', response), | |
| } | |
| } | |
| if (!(response instanceof PublicKeyCredential)) { | |
| return { | |
| data: null, | |
| error: new WebAuthnUnknownError('Browser returned unexpected credential type', response), | |
| } | |
| } | |
| return { data: response as RegistrationCredential, error: null } | |
| } catch (err) { | |
| return { | |
| data: null, | |
| error: identifyRegistrationError({ | |
| error: err as Error, | |
| options, | |
| }), | |
| } | |
| } | |
| } | |
| /** | |
| * Get a WebAuthn credential using the browser's credentials API. | |
| * Wraps navigator.credentials.get() with error handling. | |
| * | |
| * @param {CredentialRequestOptions} options - Options including publicKey parameters | |
| * @returns {Promise<RequestResult<AuthenticationCredential, WebAuthnError>>} Retrieved credential or error | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-getAssertion W3C WebAuthn Spec - Get Assertion} | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get MDN - credentials.get} | |
| */ | |
| export async function getCredential( | |
| options: StrictOmit<CredentialRequestOptions, 'publicKey'> & { | |
| publicKey: PublicKeyCredentialRequestOptionsFuture | |
| } | |
| ): Promise<RequestResult<AuthenticationCredential, WebAuthnError>> { | |
| try { | |
| const response = await navigator.credentials.get( | |
| /** we assert the type here until typescript types are updated */ | |
| options as Parameters<typeof navigator.credentials.get>[0] | |
| ) | |
| if (!response) { | |
| return { | |
| data: null, | |
| error: new WebAuthnUnknownError('Empty credential response', response), | |
| } | |
| } | |
| if (!(response instanceof PublicKeyCredential)) { | |
| return { | |
| data: null, | |
| error: new WebAuthnUnknownError('Browser returned unexpected credential type', response), | |
| } | |
| } | |
| return { data: response as AuthenticationCredential, error: null } | |
| } catch (err) { | |
| return { | |
| data: null, | |
| error: identifyAuthenticationError({ | |
| error: err as Error, | |
| options, | |
| }), | |
| } | |
| } | |
| } | |
| export const DEFAULT_CREATION_OPTIONS: Partial<PublicKeyCredentialCreationOptionsFuture> = { | |
| hints: ['security-key'], | |
| authenticatorSelection: { | |
| authenticatorAttachment: 'cross-platform', | |
| requireResidentKey: false, | |
| /** set to preferred because older yubikeys don't have PIN/Biometric */ | |
| userVerification: 'preferred', | |
| residentKey: 'discouraged', | |
| }, | |
| attestation: 'direct', | |
| } | |
| export const DEFAULT_REQUEST_OPTIONS: Partial<PublicKeyCredentialRequestOptionsFuture> = { | |
| /** set to preferred because older yubikeys don't have PIN/Biometric */ | |
| userVerification: 'preferred', | |
| hints: ['security-key'], | |
| attestation: 'direct', | |
| } | |
| function deepMerge<T>(...sources: Partial<T>[]): T { | |
| const isObject = (val: unknown): val is Record<string, unknown> => | |
| val !== null && typeof val === 'object' && !Array.isArray(val) | |
| const isArrayBufferLike = (val: unknown): val is ArrayBuffer | ArrayBufferView => | |
| val instanceof ArrayBuffer || ArrayBuffer.isView(val) | |
| const result: Partial<T> = {} | |
| for (const source of sources) { | |
| if (!source) continue | |
| for (const key in source) { | |
| const value = source[key] | |
| if (value === undefined) continue | |
| if (Array.isArray(value)) { | |
| // preserve array reference, including unions like AuthenticatorTransport[] | |
| result[key] = value as T[typeof key] | |
| } else if (isArrayBufferLike(value)) { | |
| result[key] = value as T[typeof key] | |
| } else if (isObject(value)) { | |
| const existing = result[key] | |
| if (isObject(existing)) { | |
| result[key] = deepMerge(existing, value) as unknown as T[typeof key] | |
| } else { | |
| result[key] = deepMerge(value) as unknown as T[typeof key] | |
| } | |
| } else { | |
| result[key] = value as T[typeof key] | |
| } | |
| } | |
| } | |
| return result as T | |
| } | |
| /** | |
| * Merges WebAuthn credential creation options with overrides. | |
| * Sets sensible defaults for authenticator selection and extensions. | |
| * | |
| * @param {PublicKeyCredentialCreationOptionsFuture} baseOptions - The base options from the server | |
| * @param {PublicKeyCredentialCreationOptionsFuture} overrides - Optional overrides to apply | |
| * @param {string} friendlyName - Optional friendly name for the credential | |
| * @returns {PublicKeyCredentialCreationOptionsFuture} Merged credential creation options | |
| * @see {@link https://w3c.github.io/webauthn/#dictdef-authenticatorselectioncriteria W3C WebAuthn Spec - AuthenticatorSelectionCriteria} | |
| */ | |
| export function mergeCredentialCreationOptions( | |
| baseOptions: PublicKeyCredentialCreationOptionsFuture, | |
| overrides?: Partial<PublicKeyCredentialCreationOptionsFuture> | |
| ): PublicKeyCredentialCreationOptionsFuture { | |
| return deepMerge(DEFAULT_CREATION_OPTIONS, baseOptions, overrides || {}) | |
| } | |
| /** | |
| * Merges WebAuthn credential request options with overrides. | |
| * Sets sensible defaults for user verification and hints. | |
| * | |
| * @param {PublicKeyCredentialRequestOptionsFuture} baseOptions - The base options from the server | |
| * @param {PublicKeyCredentialRequestOptionsFuture} overrides - Optional overrides to apply | |
| * @returns {PublicKeyCredentialRequestOptionsFuture} Merged credential request options | |
| * @see {@link https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptions W3C WebAuthn Spec - PublicKeyCredentialRequestOptions} | |
| */ | |
| export function mergeCredentialRequestOptions( | |
| baseOptions: PublicKeyCredentialRequestOptionsFuture, | |
| overrides?: Partial<PublicKeyCredentialRequestOptionsFuture> | |
| ): PublicKeyCredentialRequestOptionsFuture { | |
| return deepMerge(DEFAULT_REQUEST_OPTIONS, baseOptions, overrides || {}) | |
| } | |
| /** | |
| * WebAuthn API wrapper for Supabase Auth. | |
| * Provides methods for enrolling, challenging, verifying, authenticating, and registering WebAuthn credentials. | |
| * | |
| * @experimental This API is experimental and may change in future releases | |
| * @see {@link https://w3c.github.io/webauthn/ W3C WebAuthn Specification} | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API MDN - Web Authentication API} | |
| */ | |
| export class WebAuthnApi { | |
| public enroll: typeof WebAuthnApi.prototype._enroll | |
| public challenge: typeof WebAuthnApi.prototype._challenge | |
| public verify: typeof WebAuthnApi.prototype._verify | |
| public authenticate: typeof WebAuthnApi.prototype._authenticate | |
| public register: typeof WebAuthnApi.prototype._register | |
| constructor(private client: GoTrueClient) { | |
| // Bind all methods so they can be destructured | |
| this.enroll = this._enroll.bind(this) | |
| this.challenge = this._challenge.bind(this) | |
| this.verify = this._verify.bind(this) | |
| this.authenticate = this._authenticate.bind(this) | |
| this.register = this._register.bind(this) | |
| } | |
| /** | |
| * Enroll a new WebAuthn factor. | |
| * Creates an unverified WebAuthn factor that must be verified with a credential. | |
| * | |
| * @experimental This method is experimental and may change in future releases | |
| * @param {Omit<MFAEnrollWebauthnParams, 'factorType'>} params - Enrollment parameters (friendlyName required) | |
| * @returns {Promise<AuthMFAEnrollWebauthnResponse>} Enrolled factor details or error | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registering a New Credential} | |
| */ | |
| public async _enroll( | |
| params: Omit<MFAEnrollWebauthnParams, 'factorType'> | |
| ): Promise<AuthMFAEnrollWebauthnResponse> { | |
| return this.client.mfa.enroll({ ...params, factorType: 'webauthn' }) | |
| } | |
| /** | |
| * Challenge for WebAuthn credential creation or authentication. | |
| * Combines server challenge with browser credential operations. | |
| * Handles both registration (create) and authentication (request) flows. | |
| * | |
| * @experimental This method is experimental and may change in future releases | |
| * @param {MFAChallengeWebauthnParams & { friendlyName?: string; signal?: AbortSignal }} params - Challenge parameters including factorId | |
| * @param {Object} overrides - Allows you to override the parameters passed to navigator.credentials | |
| * @param {PublicKeyCredentialCreationOptionsFuture} overrides.create - Override options for credential creation | |
| * @param {PublicKeyCredentialRequestOptionsFuture} overrides.request - Override options for credential request | |
| * @returns {Promise<RequestResult>} Challenge response with credential or error | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-credential-creation W3C WebAuthn Spec - Credential Creation} | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying Assertion} | |
| */ | |
| public async _challenge( | |
| { | |
| factorId, | |
| webauthn, | |
| friendlyName, | |
| signal, | |
| }: MFAChallengeWebauthnParams & { friendlyName?: string; signal?: AbortSignal }, | |
| overrides?: | |
| | { | |
| create?: Partial<PublicKeyCredentialCreationOptionsFuture> | |
| request?: never | |
| } | |
| | { | |
| create?: never | |
| request?: Partial<PublicKeyCredentialRequestOptionsFuture> | |
| } | |
| ): Promise< | |
| RequestResult< | |
| { factorId: string; challengeId: string } & { | |
| webauthn: StrictOmit< | |
| MFAVerifyWebauthnParamFields<'create' | 'request'>['webauthn'], | |
| 'rpId' | 'rpOrigins' | |
| > | |
| }, | |
| WebAuthnError | AuthError | |
| > | |
| > { | |
| try { | |
| // Get challenge from server using the client's MFA methods | |
| const { data: challengeResponse, error: challengeError } = await this.client.mfa.challenge({ | |
| factorId, | |
| webauthn, | |
| }) | |
| if (!challengeResponse) { | |
| return { data: null, error: challengeError } | |
| } | |
| const abortSignal = signal ?? webAuthnAbortService.createNewAbortSignal() | |
| /** webauthn will fail if either of the name/displayname are blank */ | |
| if (challengeResponse.webauthn.type === 'create') { | |
| const { user } = challengeResponse.webauthn.credential_options.publicKey | |
| if (!user.name) { | |
| // Preserve original format: use friendlyName if provided, otherwise fetch fallback | |
| // This maintains backward compatibility with the ${user.id}:${name} format | |
| const nameToUse = friendlyName | |
| if (!nameToUse) { | |
| // Only fetch user data if friendlyName is not provided (bug fix for null friendlyName) | |
| const currentUser = await this.client.getUser() | |
| const userData = currentUser.data.user | |
| const fallbackName = | |
| userData?.user_metadata?.name || userData?.email || userData?.id || 'User' | |
| user.name = `${user.id}:${fallbackName}` | |
| } else { | |
| user.name = `${user.id}:${nameToUse}` | |
| } | |
| } | |
| if (!user.displayName) { | |
| user.displayName = user.name | |
| } | |
| } | |
| switch (challengeResponse.webauthn.type) { | |
| case 'create': { | |
| const options = mergeCredentialCreationOptions( | |
| challengeResponse.webauthn.credential_options.publicKey, | |
| overrides?.create | |
| ) | |
| const { data, error } = await createCredential({ | |
| publicKey: options, | |
| signal: abortSignal, | |
| }) | |
| if (data) { | |
| return { | |
| data: { | |
| factorId, | |
| challengeId: challengeResponse.id, | |
| webauthn: { | |
| type: challengeResponse.webauthn.type, | |
| credential_response: data, | |
| }, | |
| }, | |
| error: null, | |
| } | |
| } | |
| return { data: null, error } | |
| } | |
| case 'request': { | |
| const options = mergeCredentialRequestOptions( | |
| challengeResponse.webauthn.credential_options.publicKey, | |
| overrides?.request | |
| ) | |
| const { data, error } = await getCredential({ | |
| ...challengeResponse.webauthn.credential_options, | |
| publicKey: options, | |
| signal: abortSignal, | |
| }) | |
| if (data) { | |
| return { | |
| data: { | |
| factorId, | |
| challengeId: challengeResponse.id, | |
| webauthn: { | |
| type: challengeResponse.webauthn.type, | |
| credential_response: data, | |
| }, | |
| }, | |
| error: null, | |
| } | |
| } | |
| return { data: null, error } | |
| } | |
| } | |
| } catch (error) { | |
| if (isAuthError(error)) { | |
| return { data: null, error } | |
| } | |
| return { | |
| data: null, | |
| error: new AuthUnknownError('Unexpected error in challenge', error), | |
| } | |
| } | |
| } | |
| /** | |
| * Verify a WebAuthn credential with the server. | |
| * Completes the WebAuthn ceremony by sending the credential to the server for verification. | |
| * | |
| * @experimental This method is experimental and may change in future releases | |
| * @param {Object} params - Verification parameters | |
| * @param {string} params.challengeId - ID of the challenge being verified | |
| * @param {string} params.factorId - ID of the WebAuthn factor | |
| * @param {MFAVerifyWebauthnParams<T>['webauthn']} params.webauthn - WebAuthn credential response | |
| * @returns {Promise<AuthMFAVerifyResponse>} Verification result with session or error | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-verifying-assertion W3C WebAuthn Spec - Verifying an Authentication Assertion} | |
| * */ | |
| public async _verify<T extends 'create' | 'request'>({ | |
| challengeId, | |
| factorId, | |
| webauthn, | |
| }: { | |
| challengeId: string | |
| factorId: string | |
| webauthn: MFAVerifyWebauthnParams<T>['webauthn'] | |
| }): Promise<AuthMFAVerifyResponse> { | |
| return this.client.mfa.verify({ | |
| factorId, | |
| challengeId, | |
| webauthn: webauthn, | |
| }) | |
| } | |
| /** | |
| * Complete WebAuthn authentication flow. | |
| * Performs challenge and verification in a single operation for existing credentials. | |
| * | |
| * @experimental This method is experimental and may change in future releases | |
| * @param {Object} params - Authentication parameters | |
| * @param {string} params.factorId - ID of the WebAuthn factor to authenticate with | |
| * @param {Object} params.webauthn - WebAuthn configuration | |
| * @param {string} params.webauthn.rpId - Relying Party ID (defaults to current hostname) | |
| * @param {string[]} params.webauthn.rpOrigins - Allowed origins (defaults to current origin) | |
| * @param {AbortSignal} params.webauthn.signal - Optional abort signal | |
| * @param {PublicKeyCredentialRequestOptionsFuture} overrides - Override options for navigator.credentials.get | |
| * @returns {Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>>} Authentication result | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-authentication W3C WebAuthn Spec - Authentication Ceremony} | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialRequestOptions MDN - PublicKeyCredentialRequestOptions} | |
| */ | |
| public async _authenticate( | |
| { | |
| factorId, | |
| webauthn: { | |
| rpId = typeof window !== 'undefined' ? window.location.hostname : undefined, | |
| rpOrigins = typeof window !== 'undefined' ? [window.location.origin] : undefined, | |
| signal, | |
| } = {}, | |
| }: { | |
| factorId: string | |
| webauthn?: { | |
| rpId?: string | |
| rpOrigins?: string[] | |
| signal?: AbortSignal | |
| } | |
| }, | |
| overrides?: PublicKeyCredentialRequestOptionsFuture | |
| ): Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>> { | |
| if (!rpId) { | |
| return { | |
| data: null, | |
| error: new AuthError('rpId is required for WebAuthn authentication'), | |
| } | |
| } | |
| try { | |
| if (!browserSupportsWebAuthn()) { | |
| return { | |
| data: null, | |
| error: new AuthUnknownError('Browser does not support WebAuthn', null), | |
| } | |
| } | |
| // Get challenge and credential | |
| const { data: challengeResponse, error: challengeError } = await this.challenge( | |
| { | |
| factorId, | |
| webauthn: { rpId, rpOrigins }, | |
| signal, | |
| }, | |
| { request: overrides } | |
| ) | |
| if (!challengeResponse) { | |
| return { data: null, error: challengeError } | |
| } | |
| const { webauthn } = challengeResponse | |
| // Verify credential | |
| return this._verify({ | |
| factorId, | |
| challengeId: challengeResponse.challengeId, | |
| webauthn: { | |
| type: webauthn.type, | |
| rpId, | |
| rpOrigins, | |
| credential_response: webauthn.credential_response, | |
| }, | |
| }) | |
| } catch (error) { | |
| if (isAuthError(error)) { | |
| return { data: null, error } | |
| } | |
| return { | |
| data: null, | |
| error: new AuthUnknownError('Unexpected error in authenticate', error), | |
| } | |
| } | |
| } | |
| /** | |
| * Complete WebAuthn registration flow. | |
| * Performs enrollment, challenge, and verification in a single operation for new credentials. | |
| * | |
| * @experimental This method is experimental and may change in future releases | |
| * @param {Object} params - Registration parameters | |
| * @param {string} params.friendlyName - User-friendly name for the credential | |
| * @param {string} params.rpId - Relying Party ID (defaults to current hostname) | |
| * @param {string[]} params.rpOrigins - Allowed origins (defaults to current origin) | |
| * @param {AbortSignal} params.signal - Optional abort signal | |
| * @param {PublicKeyCredentialCreationOptionsFuture} overrides - Override options for navigator.credentials.create | |
| * @returns {Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>>} Registration result | |
| * @see {@link https://w3c.github.io/webauthn/#sctn-registering-a-new-credential W3C WebAuthn Spec - Registration Ceremony} | |
| * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredentialCreationOptions MDN - PublicKeyCredentialCreationOptions} | |
| */ | |
| public async _register( | |
| { | |
| friendlyName, | |
| webauthn: { | |
| rpId = typeof window !== 'undefined' ? window.location.hostname : undefined, | |
| rpOrigins = typeof window !== 'undefined' ? [window.location.origin] : undefined, | |
| signal, | |
| } = {}, | |
| }: { | |
| friendlyName: string | |
| webauthn?: { | |
| rpId?: string | |
| rpOrigins?: string[] | |
| signal?: AbortSignal | |
| } | |
| }, | |
| overrides?: Partial<PublicKeyCredentialCreationOptionsFuture> | |
| ): Promise<RequestResult<AuthMFAVerifyResponseData, WebAuthnError | AuthError>> { | |
| if (!rpId) { | |
| return { | |
| data: null, | |
| error: new AuthError('rpId is required for WebAuthn registration'), | |
| } | |
| } | |
| try { | |
| if (!browserSupportsWebAuthn()) { | |
| return { | |
| data: null, | |
| error: new AuthUnknownError('Browser does not support WebAuthn', null), | |
| } | |
| } | |
| // Enroll factor | |
| const { data: factor, error: enrollError } = await this._enroll({ | |
| friendlyName, | |
| }) | |
| if (!factor) { | |
| await this.client.mfa | |
| .listFactors() | |
| .then((factors) => | |
| // Only an unverified factor is leftover from the enrollment that just | |
| // failed, and removing it is what lets a retry with the same name | |
| // succeed. A verified factor of that name is a passkey the user is | |
| // already using, so it must never be selected here. | |
| factors.data?.all.find( | |
| (v) => | |
| v.factor_type === 'webauthn' && | |
| v.friendly_name === friendlyName && | |
| v.status === 'unverified' | |
| ) | |
| ) | |
| .then((factor) => (factor ? this.client.mfa.unenroll({ factorId: factor?.id }) : void 0)) | |
| return { data: null, error: enrollError } | |
| } | |
| // Get challenge and create credential | |
| const { data: challengeResponse, error: challengeError } = await this._challenge( | |
| { | |
| factorId: factor.id, | |
| friendlyName: factor.friendly_name, | |
| webauthn: { rpId, rpOrigins }, | |
| signal, | |
| }, | |
| { | |
| create: overrides, | |
| } | |
| ) | |
| if (!challengeResponse) { | |
| return { data: null, error: challengeError } | |
| } | |
| return this._verify({ | |
| factorId: factor.id, | |
| challengeId: challengeResponse.challengeId, | |
| webauthn: { | |
| rpId, | |
| rpOrigins, | |
| type: challengeResponse.webauthn.type, | |
| credential_response: challengeResponse.webauthn.credential_response, | |
| }, | |
| }) | |
| } catch (error) { | |
| if (isAuthError(error)) { | |
| return { data: null, error } | |
| } | |
| return { | |
| data: null, | |
| error: new AuthUnknownError('Unexpected error in register', error), | |
| } | |
| } | |
| } | |
| } | |