From 09c073098fc2ea506692de614d578c805b74705a Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Mon, 23 Aug 2021 20:12:35 -0700 Subject: Add new platformAuthenticatorIsAvailable helper --- packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts (limited to 'packages/browser/src') diff --git a/packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts new file mode 100644 index 0000000..10d84e3 --- /dev/null +++ b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts @@ -0,0 +1,9 @@ +/** + * Determine whether the browser can communicate with a built-in authenticator, like + * Touch ID, Android fingerprint scanner, or Windows Hello. + * + * This method will _not_ be able to tell you the name of the platform authenticator. + */ +export async function platformAuthenticatorIsAvailable(): Promise { + return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); +} -- cgit v1.2.3 From 808256a244c6e21c2db6b29bfc0cfe24268b460c Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Mon, 23 Aug 2021 20:18:27 -0700 Subject: Rename supportsWebAuthn (and fix tests) --- .../helpers/__mocks__/browserSupportsWebauthn.ts | 2 + .../src/helpers/__mocks__/supportsWebauthn.ts | 2 - .../src/helpers/browserSupportsWebauthn.test.ts | 27 ++++++ .../browser/src/helpers/browserSupportsWebauthn.ts | 8 ++ .../browser/src/helpers/supportsWebauthn.test.ts | 27 ------ packages/browser/src/helpers/supportsWebauthn.ts | 8 -- packages/browser/src/index.test.ts | 4 +- packages/browser/src/index.ts | 4 +- .../browser/src/methods/startAssertion.test.ts | 97 ++++++++-------------- packages/browser/src/methods/startAssertion.ts | 4 +- .../browser/src/methods/startAttestation.test.ts | 70 +++++++--------- packages/browser/src/methods/startAttestation.ts | 4 +- 12 files changed, 112 insertions(+), 145 deletions(-) create mode 100644 packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts delete mode 100644 packages/browser/src/helpers/__mocks__/supportsWebauthn.ts create mode 100644 packages/browser/src/helpers/browserSupportsWebauthn.test.ts create mode 100644 packages/browser/src/helpers/browserSupportsWebauthn.ts delete mode 100644 packages/browser/src/helpers/supportsWebauthn.test.ts delete mode 100644 packages/browser/src/helpers/supportsWebauthn.ts (limited to 'packages/browser/src') diff --git a/packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts b/packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts new file mode 100644 index 0000000..20d5d88 --- /dev/null +++ b/packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts @@ -0,0 +1,2 @@ +// We just need a simple mock so we can control whether this returns `true` or `false` +export const browserSupportsWebauthn = jest.fn(); diff --git a/packages/browser/src/helpers/__mocks__/supportsWebauthn.ts b/packages/browser/src/helpers/__mocks__/supportsWebauthn.ts deleted file mode 100644 index b6b47d4..0000000 --- a/packages/browser/src/helpers/__mocks__/supportsWebauthn.ts +++ /dev/null @@ -1,2 +0,0 @@ -// We just need a simple mock so we can control whether this returns `true` or `false` -export default jest.fn(); diff --git a/packages/browser/src/helpers/browserSupportsWebauthn.test.ts b/packages/browser/src/helpers/browserSupportsWebauthn.test.ts new file mode 100644 index 0000000..5308315 --- /dev/null +++ b/packages/browser/src/helpers/browserSupportsWebauthn.test.ts @@ -0,0 +1,27 @@ +import { browserSupportsWebauthn } from './browserSupportsWebauthn'; + +beforeEach(() => { + // @ts-ignore 2741 + window.PublicKeyCredential = jest.fn().mockReturnValue(() => {}); +}); + +test('should return true when browser supports WebAuthn', () => { + expect(browserSupportsWebauthn()).toBe(true); +}); + +test('should return false when browser does not support WebAuthn', () => { + delete (window as any).PublicKeyCredential; + expect(browserSupportsWebauthn()).toBe(false); +}); + +test('should return false when window is undefined', () => { + // Make window undefined as it is in node environments. + const windowSpy = jest.spyOn(global, 'window', 'get'); + windowSpy.mockImplementation(() => undefined); + + expect(window).toBe(undefined); + expect(browserSupportsWebauthn()).toBe(false); + + // Restore original window value. + windowSpy.mockRestore(); +}); diff --git a/packages/browser/src/helpers/browserSupportsWebauthn.ts b/packages/browser/src/helpers/browserSupportsWebauthn.ts new file mode 100644 index 0000000..030256f --- /dev/null +++ b/packages/browser/src/helpers/browserSupportsWebauthn.ts @@ -0,0 +1,8 @@ +/** + * Determine if the browser is capable of Webauthn + */ +export function browserSupportsWebauthn(): boolean { + return ( + window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function' + ); +} diff --git a/packages/browser/src/helpers/supportsWebauthn.test.ts b/packages/browser/src/helpers/supportsWebauthn.test.ts deleted file mode 100644 index e11d77b..0000000 --- a/packages/browser/src/helpers/supportsWebauthn.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import supportsWebauthn from './supportsWebauthn'; - -beforeEach(() => { - // @ts-ignore 2741 - window.PublicKeyCredential = jest.fn().mockReturnValue(() => {}); -}); - -test('should return true when browser supports WebAuthn', () => { - expect(supportsWebauthn()).toBe(true); -}); - -test('should return false when browser does not support WebAuthn', () => { - delete (window as any).PublicKeyCredential; - expect(supportsWebauthn()).toBe(false); -}); - -test('should return false when window is undefined', () => { - // Make window undefined as it is in node environments. - const windowSpy = jest.spyOn(global, 'window', 'get'); - windowSpy.mockImplementation(() => undefined); - - expect(window).toBe(undefined); - expect(supportsWebauthn()).toBe(false); - - // Restore original window value. - windowSpy.mockRestore(); -}); diff --git a/packages/browser/src/helpers/supportsWebauthn.ts b/packages/browser/src/helpers/supportsWebauthn.ts deleted file mode 100644 index b572080..0000000 --- a/packages/browser/src/helpers/supportsWebauthn.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Determine if the browser is capable of Webauthn - */ -export default function supportsWebauthn(): boolean { - return ( - window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function' - ); -} diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts index 0d132ba..49c6ebe 100644 --- a/packages/browser/src/index.test.ts +++ b/packages/browser/src/index.test.ts @@ -8,6 +8,6 @@ test('should export method `startAssertion`', () => { expect(index.startAssertion).toBeDefined(); }); -test('should export method `supportsWebauthn`', () => { - expect(index.supportsWebauthn).toBeDefined(); +test('should export method `browserSupportsWebauthn`', () => { + expect(index.browserSupportsWebauthn).toBeDefined(); }); diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 1b450d6..c342ddb 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -4,6 +4,6 @@ */ import startAttestation from './methods/startAttestation'; import startAssertion from './methods/startAssertion'; -import supportsWebauthn from './helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from './helpers/browserSupportsWebauthn'; -export { startAttestation, startAssertion, supportsWebauthn }; +export { startAttestation, startAssertion, browserSupportsWebauthn }; diff --git a/packages/browser/src/methods/startAssertion.test.ts b/packages/browser/src/methods/startAssertion.test.ts index f005a26..060d918 100644 --- a/packages/browser/src/methods/startAssertion.test.ts +++ b/packages/browser/src/methods/startAssertion.test.ts @@ -5,16 +5,16 @@ import { AuthenticationExtensionsClientOutputs, } from '@simplewebauthn/typescript-types'; -import supportsWebauthn from '../helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import startAssertion from './startAssertion'; -jest.mock('../helpers/supportsWebauthn'); +jest.mock('../helpers/browserSupportsWebauthn'); const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; -const mockSupportsWebauthn = supportsWebauthn as jest.Mock; +const mockSupportsWebauthn = browserSupportsWebauthn as jest.Mock; const mockAuthenticatorData = 'mockAuthenticatorData'; const mockClientDataJSON = 'mockClientDataJSON'; @@ -43,16 +43,14 @@ const goodOpts2UTF8: PublicKeyCredentialRequestOptionsJSON = { beforeEach(() => { // Stub out a response so the method won't throw - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ - response: {}, - getClientExtensionResults: () => ({}), - }); + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ + response: {}, + getClientExtensionResults: () => ({}), }); - }, - ); + }); + }); mockSupportsWebauthn.mockReturnValue(true); }); @@ -95,24 +93,22 @@ test('should convert allow allowCredential to undefined when empty', async () => }); test('should return base64url-encoded response values', async done => { - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ - 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'), - }, - getClientExtensionResults: () => ({}), - type: 'webauthn.get', - }); + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ + 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'), + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.get', }); - }, - ); + }); + }); const response = await startAssertion(goodOpts1); @@ -136,13 +132,11 @@ test("should throw error if WebAuthn isn't supported", async done => { }); test('should throw error if assertion is cancelled for some reason', async done => { - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve(null); + }); + }); await expect(startAssertion(goodOpts1)).rejects.toThrow('Assertion was not completed'); @@ -156,24 +150,7 @@ test('should handle UTF-8 challenges', async done => { 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, ]), ); @@ -219,13 +196,11 @@ test('should include extension results', async done => { }; // Mock extension return values from authenticator - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => extResults }); - }); - }, - ); + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ response: {}, getClientExtensionResults: () => extResults }); + }); + }); // Extensions aren't present in this object, but it doesn't matter since we're faking the response const response = await startAssertion(goodOpts1); diff --git a/packages/browser/src/methods/startAssertion.ts b/packages/browser/src/methods/startAssertion.ts index 786d55b..26fd755 100644 --- a/packages/browser/src/methods/startAssertion.ts +++ b/packages/browser/src/methods/startAssertion.ts @@ -7,7 +7,7 @@ import { import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; import bufferToUTF8String from '../helpers/bufferToUTF8String'; -import supportsWebauthn from '../helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; /** @@ -18,7 +18,7 @@ import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDes export default async function startAssertion( requestOptionsJSON: PublicKeyCredentialRequestOptionsJSON, ): Promise { - if (!supportsWebauthn()) { + if (!browserSupportsWebauthn()) { throw new Error('WebAuthn is not supported in this browser'); } diff --git a/packages/browser/src/methods/startAttestation.test.ts b/packages/browser/src/methods/startAttestation.test.ts index 244a4d2..a6a2beb 100644 --- a/packages/browser/src/methods/startAttestation.test.ts +++ b/packages/browser/src/methods/startAttestation.test.ts @@ -6,15 +6,15 @@ import { } from '@simplewebauthn/typescript-types'; import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; -import supportsWebauthn from '../helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import startAttestation from './startAttestation'; -jest.mock('../helpers/supportsWebauthn'); +jest.mock('../helpers/browserSupportsWebauthn'); const mockNavigatorCreate = window.navigator.credentials.create as jest.Mock; -const mockSupportsWebauthn = supportsWebauthn as jest.Mock; +const mockSupportsWebauthn = browserSupportsWebauthn as jest.Mock; const mockAttestationObject = 'mockAtte'; const mockClientDataJSON = 'mockClie'; @@ -49,13 +49,11 @@ const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { beforeEach(() => { // Stub out a response so the method won't throw - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => ({}) }); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ response: {}, getClientExtensionResults: () => ({}) }); + }); + }); mockSupportsWebauthn.mockReturnValue(true); }); @@ -85,22 +83,20 @@ test('should convert options before passing to navigator.credentials.create(...) }); test('should return base64url-encoded response values', async done => { - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ - id: 'foobar', - rawId: utf8StringToBuffer('foobar'), - response: { - attestationObject: Buffer.from(mockAttestationObject, 'ascii'), - clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), - }, - getClientExtensionResults: () => ({}), - type: 'webauthn.create', - }); + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ + id: 'foobar', + rawId: utf8StringToBuffer('foobar'), + response: { + attestationObject: Buffer.from(mockAttestationObject, 'ascii'), + clientDataJSON: Buffer.from(mockClientDataJSON, 'ascii'), + }, + getClientExtensionResults: () => ({}), + type: 'webauthn.create', }); - }, - ); + }); + }); const response = await startAttestation(goodOpts1); @@ -122,13 +118,11 @@ test("should throw error if WebAuthn isn't supported", async done => { }); test('should throw error if attestation is cancelled for some reason', async done => { - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve(null); + }); + }); await expect(startAttestation(goodOpts1)).rejects.toThrow('Attestation was not completed'); @@ -174,13 +168,11 @@ test('should include extension results', async done => { }; // Mock extension return values from authenticator - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => extResults }); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ response: {}, getClientExtensionResults: () => extResults }); + }); + }); // Extensions aren't present in this object, but it doesn't matter since we're faking the response const response = await startAttestation(goodOpts1); diff --git a/packages/browser/src/methods/startAttestation.ts b/packages/browser/src/methods/startAttestation.ts index 379e295..c762c6f 100644 --- a/packages/browser/src/methods/startAttestation.ts +++ b/packages/browser/src/methods/startAttestation.ts @@ -7,7 +7,7 @@ import { import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; -import supportsWebauthn from '../helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from '../helpers/browserSupportsWebauthn'; import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; /** @@ -18,7 +18,7 @@ import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDes export default async function startAttestation( creationOptionsJSON: PublicKeyCredentialCreationOptionsJSON, ): Promise { - if (!supportsWebauthn()) { + if (!browserSupportsWebauthn()) { throw new Error('WebAuthn is not supported in this browser'); } -- cgit v1.2.3 From 40c1380745bfae180a58e0ba967466ab7ffac937 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Mon, 23 Aug 2021 21:08:23 -0700 Subject: Add tests for new method --- .../platformAuthenticatorIsAvailable.test.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts (limited to 'packages/browser/src') diff --git a/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts new file mode 100644 index 0000000..ba9f233 --- /dev/null +++ b/packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts @@ -0,0 +1,26 @@ +import { platformAuthenticatorIsAvailable } from './platformAuthenticatorIsAvailable'; + +const mockIsUVPAA = jest.fn(); + +beforeEach(() => { + mockIsUVPAA.mockReset(); + + // @ts-ignore 2741 + window.PublicKeyCredential = { + isUserVerifyingPlatformAuthenticatorAvailable: mockIsUVPAA.mockResolvedValue(true), + }; +}); + +test('should return true when platform authenticator is available', async () => { + const isAvailable = await platformAuthenticatorIsAvailable(); + + expect(isAvailable).toEqual(true); +}); + +test('should return false when platform authenticator is unavailable', async () => { + mockIsUVPAA.mockResolvedValue(false); + + const isAvailable = await platformAuthenticatorIsAvailable(); + + expect(isAvailable).toEqual(false); +}); -- cgit v1.2.3 From fb79f6941a1be0a4c320bb91079e109ee01cd5ad Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Mon, 23 Aug 2021 21:08:38 -0700 Subject: Export new helper method --- packages/browser/src/index.test.ts | 4 ++++ packages/browser/src/index.ts | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) (limited to 'packages/browser/src') diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts index 49c6ebe..5e396ca 100644 --- a/packages/browser/src/index.test.ts +++ b/packages/browser/src/index.test.ts @@ -11,3 +11,7 @@ test('should export method `startAssertion`', () => { test('should export method `browserSupportsWebauthn`', () => { expect(index.browserSupportsWebauthn).toBeDefined(); }); + +test('should export method `platformAuthenticatorIsAvailable`', () => { + expect(index.browserSupportsWebauthn).toBeDefined(); +}); diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index c342ddb..94c9755 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -5,5 +5,11 @@ import startAttestation from './methods/startAttestation'; import startAssertion from './methods/startAssertion'; import { browserSupportsWebauthn } from './helpers/browserSupportsWebauthn'; +import { platformAuthenticatorIsAvailable } from './helpers/platformAuthenticatorIsAvailable'; -export { startAttestation, startAssertion, browserSupportsWebauthn }; +export { + startAttestation, + startAssertion, + browserSupportsWebauthn, + platformAuthenticatorIsAvailable, +}; -- cgit v1.2.3