summaryrefslogtreecommitdiffhomepage
path: root/packages/browser/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/browser/src')
-rw-r--r--packages/browser/src/helpers/identifyAuthenticationError.ts59
-rw-r--r--packages/browser/src/helpers/identifyRegistrationError.ts88
-rw-r--r--packages/browser/src/helpers/isValidDomain.ts14
-rw-r--r--packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts3
-rw-r--r--packages/browser/src/helpers/structs.ts23
-rw-r--r--packages/browser/src/methods/startAuthentication.test.ts96
-rw-r--r--packages/browser/src/methods/startAuthentication.ts10
-rw-r--r--packages/browser/src/methods/startRegistration.test.ts187
-rw-r--r--packages/browser/src/methods/startRegistration.ts10
-rw-r--r--packages/browser/src/setupTests.ts85
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 });