Skip to content

Commit

Permalink
fix: update credential and authentication error
Browse files Browse the repository at this point in the history
  • Loading branch information
J0 committed Oct 9, 2024
1 parent 0d6fce4 commit 1670bf1
Show file tree
Hide file tree
Showing 3 changed files with 3 additions and 269 deletions.
1 change: 0 additions & 1 deletion src/GoTrueClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
getCodeChallengeAndMethod,
base64URLStringToBuffer,
bufferToBase64URLString,
identifyAuthenticationError,
startRegistration,
startAuthentication,
} from './lib/helpers'
Expand Down
220 changes: 3 additions & 217 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
SupportedStorage,
PublicKeyCredentialDescriptorJSON,
AuthenticatorAttachment,
WebAuthnError,
AuthenticatorTransportFuture,
RegistrationCredential,
PublicKeyCredentialCreationOptionsJSON,
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -599,202 +593,14 @@ 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
*
* @param optionsJSON Output from **@simplewebauthn/server**'s `generateAuthenticationOptions()`
* @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<AuthenticationResponseJSON> {
if (!browserSupportsWebAuthn()) {
throw new Error('WebAuthn is not supported in this browser')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
)
}
51 changes: 0 additions & 51 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit 1670bf1

Please sign in to comment.