diff options
author | Matthew Miller <matthew@millerti.me> | 2021-08-23 21:13:35 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-23 21:13:35 -0700 |
commit | 30ecc73b9856747337523f1e367b10d9d96a4a95 (patch) | |
tree | 793c1d8e592d58aacbb6cd8468bd692c6131b8a5 /packages/browser/src | |
parent | 9a849c3d7eb6c82195ddfcd20f1ad4796a7873ec (diff) | |
parent | fb79f6941a1be0a4c320bb91079e109ee01cd5ad (diff) |
Merge pull request #151 from MasterKale/feat/browser-supports-platform-authr
feat/browser-supports-platform-authr
Diffstat (limited to 'packages/browser/src')
-rw-r--r-- | packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts (renamed from packages/browser/src/helpers/__mocks__/supportsWebauthn.ts) | 2 | ||||
-rw-r--r-- | packages/browser/src/helpers/browserSupportsWebauthn.test.ts (renamed from packages/browser/src/helpers/supportsWebauthn.test.ts) | 8 | ||||
-rw-r--r-- | packages/browser/src/helpers/browserSupportsWebauthn.ts (renamed from packages/browser/src/helpers/supportsWebauthn.ts) | 2 | ||||
-rw-r--r-- | packages/browser/src/helpers/platformAuthenticatorIsAvailable.test.ts | 26 | ||||
-rw-r--r-- | packages/browser/src/helpers/platformAuthenticatorIsAvailable.ts | 9 | ||||
-rw-r--r-- | packages/browser/src/index.test.ts | 8 | ||||
-rw-r--r-- | packages/browser/src/index.ts | 10 | ||||
-rw-r--r-- | packages/browser/src/methods/startAssertion.test.ts | 97 | ||||
-rw-r--r-- | packages/browser/src/methods/startAssertion.ts | 4 | ||||
-rw-r--r-- | packages/browser/src/methods/startAttestation.test.ts | 70 | ||||
-rw-r--r-- | packages/browser/src/methods/startAttestation.ts | 4 |
11 files changed, 126 insertions, 114 deletions
diff --git a/packages/browser/src/helpers/__mocks__/supportsWebauthn.ts b/packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts index b6b47d4..20d5d88 100644 --- a/packages/browser/src/helpers/__mocks__/supportsWebauthn.ts +++ b/packages/browser/src/helpers/__mocks__/browserSupportsWebauthn.ts @@ -1,2 +1,2 @@ // We just need a simple mock so we can control whether this returns `true` or `false` -export default jest.fn(); +export const browserSupportsWebauthn = jest.fn(); diff --git a/packages/browser/src/helpers/supportsWebauthn.test.ts b/packages/browser/src/helpers/browserSupportsWebauthn.test.ts index e11d77b..5308315 100644 --- a/packages/browser/src/helpers/supportsWebauthn.test.ts +++ b/packages/browser/src/helpers/browserSupportsWebauthn.test.ts @@ -1,4 +1,4 @@ -import supportsWebauthn from './supportsWebauthn'; +import { browserSupportsWebauthn } from './browserSupportsWebauthn'; beforeEach(() => { // @ts-ignore 2741 @@ -6,12 +6,12 @@ beforeEach(() => { }); test('should return true when browser supports WebAuthn', () => { - expect(supportsWebauthn()).toBe(true); + expect(browserSupportsWebauthn()).toBe(true); }); test('should return false when browser does not support WebAuthn', () => { delete (window as any).PublicKeyCredential; - expect(supportsWebauthn()).toBe(false); + expect(browserSupportsWebauthn()).toBe(false); }); test('should return false when window is undefined', () => { @@ -20,7 +20,7 @@ test('should return false when window is undefined', () => { windowSpy.mockImplementation(() => undefined); expect(window).toBe(undefined); - expect(supportsWebauthn()).toBe(false); + expect(browserSupportsWebauthn()).toBe(false); // Restore original window value. windowSpy.mockRestore(); diff --git a/packages/browser/src/helpers/supportsWebauthn.ts b/packages/browser/src/helpers/browserSupportsWebauthn.ts index b572080..030256f 100644 --- a/packages/browser/src/helpers/supportsWebauthn.ts +++ b/packages/browser/src/helpers/browserSupportsWebauthn.ts @@ -1,7 +1,7 @@ /** * Determine if the browser is capable of Webauthn */ -export default function supportsWebauthn(): boolean { +export function browserSupportsWebauthn(): boolean { return ( window?.PublicKeyCredential !== undefined && typeof window.PublicKeyCredential === 'function' ); 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); +}); 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<boolean> { + return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); +} diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts index 0d132ba..5e396ca 100644 --- a/packages/browser/src/index.test.ts +++ b/packages/browser/src/index.test.ts @@ -8,6 +8,10 @@ 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(); +}); + +test('should export method `platformAuthenticatorIsAvailable`', () => { + expect(index.browserSupportsWebauthn).toBeDefined(); }); diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 1b450d6..94c9755 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -4,6 +4,12 @@ */ import startAttestation from './methods/startAttestation'; import startAssertion from './methods/startAssertion'; -import supportsWebauthn from './helpers/supportsWebauthn'; +import { browserSupportsWebauthn } from './helpers/browserSupportsWebauthn'; +import { platformAuthenticatorIsAvailable } from './helpers/platformAuthenticatorIsAvailable'; -export { startAttestation, startAssertion, supportsWebauthn }; +export { + startAttestation, + startAssertion, + browserSupportsWebauthn, + platformAuthenticatorIsAvailable, +}; 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<any> => { - return new Promise(resolve => { - resolve({ - response: {}, - getClientExtensionResults: () => ({}), - }); + mockNavigatorGet.mockImplementation((): Promise<any> => { + 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<AssertionCredential> => { - 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<AssertionCredential> => { + 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<null> => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); + mockNavigatorGet.mockImplementation((): Promise<null> => { + 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<any> => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => extResults }); - }); - }, - ); + mockNavigatorGet.mockImplementation((): Promise<any> => { + 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<AssertionCredentialJSON> { - 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<any> => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => ({}) }); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise<any> => { + 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<AttestationCredential> => { - 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<AttestationCredential> => { + 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<null> => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise<null> => { + 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<any> => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => extResults }); - }); - }, - ); + mockNavigatorCreate.mockImplementation((): Promise<any> => { + 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<AttestationCredentialJSON> { - if (!supportsWebauthn()) { + if (!browserSupportsWebauthn()) { throw new Error('WebAuthn is not supported in this browser'); } |