Skip to content

Commit

Permalink
Merge pull request #367 from MasterKale/feat/better-errors
Browse files Browse the repository at this point in the history
feat/better-errors
  • Loading branch information
MasterKale authored Mar 16, 2023
2 parents 70f7e79 + ac97005 commit cc6b4e8
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { PublicKeyCredentialFuture } from '@simplewebauthn/typescript-types';

/**
Expand Down
37 changes: 26 additions & 11 deletions packages/browser/src/helpers/identifyAuthenticationError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isValidDomain } from './isValidDomain';
import { WebAuthnError } from './structs';
import { WebAuthnError } from './webAuthnError';

/**
* Attempt to intuit _why_ an error was raised after calling `navigator.credentials.get()`
Expand All @@ -20,32 +20,47 @@ export function identifyAuthenticationError({
if (error.name === 'AbortError') {
if (options.signal === new AbortController().signal) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16)
return new WebAuthnError('Authentication ceremony was sent an abort signal', 'AbortError');
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(`${window.location.hostname} is an invalid domain`, 'SecurityError');
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(
`The RP ID "${publicKey.rpId}" is invalid for this domain`,
'SecurityError',
);
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(
'The authenticator was unable to process the specified options, or could not create a new assertion signature',
'UnknownError',
);
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;
Expand Down
85 changes: 56 additions & 29 deletions packages/browser/src/helpers/identifyRegistrationError.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isValidDomain } from './isValidDomain';
import { WebAuthnError } from './structs';
import { WebAuthnError } from './webAuthnError';

/**
* Attempt to intuit _why_ an error was raised after calling `navigator.credentials.create()`
Expand All @@ -20,73 +20,100 @@ export function identifyRegistrationError({
if (error.name === 'AbortError') {
if (options.signal === new AbortController().signal) {
// https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16)
return new WebAuthnError('Registration ceremony was sent an abort signal', 'AbortError');
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(
'Discoverable credentials were required but no available authenticator supported it',
'ConstraintError',
);
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(
'User verification was required but no available authenticator supported it',
'ConstraintError',
);
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('The authenticator was previously registered', 'InvalidStateError');
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(
'No entry in pubKeyCredParams was of type "public-key"',
'NotSupportedError',
);
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(
'No available authenticator supported any of the specified pubKeyCredParams algorithms',
'NotSupportedError',
);
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(`${window.location.hostname} is an invalid domain`, 'SecurityError');
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(
`The RP ID "${publicKey.rp.id}" is invalid for this domain`,
'SecurityError',
);
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('User ID was not between 1 and 64 characters', 'TypeError');
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(
'The authenticator was unable to process the specified options, or could not create a new credential',
'UnknownError',
);
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;
Expand Down
23 changes: 0 additions & 23 deletions packages/browser/src/helpers/structs.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/browser/src/helpers/webAuthnAbortService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ test('should call abort() on existing controller when creating a new signal', ()
// Spy on the existing instance of AbortController
const abortSpy = jest.fn();
// @ts-ignore
webauthnAbortService.controller?.abort = abortSpy;
webauthnAbortService.controller.abort = abortSpy;

// Generate a new signal, which should call `abort()` on the existing controller
webauthnAbortService.createNewAbortSignal();
Expand Down
56 changes: 56 additions & 0 deletions packages/browser/src/helpers/webAuthnError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/**
* 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,
}) {
/**
* `cause` is supported in evergreen browsers, but not IE10, so this ts-ignore is to
* help Rollup complete the ES5 build.
*/
// @ts-ignore
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'
;
2 changes: 2 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ export {
browserSupportsWebAuthnAutofill,
platformAuthenticatorIsAvailable,
};

export type { WebAuthnErrorCode } from './helpers/webAuthnError';
16 changes: 14 additions & 2 deletions packages/browser/src/methods/startAuthentication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn';
import { browserSupportsWebAuthnAutofill } from '../helpers/browserSupportsWebAuthnAutofill';
import { utf8StringToBuffer } from '../helpers/utf8StringToBuffer';
import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString';
import { WebAuthnError } from '../helpers/structs';
import { WebAuthnError } from '../helpers/webAuthnError';
import { generateCustomError } from '../helpers/__jest__/generateCustomError';
import { webauthnAbortService } from '../helpers/webAuthnAbortService';

Expand Down Expand Up @@ -299,7 +299,7 @@ test('should return authenticatorAttachment if present', async () => {
return new Promise(resolve => {
resolve({
response: {},
getClientExtensionResults: () => {},
getClientExtensionResults: () => { },
authenticatorAttachment: 'cross-platform',
});
});
Expand Down Expand Up @@ -328,6 +328,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(WebAuthnError);
rejected.toThrow(/abort signal/i);
rejected.toHaveProperty('name', 'AbortError');
rejected.toHaveProperty('code', 'ERROR_CEREMONY_ABORTED');
rejected.toHaveProperty('cause', AbortError);
});
});

Expand All @@ -346,6 +348,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(Error);
rejected.toThrow(/operation failed/i);
rejected.toHaveProperty('name', 'NotAllowedError');
rejected.toHaveProperty('code', 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY');
rejected.toHaveProperty('cause', NotAllowedError);
});

test('should pass through error message (Chrome M110 - Bad TLS Cert)', async () => {
Expand All @@ -365,6 +369,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(Error);
rejected.toThrow(/sites with TLS certificate errors/i);
rejected.toHaveProperty('name', 'NotAllowedError');
rejected.toHaveProperty('code', 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY');
rejected.toHaveProperty('cause', NotAllowedError);
});
});

Expand All @@ -391,6 +397,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(/1\.2\.3\.4/);
rejected.toThrow(/invalid domain/i);
rejected.toHaveProperty('name', 'SecurityError');
rejected.toHaveProperty('code', 'ERROR_INVALID_DOMAIN');
rejected.toHaveProperty('cause', SecurityError);
});

test('should identify invalid RP ID', async () => {
Expand All @@ -403,6 +411,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(goodOpts1.rpId);
rejected.toThrow(/invalid for this domain/i);
rejected.toHaveProperty('name', 'SecurityError');
rejected.toHaveProperty('code', 'ERROR_INVALID_RP_ID');
rejected.toHaveProperty('cause', SecurityError);
});
});

Expand All @@ -418,6 +428,8 @@ describe('WebAuthnError', () => {
rejected.toThrow(/unable to process the specified options/i);
rejected.toThrow(/could not create a new assertion signature/i);
rejected.toHaveProperty('name', 'UnknownError');
rejected.toHaveProperty('code', 'ERROR_AUTHENTICATOR_GENERAL_ERROR');
rejected.toHaveProperty('cause', UnknownError);
});
});
});
Loading

0 comments on commit cc6b4e8

Please sign in to comment.