diff options
Diffstat (limited to 'packages/browser/src/methods/startAuthentication.test.ts')
-rw-r--r-- | packages/browser/src/methods/startAuthentication.test.ts | 252 |
1 files changed, 142 insertions, 110 deletions
diff --git a/packages/browser/src/methods/startAuthentication.test.ts b/packages/browser/src/methods/startAuthentication.test.ts index f8830ae..62f3061 100644 --- a/packages/browser/src/methods/startAuthentication.test.ts +++ b/packages/browser/src/methods/startAuthentication.test.ts @@ -1,40 +1,41 @@ import { AuthenticationCredential, - PublicKeyCredentialRequestOptionsJSON, AuthenticationExtensionsClientInputs, AuthenticationExtensionsClientOutputs, -} from '@simplewebauthn/typescript-types'; + PublicKeyCredentialRequestOptionsJSON, +} from "@simplewebauthn/typescript-types"; -import { browserSupportsWebAuthn } from '../helpers/browserSupportsWebAuthn'; -import { browserSupportsWebAuthnAutofill } from '../helpers/browserSupportsWebAuthnAutofill'; -import { utf8StringToBuffer } from '../helpers/utf8StringToBuffer'; -import { bufferToBase64URLString } from '../helpers/bufferToBase64URLString'; -import { WebAuthnError } from '../helpers/webAuthnError'; -import { generateCustomError } from '../helpers/__jest__/generateCustomError'; -import { webauthnAbortService } from '../helpers/webAuthnAbortService'; +import { browserSupportsWebAuthn } from "../helpers/browserSupportsWebAuthn"; +import { browserSupportsWebAuthnAutofill } from "../helpers/browserSupportsWebAuthnAutofill"; +import { utf8StringToBuffer } from "../helpers/utf8StringToBuffer"; +import { bufferToBase64URLString } from "../helpers/bufferToBase64URLString"; +import { WebAuthnError } from "../helpers/webAuthnError"; +import { generateCustomError } from "../helpers/__jest__/generateCustomError"; +import { webauthnAbortService } from "../helpers/webAuthnAbortService"; -import { startAuthentication } from './startAuthentication'; +import { startAuthentication } from "./startAuthentication"; -jest.mock('../helpers/browserSupportsWebAuthn'); -jest.mock('../helpers/browserSupportsWebAuthnAutofill'); +jest.mock("../helpers/browserSupportsWebAuthn"); +jest.mock("../helpers/browserSupportsWebAuthnAutofill"); const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; const mockSupportsWebAuthn = browserSupportsWebAuthn as jest.Mock; const mockSupportsAutofill = browserSupportsWebAuthnAutofill as jest.Mock; -const mockAuthenticatorData = 'mockAuthenticatorData'; -const mockClientDataJSON = 'mockClientDataJSON'; -const mockSignature = 'mockSignature'; -const mockUserHandle = 'mockUserHandle'; +const mockAuthenticatorData = "mockAuthenticatorData"; +const mockClientDataJSON = "mockClientDataJSON"; +const mockSignature = "mockSignature"; +const mockUserHandle = "mockUserHandle"; // With ASCII challenge const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + challenge: bufferToBase64URLString(utf8StringToBuffer("fizz")), allowCredentials: [ { - id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', - type: 'public-key', - transports: ['nfc'], + id: + "C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg", + type: "public-key", + transports: ["nfc"], }, ], timeout: 1, @@ -42,7 +43,7 @@ const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { // With UTF-8 challenge const goodOpts2UTF8: PublicKeyCredentialRequestOptionsJSON = { - challenge: bufferToBase64URLString(utf8StringToBuffer('やれやれだぜ')), + challenge: bufferToBase64URLString(utf8StringToBuffer("やれやれだぜ")), allowCredentials: [], timeout: 1, }; @@ -50,7 +51,7 @@ const goodOpts2UTF8: PublicKeyCredentialRequestOptionsJSON = { beforeEach(() => { // Stub out a response so the method won't throw mockNavigatorGet.mockImplementation((): Promise<any> => { - return new Promise(resolve => { + return new Promise((resolve) => { resolve({ response: {}, getClientExtensionResults: () => ({}), @@ -72,102 +73,125 @@ afterEach(() => { mockSupportsAutofill.mockReset(); }); -test('should convert options before passing to navigator.credentials.get(...)', async () => { +test("should convert options before passing to navigator.credentials.get(...)", async () => { await startAuthentication(goodOpts1); const argsPublicKey = mockNavigatorGet.mock.calls[0][0].publicKey; const credId = argsPublicKey.allowCredentials[0].id; - expect(new Uint8Array(argsPublicKey.challenge)).toEqual(new Uint8Array([102, 105, 122, 122])); + expect(new Uint8Array(argsPublicKey.challenge)).toEqual( + new Uint8Array([102, 105, 122, 122]), + ); // Make sure the credential ID is an ArrayBuffer with a length of 64 expect(credId instanceof ArrayBuffer).toEqual(true); expect(credId.byteLength).toEqual(64); }); -test('should support optional allowCredential', async () => { +test("should support optional allowCredential", async () => { await startAuthentication({ - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + challenge: bufferToBase64URLString(utf8StringToBuffer("fizz")), timeout: 1, }); expect(mockNavigatorGet.mock.calls[0][0].allowCredentials).toEqual(undefined); }); -test('should convert allow allowCredential to undefined when empty', async () => { +test("should convert allow allowCredential to undefined when empty", async () => { await startAuthentication({ - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + challenge: bufferToBase64URLString(utf8StringToBuffer("fizz")), timeout: 1, allowCredentials: [], }); expect(mockNavigatorGet.mock.calls[0][0].allowCredentials).toEqual(undefined); }); -test('should return base64url-encoded response values', async () => { +test("should return base64url-encoded response values", async () => { mockNavigatorGet.mockImplementation((): Promise<AuthenticationCredential> => { - return new Promise(resolve => { + return new Promise((resolve) => { resolve({ - id: 'foobar', - rawId: Buffer.from('foobar', 'ascii'), + id: "foobar", + rawId: Buffer.from("foobar", "ascii"), response: { - authenticatorData: Buffer.from(mockAuthenticatorData, 'ascii'), - clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), - signature: Buffer.from(mockSignature, 'ascii'), - userHandle: Buffer.from(mockUserHandle, 'ascii'), + authenticatorData: Buffer.from(mockAuthenticatorData, "ascii"), + clientDataJSON: Buffer.from(mockClientDataJSON, "ascii"), + signature: Buffer.from(mockSignature, "ascii"), + userHandle: Buffer.from(mockUserHandle, "ascii"), }, getClientExtensionResults: () => ({}), - type: 'public-key', - authenticatorAttachment: '', + type: "public-key", + authenticatorAttachment: "", }); }); }); const response = await startAuthentication(goodOpts1); - expect(response.rawId).toEqual('Zm9vYmFy'); - expect(response.response.authenticatorData).toEqual('bW9ja0F1dGhlbnRpY2F0b3JEYXRh'); - expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWVudERhdGFKU09O'); - expect(response.response.signature).toEqual('bW9ja1NpZ25hdHVyZQ'); - expect(response.response.userHandle).toEqual('mockUserHandle'); + expect(response.rawId).toEqual("Zm9vYmFy"); + expect(response.response.authenticatorData).toEqual( + "bW9ja0F1dGhlbnRpY2F0b3JEYXRh", + ); + expect(response.response.clientDataJSON).toEqual("bW9ja0NsaWVudERhdGFKU09O"); + expect(response.response.signature).toEqual("bW9ja1NpZ25hdHVyZQ"); + expect(response.response.userHandle).toEqual("mockUserHandle"); }); test("should throw error if WebAuthn isn't supported", async () => { mockSupportsWebAuthn.mockReturnValue(false); await expect(startAuthentication(goodOpts1)).rejects.toThrow( - 'WebAuthn is not supported in this browser', + "WebAuthn is not supported in this browser", ); }); -test('should throw error if assertion is cancelled for some reason', async () => { +test("should throw error if assertion is cancelled for some reason", async () => { mockNavigatorGet.mockImplementation((): Promise<null> => { - return new Promise(resolve => { + return new Promise((resolve) => { resolve(null); }); }); - await expect(startAuthentication(goodOpts1)).rejects.toThrow('Authentication was not completed'); + await expect(startAuthentication(goodOpts1)).rejects.toThrow( + "Authentication was not completed", + ); }); -test('should handle UTF-8 challenges', async () => { +test("should handle UTF-8 challenges", async () => { await startAuthentication(goodOpts2UTF8); const argsPublicKey = mockNavigatorGet.mock.calls[0][0].publicKey; expect(new Uint8Array(argsPublicKey.challenge)).toEqual( new Uint8Array([ - 227, 130, 132, 227, 130, 140, 227, 130, 132, 227, 130, 140, 227, 129, 160, 227, 129, 156, + 227, + 130, + 132, + 227, + 130, + 140, + 227, + 130, + 132, + 227, + 130, + 140, + 227, + 129, + 160, + 227, + 129, + 156, ]), ); }); -test('should send extensions to authenticator if present in options', async () => { +test("should send extensions to authenticator if present in options", async () => { const extensions: AuthenticationExtensionsClientInputs = { credProps: true, - appid: 'appidHere', + appid: "appidHere", // @ts-ignore uvm: true, // @ts-ignore - appidExclude: 'appidExcludeHere', + appidExclude: "appidExcludeHere", }; const optsWithExts: PublicKeyCredentialRequestOptionsJSON = { ...goodOpts1, @@ -180,7 +204,7 @@ test('should send extensions to authenticator if present in options', async () = expect(argsExtensions).toEqual(extensions); }); -test('should not set any extensions if not present in options', async () => { +test("should not set any extensions if not present in options", async () => { await startAuthentication(goodOpts1); const argsExtensions = mockNavigatorGet.mock.calls[0][0].publicKey.extensions; @@ -188,7 +212,7 @@ test('should not set any extensions if not present in options', async () => { expect(argsExtensions).toEqual(undefined); }); -test('should include extension results', async () => { +test("should include extension results", async () => { const extResults: AuthenticationExtensionsClientOutputs = { appid: true, credProps: { @@ -198,7 +222,7 @@ test('should include extension results', async () => { // Mock extension return values from authenticator mockNavigatorGet.mockImplementation((): Promise<any> => { - return new Promise(resolve => { + return new Promise((resolve) => { resolve({ response: {}, getClientExtensionResults: () => extResults }); }); }); @@ -209,7 +233,7 @@ test('should include extension results', async () => { expect(response.clientExtensionResults).toEqual(extResults); }); -test('should include extension results when no extensions specified', async () => { +test("should include extension results when no extensions specified", async () => { const response = await startAuthentication(goodOpts1); expect(response.clientExtensionResults).toEqual({}); @@ -221,20 +245,23 @@ test('should support "cable" transport', async () => { allowCredentials: [ { ...goodOpts1.allowCredentials![0], - transports: ['cable'], + transports: ["cable"], }, ], }; await startAuthentication(opts); - expect(mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials[0].transports[0]).toEqual( - 'cable', + expect( + mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials[0] + .transports[0], + ).toEqual( + "cable", ); }); -test('should cancel an existing call when executed again', async () => { - const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); +test("should cancel an existing call when executed again", async () => { + const abortSpy = jest.spyOn(AbortController.prototype, "abort"); // Fire off a request and immediately attempt a second one startAuthentication(goodOpts1); @@ -242,13 +269,13 @@ test('should cancel an existing call when executed again', async () => { expect(abortSpy).toHaveBeenCalledTimes(1); }); -test('should set up autofill a.k.a. Conditional UI', async () => { +test("should set up autofill a.k.a. Conditional UI", async () => { const opts: PublicKeyCredentialRequestOptionsJSON = { ...goodOpts1, allowCredentials: [ { ...goodOpts1.allowCredentials![0], - transports: ['cable'], + transports: ["cable"], }, ], }; @@ -263,14 +290,16 @@ test('should set up autofill a.k.a. Conditional UI', async () => { await startAuthentication(opts, true); // The most important bit - expect(mockNavigatorGet.mock.calls[0][0].mediation).toEqual('conditional'); + expect(mockNavigatorGet.mock.calls[0][0].mediation).toEqual("conditional"); // The latest version of https://github.com/w3c/webauthn/pull/1576 says allowCredentials should // be an "empty list", as opposed to being undefined - expect(mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials).toBeDefined(); - expect(mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials.length).toEqual(0); + expect(mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials) + .toBeDefined(); + expect(mockNavigatorGet.mock.calls[0][0].publicKey.allowCredentials.length) + .toEqual(0); }); -test('should throw error if autofill not supported', async () => { +test("should throw error if autofill not supported", async () => { mockSupportsAutofill.mockResolvedValue(false); const rejected = await expect(startAuthentication(goodOpts1, true)).rejects; @@ -278,7 +307,7 @@ test('should throw error if autofill not supported', async () => { rejected.toThrow(/does not support webauthn autofill/i); }); -test('should throw error if no acceptable <input> is found', async () => { +test("should throw error if no acceptable <input> is found", async () => { // <input> is missing "webauthn" from the autocomplete attribute document.body.innerHTML = ` <form> @@ -293,26 +322,26 @@ test('should throw error if no acceptable <input> is found', async () => { rejected.toThrow(/no <input>/i); }); -test('should return authenticatorAttachment if present', async () => { +test("should return authenticatorAttachment if present", async () => { // Mock extension return values from authenticator mockNavigatorGet.mockImplementation((): Promise<any> => { - return new Promise(resolve => { + return new Promise((resolve) => { resolve({ response: {}, - getClientExtensionResults: () => { }, - authenticatorAttachment: 'cross-platform', + getClientExtensionResults: () => {}, + authenticatorAttachment: "cross-platform", }); }); }); const response = await startAuthentication(goodOpts1); - expect(response.authenticatorAttachment).toEqual('cross-platform'); + expect(response.authenticatorAttachment).toEqual("cross-platform"); }); -describe('WebAuthnError', () => { - describe('AbortError', () => { - const AbortError = generateCustomError('AbortError'); +describe("WebAuthnError", () => { + describe("AbortError", () => { + const AbortError = generateCustomError("AbortError"); /** * We can't actually test this because nothing in startAuthentication() propagates the abort @@ -321,38 +350,41 @@ describe('WebAuthnError', () => { * * As a matter of fact I couldn't actually get any browser to respect the abort signal... */ - test.skip('should identify abort signal', async () => { + test.skip("should identify abort signal", async () => { mockNavigatorGet.mockRejectedValueOnce(AbortError); const rejected = await expect(startAuthentication(goodOpts1)).rejects; rejected.toThrow(WebAuthnError); rejected.toThrow(/abort signal/i); - rejected.toHaveProperty('name', 'AbortError'); - rejected.toHaveProperty('code', 'ERROR_CEREMONY_ABORTED'); - rejected.toHaveProperty('cause', AbortError); + rejected.toHaveProperty("name", "AbortError"); + rejected.toHaveProperty("code", "ERROR_CEREMONY_ABORTED"); + rejected.toHaveProperty("cause", AbortError); }); }); - describe('NotAllowedError', () => { - test('should pass through error message (iOS Safari - Operation failed)', async () => { + describe("NotAllowedError", () => { + test("should pass through error message (iOS Safari - Operation failed)", async () => { /** * Thrown when biometric is not enrolled, or a Safari bug prevents conditional UI from being * aborted properly between page reloads. * * See https://github.com/MasterKale/SimpleWebAuthn/discussions/350#discussioncomment-4896572 */ - const NotAllowedError = generateCustomError('NotAllowedError', 'Operation failed.'); + const NotAllowedError = generateCustomError( + "NotAllowedError", + "Operation failed.", + ); mockNavigatorGet.mockRejectedValueOnce(NotAllowedError); const rejected = await expect(startAuthentication(goodOpts1)).rejects; rejected.toThrow(Error); rejected.toThrow(/operation failed/i); - rejected.toHaveProperty('name', 'NotAllowedError'); - rejected.toHaveProperty('code', 'ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY'); - rejected.toHaveProperty('cause', NotAllowedError); + 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 () => { + test("should pass through error message (Chrome M110 - Bad TLS Cert)", async () => { /** * Starting from Chrome M110, WebAuthn is blocked if the site is being displayed on a URL with * TLS certificate issues. This includes during development. @@ -360,22 +392,22 @@ describe('WebAuthnError', () => { * See https://github.com/MasterKale/SimpleWebAuthn/discussions/351#discussioncomment-4910458 */ const NotAllowedError = generateCustomError( - 'NotAllowedError', - 'WebAuthn is not supported on sites with TLS certificate errors.' + "NotAllowedError", + "WebAuthn is not supported on sites with TLS certificate errors.", ); mockNavigatorGet.mockRejectedValueOnce(NotAllowedError); const rejected = await expect(startAuthentication(goodOpts1)).rejects; 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); + rejected.toHaveProperty("name", "NotAllowedError"); + rejected.toHaveProperty("code", "ERROR_PASSTHROUGH_SEE_CAUSE_PROPERTY"); + rejected.toHaveProperty("cause", NotAllowedError); }); }); - describe('SecurityError', () => { - const SecurityError = generateCustomError('SecurityError'); + describe("SecurityError", () => { + const SecurityError = generateCustomError("SecurityError"); let _originalHostName: string; @@ -387,8 +419,8 @@ describe('WebAuthnError', () => { window.location.hostname = _originalHostName; }); - test('should identify invalid domain', async () => { - window.location.hostname = '1.2.3.4'; + test("should identify invalid domain", async () => { + window.location.hostname = "1.2.3.4"; mockNavigatorGet.mockRejectedValueOnce(SecurityError); @@ -396,13 +428,13 @@ describe('WebAuthnError', () => { rejected.toThrowError(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); + rejected.toHaveProperty("name", "SecurityError"); + rejected.toHaveProperty("code", "ERROR_INVALID_DOMAIN"); + rejected.toHaveProperty("cause", SecurityError); }); - test('should identify invalid RP ID', async () => { - window.location.hostname = 'simplewebauthn.com'; + test("should identify invalid RP ID", async () => { + window.location.hostname = "simplewebauthn.com"; mockNavigatorGet.mockRejectedValueOnce(SecurityError); @@ -410,16 +442,16 @@ describe('WebAuthnError', () => { rejected.toThrowError(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); + rejected.toHaveProperty("name", "SecurityError"); + rejected.toHaveProperty("code", "ERROR_INVALID_RP_ID"); + rejected.toHaveProperty("cause", SecurityError); }); }); - describe('UnknownError', () => { - const UnknownError = generateCustomError('UnknownError'); + describe("UnknownError", () => { + const UnknownError = generateCustomError("UnknownError"); - test('should identify potential authenticator issues', async () => { + test("should identify potential authenticator issues", async () => { mockNavigatorGet.mockRejectedValueOnce(UnknownError); const rejected = await expect(startAuthentication(goodOpts1)).rejects; @@ -427,9 +459,9 @@ describe('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.toHaveProperty('name', 'UnknownError'); - rejected.toHaveProperty('code', 'ERROR_AUTHENTICATOR_GENERAL_ERROR'); - rejected.toHaveProperty('cause', UnknownError); + rejected.toHaveProperty("name", "UnknownError"); + rejected.toHaveProperty("code", "ERROR_AUTHENTICATOR_GENERAL_ERROR"); + rejected.toHaveProperty("cause", UnknownError); }); }); }); |