diff options
Diffstat (limited to 'packages/browser/src/methods')
4 files changed, 299 insertions, 4 deletions
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'); |