diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index d9c843d2..a98188db 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -37,7 +37,6 @@ import { getCodeChallengeAndMethod, base64URLStringToBuffer, bufferToBase64URLString, - identifyAuthenticationError, startRegistration, startAuthentication, } from './lib/helpers' diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 2c228edc..757f3cca 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -3,7 +3,6 @@ import { SupportedStorage, PublicKeyCredentialDescriptorJSON, AuthenticatorAttachment, - WebAuthnError, AuthenticatorTransportFuture, RegistrationCredential, PublicKeyCredentialCreationOptionsJSON, @@ -531,12 +530,7 @@ export async function startRegistration( options.signal = WebAuthnAbortService.createNewAbortSignal() // Wait for the user to complete attestation - let credential - try { - credential = (await navigator.credentials.create(options)) as RegistrationCredential - } catch (err) { - throw identifyRegistrationError({ error: err as Error, options }) - } + const credential = (await navigator.credentials.create(options)) as RegistrationCredential if (!credential) { throw new Error('Registration was not completed') @@ -599,193 +593,6 @@ export async function startRegistration( } } -/** - * Attempt to intuit _why_ an error was raised after calling `navigator.credentials.create()` - */ -export function identifyRegistrationError({ - error, - options, -}: { - error: Error - options: CredentialCreationOptions -}): WebAuthnError | Error { - const { publicKey } = options - - if (!publicKey) { - throw Error('options was missing required publicKey property') - } - - if (error.name === 'AbortError') { - if (options.signal instanceof AbortSignal) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16) - return new WebAuthnError({ - message: 'Registration ceremony was sent an abort signal', - code: 'ERROR_CEREMONY_ABORTED', - cause: error, - }) - } - } else if (error.name === 'ConstraintError') { - if (publicKey.authenticatorSelection?.requireResidentKey === true) { - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 4) - return new WebAuthnError({ - message: - 'Discoverable credentials were required but no available authenticator supported it', - code: 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT', - cause: error, - }) - } else if (publicKey.authenticatorSelection?.userVerification === 'required') { - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 5) - return new WebAuthnError({ - message: 'User verification was required but no available authenticator supported it', - code: 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT', - cause: error, - }) - } - } else if (error.name === 'InvalidStateError') { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 20) - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 3) - return new WebAuthnError({ - message: 'The authenticator was previously registered', - code: 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED', - cause: error, - }) - } else if (error.name === 'NotAllowedError') { - /** - * Pass the error directly through. Platforms are overloading this error beyond what the spec - * defines and we don't want to overwrite potentially useful error messages. - */ - return new WebAuthnError({ - message: error.message, - code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', - cause: error, - }) - } else if (error.name === 'NotSupportedError') { - const validPubKeyCredParams = publicKey.pubKeyCredParams.filter( - (param) => param.type === 'public-key' - ) - - if (validPubKeyCredParams.length === 0) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 10) - return new WebAuthnError({ - message: 'No entry in pubKeyCredParams was of type "public-key"', - code: 'ERROR_MALFORMED_PUBKEYCREDPARAMS', - cause: error, - }) - } - - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 2) - return new WebAuthnError({ - message: - 'No available authenticator supported any of the specified pubKeyCredParams algorithms', - code: 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG', - cause: error, - }) - } else if (error.name === 'SecurityError') { - const effectiveDomain = window.location.hostname - if (!isValidDomain(effectiveDomain)) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 7) - return new WebAuthnError({ - message: `${window.location.hostname} is an invalid domain`, - code: 'ERROR_INVALID_DOMAIN', - cause: error, - }) - } else if (publicKey.rp.id !== effectiveDomain) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 8) - return new WebAuthnError({ - message: `The RP ID "${publicKey.rp.id}" is invalid for this domain`, - code: 'ERROR_INVALID_RP_ID', - cause: error, - }) - } - } else if (error.name === 'TypeError') { - if (publicKey.user.id.byteLength < 1 || publicKey.user.id.byteLength > 64) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 5) - return new WebAuthnError({ - message: 'User ID was not between 1 and 64 characters', - code: 'ERROR_INVALID_USER_ID_LENGTH', - cause: error, - }) - } - } else if (error.name === 'UnknownError') { - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 1) - // https://www.w3.org/TR/webauthn-2/#sctn-op-make-cred (Step 8) - return new WebAuthnError({ - message: - 'The authenticator was unable to process the specified options, or could not create a new credential', - code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', - cause: error, - }) - } - - return error -} - -/** - * Attempt to intuit _why_ an error was raised after calling `navigator.credentials.get()` - */ -export function identifyAuthenticationError({ - error, - options, -}: { - error: Error - options: CredentialRequestOptions -}): WebAuthnError | Error { - const { publicKey } = options - - if (!publicKey) { - throw Error('options was missing required publicKey property') - } - - if (error.name === 'AbortError') { - if (options.signal instanceof AbortSignal) { - // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16) - return new WebAuthnError({ - message: 'Authentication ceremony was sent an abort signal', - code: 'ERROR_CEREMONY_ABORTED', - cause: error, - }) - } - } else if (error.name === 'NotAllowedError') { - /** - * Pass the error directly through. Platforms are overloading this error beyond what the spec - * defines and we don't want to overwrite potentially useful error messages. - */ - return new WebAuthnError({ - message: error.message, - code: 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY', - cause: error, - }) - } else if (error.name === 'SecurityError') { - const effectiveDomain = window.location.hostname - if (!isValidDomain(effectiveDomain)) { - // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 5) - return new WebAuthnError({ - message: `${window.location.hostname} is an invalid domain`, - code: 'ERROR_INVALID_DOMAIN', - cause: error, - }) - } else if (publicKey.rpId !== effectiveDomain) { - // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 6) - return new WebAuthnError({ - message: `The RP ID "${publicKey.rpId}" is invalid for this domain`, - code: 'ERROR_INVALID_RP_ID', - cause: error, - }) - } - } else if (error.name === 'UnknownError') { - // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 1) - // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 12) - return new WebAuthnError({ - message: - 'The authenticator was unable to process the specified options, or could not create a new assertion signature', - code: 'ERROR_AUTHENTICATOR_GENERAL_ERROR', - cause: error, - }) - } - - return error -} - /** * Begin authenticator "login" via WebAuthn assertion * @@ -793,8 +600,7 @@ export function identifyAuthenticationError({ * @param useBrowserAutofill (Optional) Initialize conditional UI to enable logging in via browser autofill prompts. Defaults to `false`. */ export async function startAuthentication( - optionsJSON: PublicKeyCredentialRequestOptionsJSON, - useBrowserAutofill = false + optionsJSON: PublicKeyCredentialRequestOptionsJSON ): Promise { if (!browserSupportsWebAuthn()) { throw new Error('WebAuthn is not supported in this browser') @@ -850,12 +656,7 @@ export async function startAuthentication( options.signal = WebAuthnAbortService.createNewAbortSignal() // Wait for the user to complete assertion - let credential - try { - credential = (await navigator.credentials.get(options)) as AuthenticationCredential - } catch (err) { - throw identifyAuthenticationError({ error: err as Error, options }) - } + const credential = (await navigator.credentials.get(options)) as AuthenticationCredential if (!credential) { throw new Error('Authentication was not completed') @@ -889,18 +690,3 @@ export function browserSupportsWebAuthn(): boolean { window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function' ) } - -/** - * A simple test to determine if a hostname is a properly-formatted domain name - * - * 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 - */ -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) - ) -} diff --git a/src/lib/types.ts b/src/lib/types.ts index 3a57200d..0cd3d908 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1408,54 +1408,3 @@ export interface PublicKeyCredentialFuture extends PublicKeyCredential { // See https://w3c.github.io/webauthn/#dom-publickeycredential-tojson toJSON?(): PublicKeyCredentialJSON } - -/** - * A custom Error used to return a more nuanced error detailing _why_ one of the eight documented - * errors in the spec was raised after calling `navigator.credentials.create()` or - * `navigator.credentials.get()`: - * - * - `AbortError` - * - `ConstraintError` - * - `InvalidStateError` - * - `NotAllowedError` - * - `NotSupportedError` - * - `SecurityError` - * - `TypeError` - * - `UnknownError` - * - * Error messages were determined through investigation of the spec to determine under which - * scenarios a given error would be raised. - */ -export class WebAuthnError extends Error { - code: WebAuthnErrorCode - - constructor({ - message, - code, - cause, - name, - }: { - message: string - code: WebAuthnErrorCode - cause: Error - name?: string - }) { - // @ts-ignore: help Rollup understand that `cause` is okay to set - super(message, { cause }) - this.name = name ?? cause.name - this.code = code - } -} - -export type WebAuthnErrorCode = - | 'ERROR_CEREMONY_ABORTED' - | 'ERROR_INVALID_DOMAIN' - | 'ERROR_INVALID_RP_ID' - | 'ERROR_INVALID_USER_ID_LENGTH' - | 'ERROR_MALFORMED_PUBKEYCREDPARAMS' - | 'ERROR_AUTHENTICATOR_GENERAL_ERROR' - | 'ERROR_AUTHENTICATOR_MISSING_DISCOVERABLE_CREDENTIAL_SUPPORT' - | 'ERROR_AUTHENTICATOR_MISSING_USER_VERIFICATION_SUPPORT' - | 'ERROR_AUTHENTICATOR_PREVIOUSLY_REGISTERED' - | 'ERROR_AUTHENTICATOR_NO_SUPPORTED_PUBKEYCREDPARAMS_ALG' - | 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY'