diff options
Diffstat (limited to 'packages/browser/src')
-rw-r--r-- | packages/browser/src/helpers/identifyAuthenticationError.ts | 59 | ||||
-rw-r--r-- | packages/browser/src/helpers/identifyRegistrationError.ts | 88 | ||||
-rw-r--r-- | packages/browser/src/helpers/isValidDomain.ts | 14 | ||||
-rw-r--r-- | packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts | 3 | ||||
-rw-r--r-- | packages/browser/src/helpers/structs.ts | 23 | ||||
-rw-r--r-- | packages/browser/src/methods/startAuthentication.test.ts | 96 | ||||
-rw-r--r-- | packages/browser/src/methods/startAuthentication.ts | 10 | ||||
-rw-r--r-- | packages/browser/src/methods/startRegistration.test.ts | 187 | ||||
-rw-r--r-- | packages/browser/src/methods/startRegistration.ts | 10 | ||||
-rw-r--r-- | packages/browser/src/setupTests.ts | 85 |
10 files changed, 563 insertions, 12 deletions
diff --git a/packages/browser/src/helpers/identifyAuthenticationError.ts b/packages/browser/src/helpers/identifyAuthenticationError.ts new file mode 100644 index 0000000..7f9bd82 --- /dev/null +++ b/packages/browser/src/helpers/identifyAuthenticationError.ts @@ -0,0 +1,59 @@ +import { isValidDomain } from './isValidDomain'; +import { WebAuthnError } from './structs'; + +/** + * 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 === 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)'); + } + } else if (error.name === 'NotAllowedError') { + if (publicKey.allowCredentials?.length) { + // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 17) + // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 6) + return new WebAuthnError( + 'No available authenticator recognized any of the allowed credentials (NotAllowedError)', + ); + } + + // https://www.w3.org/TR/webauthn-2/#sctn-discover-from-external-source (Step 18) + // https://www.w3.org/TR/webauthn-2/#sctn-op-get-assertion (Step 7) + return new WebAuthnError( + 'User clicked cancel, or the authentication ceremony timed out (NotAllowedError)', + ); + } 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)`); + } 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)`, + ); + } + } 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 error; +} diff --git a/packages/browser/src/helpers/identifyRegistrationError.ts b/packages/browser/src/helpers/identifyRegistrationError.ts new file mode 100644 index 0000000..544953b --- /dev/null +++ b/packages/browser/src/helpers/identifyRegistrationError.ts @@ -0,0 +1,88 @@ +import { isValidDomain } from './isValidDomain'; +import { WebAuthnError } from './structs'; + +/** + * 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 === new AbortController().signal) { + // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 16) + return new WebAuthnError('Registration ceremony was sent an abort signal'); + } + } 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)', + ); + } 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)', + ); + } + } 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)'); + } else if (error.name === 'NotAllowedError') { + // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 20) + // https://www.w3.org/TR/webauthn-2/#sctn-createCredential (Step 21) + return new WebAuthnError( + 'User clicked cancel, or the registration ceremony timed out (NotAllowedError)', + ); + } 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)', + ); + } + + // 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)', + ); + } 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)`); + } 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)`, + ); + } + } 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)'); + } + } 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 error; +} diff --git a/packages/browser/src/helpers/isValidDomain.ts b/packages/browser/src/helpers/isValidDomain.ts new file mode 100644 index 0000000..4d2eedd --- /dev/null +++ b/packages/browser/src/helpers/isValidDomain.ts @@ -0,0 +1,14 @@ +/** + * 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 + */ +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) + ); +} diff --git a/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts index e8e53c7..3e0b65b 100644 --- a/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts +++ b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts @@ -7,7 +7,8 @@ beforeEach(() => { // @ts-ignore 2741 window.PublicKeyCredential = jest.fn().mockReturnValue(() => {}); - window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = mockIsUVPAA.mockResolvedValue(true); + window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = + mockIsUVPAA.mockResolvedValue(true); }); test('should return true when platform authenticator is available', async () => { diff --git a/packages/browser/src/helpers/structs.ts b/packages/browser/src/helpers/structs.ts new file mode 100644 index 0000000..66b6d63 --- /dev/null +++ b/packages/browser/src/helpers/structs.ts @@ -0,0 +1,23 @@ +/** + * 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 { + constructor(message: string) { + super(message); + this.name = 'WebAuthnError'; + } +} diff --git a/packages/browser/src/methods/startAuthentication.test.ts b/packages/browser/src/methods/startAuthentication.test.ts index 2e455d6..4be0ad6 100644 --- a/packages/browser/src/methods/startAuthentication.test.ts +++ b/packages/browser/src/methods/startAuthentication.test.ts @@ -8,6 +8,7 @@ import { import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; +import { WebAuthnError } from '../helpers/structs'; import startAuthentication from './startAuthentication'; @@ -199,3 +200,98 @@ test('should include extension results when no extensions specified', async () = expect(response.clientExtensionResults).toEqual({}); }); + +describe('WebAuthnError', () => { + describe('AbortError', () => { + /** + * We can't actually test this because nothing in startAuthentication() propagates the abort + * signal. But if you invoked WebAuthn via this and then manually sent an abort signal I guess + * this will catch. + * + * As a matter of fact I couldn't actually get any browser to respect the abort signal... + */ + test.skip('should identify abort signal', async () => { + mockNavigatorGet.mockRejectedValueOnce(new AbortError()); + + const rejected = await expect(startAuthentication(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/abort signal/i); + rejected.toThrow(/AbortError/); + }); + }); + + describe('NotAllowedError', () => { + test('should identify unrecognized allowed credentials', async () => { + mockNavigatorGet.mockRejectedValueOnce(new NotAllowedError()); + + const rejected = await expect(startAuthentication(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/allowed credentials/i); + rejected.toThrow(/NotAllowedError/); + }); + + test('should identify cancellation or timeout', async () => { + mockNavigatorGet.mockRejectedValueOnce(new NotAllowedError()); + + const opts = { + ...goodOpts1, + allowCredentials: [], + }; + + const rejected = await expect(startAuthentication(opts)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/cancel/i); + rejected.toThrow(/timed out/i); + rejected.toThrow(/NotAllowedError/); + }); + }); + + describe('SecurityError', () => { + let _originalHostName: string; + + beforeEach(() => { + _originalHostName = window.location.hostname; + }); + + afterEach(() => { + window.location.hostname = _originalHostName; + }); + + test('should identify invalid domain', async () => { + window.location.hostname = '1.2.3.4'; + + mockNavigatorGet.mockRejectedValueOnce(new SecurityError()); + + const rejected = await expect(startAuthentication(goodOpts1)).rejects; + rejected.toThrowError(WebAuthnError); + rejected.toThrow(/1\.2\.3\.4/); + rejected.toThrow(/invalid domain/i); + rejected.toThrow(/SecurityError/); + }); + + test('should identify invalid RP ID', async () => { + window.location.hostname = 'simplewebauthn.com'; + + mockNavigatorGet.mockRejectedValueOnce(new SecurityError()); + + const rejected = await expect(startAuthentication(goodOpts1)).rejects; + rejected.toThrowError(WebAuthnError); + rejected.toThrow(goodOpts1.rpId); + rejected.toThrow(/invalid for this domain/i); + rejected.toThrow(/SecurityError/); + }); + }); + + describe('UnknownError', () => { + test('should identify potential authenticator issues', async () => { + mockNavigatorGet.mockRejectedValueOnce(new UnknownError()); + + const rejected = await expect(startAuthentication(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/authenticator/i); + rejected.toThrow(/unable to process the specified options/i); + rejected.toThrow(/could not create a new assertion signature /i); + rejected.toThrow(/UnknownError/); + }); + }); +}); diff --git a/packages/browser/src/methods/startAuthentication.ts b/packages/browser/src/methods/startAuthentication.ts index 277b8f0..ee401a1 100644 --- a/packages/browser/src/methods/startAuthentication.ts +++ b/packages/browser/src/methods/startAuthentication.ts @@ -9,6 +9,7 @@ import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; import bufferToUTF8String from '../helpers/bufferToUTF8String'; import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; +import { identifyAuthenticationError } from '../helpers/identifyAuthenticationError'; /** * Begin authenticator "login" via WebAuthn assertion @@ -36,8 +37,15 @@ export default async function startAuthentication( allowCredentials, }; + const options: CredentialRequestOptions = { publicKey }; + // Wait for the user to complete assertion - const credential = (await navigator.credentials.get({ publicKey })) as AuthenticationCredential; + let credential; + try { + credential = (await navigator.credentials.get(options)) as AuthenticationCredential; + } catch (err) { + throw identifyAuthenticationError({ error: err as Error, options }); + } if (!credential) { throw new Error('Authentication was not completed'); diff --git a/packages/browser/src/methods/startRegistration.test.ts b/packages/browser/src/methods/startRegistration.test.ts index 76a01fb..78b0157 100644 --- a/packages/browser/src/methods/startRegistration.test.ts +++ b/packages/browser/src/methods/startRegistration.test.ts @@ -8,6 +8,7 @@ import { import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; +import { WebAuthnError } from '../helpers/structs'; import startRegistration from './startRegistration'; @@ -29,8 +30,8 @@ const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { }, ], rp: { - id: '1234', - name: 'simplewebauthn', + id: 'simplewebauthn.dev', + name: 'SimpleWebAuthn', }, user: { id: '5678', @@ -173,3 +174,185 @@ test('should include extension results when no extensions specified', async () = expect(response.clientExtensionResults).toEqual({}); }); + +describe('WebAuthnError', () => { + describe('AbortError', () => { + /** + * We can't actually test this because nothing in startRegistration() propagates the abort + * signal. But if you invoked WebAuthn via this and then manually sent an abort signal I guess + * this will catch. + * + * As a matter of fact I couldn't actually get any browser to respect the abort signal... + */ + test.skip('should identify abort signal', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new AbortError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/abort signal/i); + rejected.toThrow(/AbortError/); + }); + }); + + describe('ConstraintError', () => { + test('should identify unsupported discoverable credentials', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new ConstraintError()); + + const opts: PublicKeyCredentialCreationOptionsJSON = { + ...goodOpts1, + authenticatorSelection: { + residentKey: 'required', + requireResidentKey: true, + }, + }; + + const rejected = await expect(startRegistration(opts)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/discoverable credentials were required/i); + rejected.toThrow(/no available authenticator supported/i); + rejected.toThrow(/ConstraintError/); + }); + + test('should identify unsupported user verification', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new ConstraintError()); + + const opts: PublicKeyCredentialCreationOptionsJSON = { + ...goodOpts1, + authenticatorSelection: { + userVerification: 'required', + }, + }; + + const rejected = await expect(startRegistration(opts)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/user verification was required/i); + rejected.toThrow(/no available authenticator supported/i); + rejected.toThrow(/ConstraintError/); + }); + }); + + describe('InvalidStateError', () => { + test('should identify re-registration attempt', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new InvalidStateError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/authenticator/i); + rejected.toThrow(/previously registered/i); + rejected.toThrow(/InvalidStateError/); + }); + }); + + describe('NotAllowedError', () => { + test('should identify cancellation or timeout', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new NotAllowedError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/cancel/i); + rejected.toThrow(/timed out/i); + rejected.toThrow(/NotAllowedError/); + }); + }); + + describe('NotSupportedError', () => { + test('should identify missing "public-key" entries in pubKeyCredParams', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new NotSupportedError()); + + const opts = { + ...goodOpts1, + pubKeyCredParams: [], + }; + + const rejected = await expect(startRegistration(opts)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/pubKeyCredParams/i); + rejected.toThrow(/public-key/i); + rejected.toThrow(/NotSupportedError/); + }); + + test('should identify no authenticator supports algs in pubKeyCredParams', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new NotSupportedError()); + + const opts: PublicKeyCredentialCreationOptionsJSON = { + ...goodOpts1, + pubKeyCredParams: [{ alg: -7, type: 'public-key' }], + }; + + const rejected = await expect(startRegistration(opts)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/No available authenticator/i); + rejected.toThrow(/pubKeyCredParams/i); + rejected.toThrow(/NotSupportedError/); + }); + }); + + describe('SecurityError', () => { + let _originalHostName: string; + + beforeEach(() => { + _originalHostName = window.location.hostname; + }); + + afterEach(() => { + window.location.hostname = _originalHostName; + }); + + test('should identify invalid domain', async () => { + window.location.hostname = '1.2.3.4'; + + mockNavigatorCreate.mockRejectedValueOnce(new SecurityError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrowError(WebAuthnError); + rejected.toThrow(/1\.2\.3\.4/); + rejected.toThrow(/invalid domain/i); + rejected.toThrow(/SecurityError/); + }); + + test('should identify invalid RP ID', async () => { + window.location.hostname = 'simplewebauthn.com'; + + mockNavigatorCreate.mockRejectedValueOnce(new SecurityError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrowError(WebAuthnError); + rejected.toThrow(goodOpts1.rp.id); + rejected.toThrow(/invalid for this domain/i); + rejected.toThrow(/SecurityError/); + }); + }); + + describe('TypeError', () => { + test('should identify malformed user ID', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new TypeError('user id is bad')); + + const opts = { + ...goodOpts1, + user: { + ...goodOpts1.user, + id: Array(65).fill('a').join(''), + }, + }; + + const rejected = await expect(startRegistration(opts)).rejects; + rejected.toThrowError(WebAuthnError); + rejected.toThrow(/user id/i); + rejected.toThrow(/not between 1 and 64 characters/i); + rejected.toThrow(/TypeError/); + }); + }); + + describe('UnknownError', () => { + test('should identify potential authenticator issues', async () => { + mockNavigatorCreate.mockRejectedValueOnce(new UnknownError()); + + const rejected = await expect(startRegistration(goodOpts1)).rejects; + rejected.toThrow(WebAuthnError); + rejected.toThrow(/authenticator/i); + rejected.toThrow(/unable to process the specified options/i); + rejected.toThrow(/could not create a new credential/i); + rejected.toThrow(/UnknownError/); + }); + }); +}); diff --git a/packages/browser/src/methods/startRegistration.ts b/packages/browser/src/methods/startRegistration.ts index eec07c5..4037092 100644 --- a/packages/browser/src/methods/startRegistration.ts +++ b/packages/browser/src/methods/startRegistration.ts @@ -9,6 +9,7 @@ import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; +import { identifyRegistrationError } from '../helpers/identifyRegistrationError'; /** * Begin authenticator "registration" via WebAuthn attestation @@ -33,8 +34,15 @@ export default async function startRegistration( excludeCredentials: creationOptionsJSON.excludeCredentials.map(toPublicKeyCredentialDescriptor), }; + const options: CredentialCreationOptions = { publicKey }; + // Wait for the user to complete attestation - const credential = (await navigator.credentials.create({ publicKey })) as RegistrationCredential; + let credential; + try { + credential = (await navigator.credentials.create(options)) as RegistrationCredential; + } catch (err) { + throw identifyRegistrationError({ error: err as Error, options }); + } if (!credential) { throw new Error('Registration was not completed'); diff --git a/packages/browser/src/setupTests.ts b/packages/browser/src/setupTests.ts index 019ba42..fda8840 100644 --- a/packages/browser/src/setupTests.ts +++ b/packages/browser/src/setupTests.ts @@ -7,10 +7,81 @@ * JSDom doesn't seem to support `credentials`, so let's define them here so we can mock their * implementations in specific tests. */ -// @ts-ignore 2540 -window.navigator.credentials = { - // attestation - create: jest.fn(), - // assertion - get: jest.fn(), -}; +Object.defineProperty(window.navigator, 'credentials', { + writable: true, + value: { + create: jest.fn(), + get: jest.fn(), + }, +}); + +/** + * Allow for setting values to `window.location.hostname` + */ +Object.defineProperty(window, 'location', { + writable: true, + value: { + hostname: '', + }, +}); + +/** + * Define WebAuthn's custom API errors + */ + +class AbortError extends Error { + constructor() { + super(); + this.name = 'AbortError'; + } +} + +class ConstraintError extends Error { + constructor() { + super(); + this.name = 'ConstraintError'; + } +} + +class InvalidStateError extends Error { + constructor() { + super(); + this.name = 'InvalidStateError'; + } +} + +class NotAllowedError extends Error { + constructor() { + super(); + this.name = 'NotAllowedError'; + } +} + +class NotSupportedError extends Error { + constructor() { + super(); + this.name = 'NotSupportedError'; + } +} + +class SecurityError extends Error { + constructor() { + super(); + this.name = 'SecurityError'; + } +} + +class UnknownError extends Error { + constructor() { + super(); + this.name = 'UnknownError'; + } +} + +Object.defineProperty(global, 'AbortError', { value: AbortError }); +Object.defineProperty(global, 'ConstraintError', { value: ConstraintError }); +Object.defineProperty(global, 'InvalidStateError', { value: InvalidStateError }); +Object.defineProperty(global, 'NotAllowedError', { value: NotAllowedError }); +Object.defineProperty(global, 'NotSupportedError', { value: NotSupportedError }); +Object.defineProperty(global, 'SecurityError', { value: SecurityError }); +Object.defineProperty(global, 'UnknownError', { value: UnknownError }); |