From df30228f4ebcccba03ddcfb3fc2dbd7102ae020d Mon Sep 17 00:00:00 2001 From: Jarrett Helton Date: Sat, 21 Aug 2021 16:14:07 -0400 Subject: starting attestation -> registration rename --- packages/browser/src/index.ts | 4 +- .../browser/src/methods/startAttestation.test.ts | 199 --------------------- packages/browser/src/methods/startAttestation.ts | 65 ------- .../browser/src/methods/startRegistration.test.ts | 191 ++++++++++++++++++++ packages/browser/src/methods/startRegistration.ts | 65 +++++++ 5 files changed, 258 insertions(+), 266 deletions(-) delete mode 100644 packages/browser/src/methods/startAttestation.test.ts delete mode 100644 packages/browser/src/methods/startAttestation.ts create mode 100644 packages/browser/src/methods/startRegistration.test.ts create mode 100644 packages/browser/src/methods/startRegistration.ts (limited to 'packages/browser/src') diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 1b450d6..4f42044 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -2,8 +2,8 @@ * @packageDocumentation * @module @simplewebauthn/browser */ -import startAttestation from './methods/startAttestation'; +import startRegistration from './methods/startRegistration'; import startAssertion from './methods/startAssertion'; import supportsWebauthn from './helpers/supportsWebauthn'; -export { startAttestation, startAssertion, supportsWebauthn }; +export { startRegistration, startAssertion, supportsWebauthn }; diff --git a/packages/browser/src/methods/startAttestation.test.ts b/packages/browser/src/methods/startAttestation.test.ts deleted file mode 100644 index 244a4d2..0000000 --- a/packages/browser/src/methods/startAttestation.test.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - AttestationCredential, - AuthenticationExtensionsClientInputs, - AuthenticationExtensionsClientOutputs, - PublicKeyCredentialCreationOptionsJSON, -} from '@simplewebauthn/typescript-types'; - -import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; -import supportsWebauthn from '../helpers/supportsWebauthn'; -import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; - -import startAttestation from './startAttestation'; - -jest.mock('../helpers/supportsWebauthn'); - -const mockNavigatorCreate = window.navigator.credentials.create as jest.Mock; -const mockSupportsWebauthn = supportsWebauthn as jest.Mock; - -const mockAttestationObject = 'mockAtte'; -const mockClientDataJSON = 'mockClie'; - -const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), - attestation: 'direct', - pubKeyCredParams: [ - { - alg: -7, - type: 'public-key', - }, - ], - rp: { - id: '1234', - name: 'simplewebauthn', - }, - user: { - id: '5678', - displayName: 'username', - name: 'username', - }, - timeout: 1, - excludeCredentials: [ - { - id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', - type: 'public-key', - transports: ['internal'], - }, - ], -}; - -beforeEach(() => { - // Stub out a response so the method won't throw - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ response: {}, getClientExtensionResults: () => ({}) }); - }); - }, - ); - - mockSupportsWebauthn.mockReturnValue(true); -}); - -afterEach(() => { - mockNavigatorCreate.mockReset(); - mockSupportsWebauthn.mockReset(); -}); - -test('should convert options before passing to navigator.credentials.create(...)', async done => { - await startAttestation(goodOpts1); - - const argsPublicKey = mockNavigatorCreate.mock.calls[0][0].publicKey; - const credId = argsPublicKey.excludeCredentials[0].id; - - // Make sure challenge and user.id are converted to Buffers - expect(new Uint8Array(argsPublicKey.challenge)).toEqual(new Uint8Array([102, 105, 122, 122])); - expect(new Uint8Array(argsPublicKey.user.id)).toEqual(new Uint8Array([53, 54, 55, 56])); - - // Confirm construction of excludeCredentials array - expect(credId instanceof ArrayBuffer).toEqual(true); - expect(credId.byteLength).toEqual(64); - expect(argsPublicKey.excludeCredentials[0].type).toEqual('public-key'); - expect(argsPublicKey.excludeCredentials[0].transports).toEqual(['internal']); - - done(); -}); - -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', - }); - }); - }, - ); - - const response = await startAttestation(goodOpts1); - - expect(response.rawId).toEqual('Zm9vYmFy'); - expect(response.response.attestationObject).toEqual('bW9ja0F0dGU'); - expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWU'); - - done(); -}); - -test("should throw error if WebAuthn isn't supported", async done => { - mockSupportsWebauthn.mockReturnValue(false); - - await expect(startAttestation(goodOpts1)).rejects.toThrow( - 'WebAuthn is not supported in this browser', - ); - - done(); -}); - -test('should throw error if attestation is cancelled for some reason', async done => { - mockNavigatorCreate.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); - - await expect(startAttestation(goodOpts1)).rejects.toThrow('Attestation was not completed'); - - done(); -}); - -test('should send extensions to authenticator if present in options', async done => { - const extensions: AuthenticationExtensionsClientInputs = { - credProps: true, - appid: 'appidHere', - uvm: true, - appidExclude: 'appidExcludeHere', - }; - const optsWithExts: PublicKeyCredentialCreationOptionsJSON = { - ...goodOpts1, - extensions, - }; - await startAttestation(optsWithExts); - - const argsExtensions = mockNavigatorCreate.mock.calls[0][0].publicKey.extensions; - - expect(argsExtensions).toEqual(extensions); - - done(); -}); - -test('should not set any extensions if not present in options', async done => { - await startAttestation(goodOpts1); - - const argsExtensions = mockNavigatorCreate.mock.calls[0][0].publicKey.extensions; - - expect(argsExtensions).toEqual(undefined); - - done(); -}); - -test('should include extension results', async done => { - const extResults: AuthenticationExtensionsClientOutputs = { - appid: true, - credProps: { - rk: true, - }, - }; - - // Mock extension return values from authenticator - 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); - - expect(response.clientExtensionResults).toEqual(extResults); - - done(); -}); - -test('should include extension results when no extensions specified', async done => { - const response = await startAttestation(goodOpts1); - - expect(response.clientExtensionResults).toEqual({}); - - done(); -}); diff --git a/packages/browser/src/methods/startAttestation.ts b/packages/browser/src/methods/startAttestation.ts deleted file mode 100644 index 379e295..0000000 --- a/packages/browser/src/methods/startAttestation.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - PublicKeyCredentialCreationOptionsJSON, - AttestationCredential, - AttestationCredentialJSON, -} from '@simplewebauthn/typescript-types'; - -import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; -import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; -import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; -import supportsWebauthn from '../helpers/supportsWebauthn'; -import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; - -/** - * Begin authenticator "registration" via WebAuthn attestation - * - * @param creationOptionsJSON Output from @simplewebauthn/server's generateAttestationOptions(...) - */ -export default async function startAttestation( - creationOptionsJSON: PublicKeyCredentialCreationOptionsJSON, -): Promise { - if (!supportsWebauthn()) { - throw new Error('WebAuthn is not supported in this browser'); - } - - // We need to convert some values to Uint8Arrays before passing the credentials to the navigator - const publicKey: PublicKeyCredentialCreationOptions = { - ...creationOptionsJSON, - challenge: base64URLStringToBuffer(creationOptionsJSON.challenge), - user: { - ...creationOptionsJSON.user, - id: utf8StringToBuffer(creationOptionsJSON.user.id), - }, - excludeCredentials: creationOptionsJSON.excludeCredentials.map(toPublicKeyCredentialDescriptor), - }; - - // Wait for the user to complete attestation - const credential = (await navigator.credentials.create({ publicKey })) as AttestationCredential; - - if (!credential) { - throw new Error('Attestation was not completed'); - } - - const { id, rawId, response, type } = credential; - - // Convert values to base64 to make it easier to send back to the server - const credentialJSON: AttestationCredentialJSON = { - id, - rawId: bufferToBase64URLString(rawId), - response: { - attestationObject: bufferToBase64URLString(response.attestationObject), - clientDataJSON: bufferToBase64URLString(response.clientDataJSON), - }, - type, - clientExtensionResults: credential.getClientExtensionResults(), - }; - - /** - * Include the authenticator's transports if the browser supports querying for them - */ - if (typeof response.getTransports === 'function') { - credentialJSON.transports = response.getTransports(); - } - - return credentialJSON; -} diff --git a/packages/browser/src/methods/startRegistration.test.ts b/packages/browser/src/methods/startRegistration.test.ts new file mode 100644 index 0000000..3567b02 --- /dev/null +++ b/packages/browser/src/methods/startRegistration.test.ts @@ -0,0 +1,191 @@ +import { + RegistrationCredential, + AuthenticationExtensionsClientInputs, + AuthenticationExtensionsClientOutputs, + PublicKeyCredentialCreationOptionsJSON, +} from '@simplewebauthn/typescript-types'; + +import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; +import supportsWebauthn from '../helpers/supportsWebauthn'; +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; + +import startRegistration from './startRegistration'; + +jest.mock('../helpers/supportsWebauthn'); + +const mockNavigatorCreate = window.navigator.credentials.create as jest.Mock; +const mockSupportsWebauthn = supportsWebauthn as jest.Mock; + +const mockAttestationObject = 'mockAtte'; +const mockClientDataJSON = 'mockClie'; + +const goodOpts1: PublicKeyCredentialCreationOptionsJSON = { + challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + attestation: 'direct', + pubKeyCredParams: [ + { + alg: -7, + type: 'public-key', + }, + ], + rp: { + id: '1234', + name: 'simplewebauthn', + }, + user: { + id: '5678', + displayName: 'username', + name: 'username', + }, + timeout: 1, + excludeCredentials: [ + { + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', + type: 'public-key', + transports: ['internal'], + }, + ], +}; + +beforeEach(() => { + // Stub out a response so the method won't throw + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ response: {}, getClientExtensionResults: () => ({}) }); + }); + }); + + mockSupportsWebauthn.mockReturnValue(true); +}); + +afterEach(() => { + mockNavigatorCreate.mockReset(); + mockSupportsWebauthn.mockReset(); +}); + +test('should convert options before passing to navigator.credentials.create(...)', async done => { + await startRegistration(goodOpts1); + + const argsPublicKey = mockNavigatorCreate.mock.calls[0][0].publicKey; + const credId = argsPublicKey.excludeCredentials[0].id; + + // Make sure challenge and user.id are converted to Buffers + expect(new Uint8Array(argsPublicKey.challenge)).toEqual(new Uint8Array([102, 105, 122, 122])); + expect(new Uint8Array(argsPublicKey.user.id)).toEqual(new Uint8Array([53, 54, 55, 56])); + + // Confirm construction of excludeCredentials array + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); + expect(argsPublicKey.excludeCredentials[0].type).toEqual('public-key'); + expect(argsPublicKey.excludeCredentials[0].transports).toEqual(['internal']); + + done(); +}); + +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', + }); + }); + }); + + const response = await startRegistration(goodOpts1); + + expect(response.rawId).toEqual('Zm9vYmFy'); + expect(response.response.attestationObject).toEqual('bW9ja0F0dGU'); + expect(response.response.clientDataJSON).toEqual('bW9ja0NsaWU'); + + done(); +}); + +test("should throw error if WebAuthn isn't supported", async done => { + mockSupportsWebauthn.mockReturnValue(false); + + await expect(startRegistration(goodOpts1)).rejects.toThrow( + 'WebAuthn is not supported in this browser', + ); + + done(); +}); + +test('should throw error if attestation is cancelled for some reason', async done => { + mockNavigatorCreate.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve(null); + }); + }); + + await expect(startRegistration(goodOpts1)).rejects.toThrow('Attestation was not completed'); + + done(); +}); + +test('should send extensions to authenticator if present in options', async done => { + const extensions: AuthenticationExtensionsClientInputs = { + credProps: true, + appid: 'appidHere', + uvm: true, + appidExclude: 'appidExcludeHere', + }; + const optsWithExts: PublicKeyCredentialCreationOptionsJSON = { + ...goodOpts1, + extensions, + }; + await startRegistration(optsWithExts); + + const argsExtensions = mockNavigatorCreate.mock.calls[0][0].publicKey.extensions; + + expect(argsExtensions).toEqual(extensions); + + done(); +}); + +test('should not set any extensions if not present in options', async done => { + await startRegistration(goodOpts1); + + const argsExtensions = mockNavigatorCreate.mock.calls[0][0].publicKey.extensions; + + expect(argsExtensions).toEqual(undefined); + + done(); +}); + +test('should include extension results', async done => { + const extResults: AuthenticationExtensionsClientOutputs = { + appid: true, + credProps: { + rk: true, + }, + }; + + // Mock extension return values from authenticator + 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 startRegistration(goodOpts1); + + expect(response.clientExtensionResults).toEqual(extResults); + + done(); +}); + +test('should include extension results when no extensions specified', async done => { + const response = await startRegistration(goodOpts1); + + expect(response.clientExtensionResults).toEqual({}); + + done(); +}); diff --git a/packages/browser/src/methods/startRegistration.ts b/packages/browser/src/methods/startRegistration.ts new file mode 100644 index 0000000..be97a1a --- /dev/null +++ b/packages/browser/src/methods/startRegistration.ts @@ -0,0 +1,65 @@ +import { + PublicKeyCredentialCreationOptionsJSON, + RegistrationCredential, + RegistrationCredentialJSON, +} from '@simplewebauthn/typescript-types'; + +import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; +import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; +import supportsWebauthn from '../helpers/supportsWebauthn'; +import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; + +/** + * Begin authenticator "registration" via WebAuthn attestation + * + * @param creationOptionsJSON Output from @simplewebauthn/server's generateRegistrationOptions(...) + */ +export default async function startRegistration( + creationOptionsJSON: PublicKeyCredentialCreationOptionsJSON, +): Promise { + if (!supportsWebauthn()) { + throw new Error('WebAuthn is not supported in this browser'); + } + + // We need to convert some values to Uint8Arrays before passing the credentials to the navigator + const publicKey: PublicKeyCredentialCreationOptions = { + ...creationOptionsJSON, + challenge: base64URLStringToBuffer(creationOptionsJSON.challenge), + user: { + ...creationOptionsJSON.user, + id: utf8StringToBuffer(creationOptionsJSON.user.id), + }, + excludeCredentials: creationOptionsJSON.excludeCredentials.map(toPublicKeyCredentialDescriptor), + }; + + // Wait for the user to complete attestation + const credential = (await navigator.credentials.create({ publicKey })) as RegistrationCredential; + + if (!credential) { + throw new Error('Attestation was not completed'); + } + + const { id, rawId, response, type } = credential; + + // Convert values to base64 to make it easier to send back to the server + const credentialJSON: RegistrationCredentialJSON = { + id, + rawId: bufferToBase64URLString(rawId), + response: { + attestationObject: bufferToBase64URLString(response.attestationObject), + clientDataJSON: bufferToBase64URLString(response.clientDataJSON), + }, + type, + clientExtensionResults: credential.getClientExtensionResults(), + }; + + /** + * Include the authenticator's transports if the browser supports querying for them + */ + if (typeof response.getTransports === 'function') { + credentialJSON.transports = response.getTransports(); + } + + return credentialJSON; +} -- cgit v1.2.3 From 100ea77af46317d815b7bf4f695144187414d5b8 Mon Sep 17 00:00:00 2001 From: Jarrett Helton Date: Sat, 21 Aug 2021 17:10:21 -0400 Subject: renaming assertion -> authentication --- packages/browser/src/index.test.ts | 8 +- packages/browser/src/index.ts | 4 +- .../browser/src/methods/startAssertion.test.ts | 244 --------------------- packages/browser/src/methods/startAssertion.ts | 66 ------ .../src/methods/startAuthentication.test.ts | 219 ++++++++++++++++++ .../browser/src/methods/startAuthentication.ts | 66 ++++++ .../src/assertion/verifyAssertionResponse.test.ts | 9 +- .../src/assertion/verifyAssertionResponse.ts | 4 +- packages/typescript-types/src/index.ts | 10 +- 9 files changed, 304 insertions(+), 326 deletions(-) delete mode 100644 packages/browser/src/methods/startAssertion.test.ts delete mode 100644 packages/browser/src/methods/startAssertion.ts create mode 100644 packages/browser/src/methods/startAuthentication.test.ts create mode 100644 packages/browser/src/methods/startAuthentication.ts (limited to 'packages/browser/src') diff --git a/packages/browser/src/index.test.ts b/packages/browser/src/index.test.ts index 0d132ba..ffd3b2b 100644 --- a/packages/browser/src/index.test.ts +++ b/packages/browser/src/index.test.ts @@ -1,11 +1,11 @@ import * as index from './index'; -test('should export method `startAttestation`', () => { - expect(index.startAttestation).toBeDefined(); +test('should export method `startRegistration`', () => { + expect(index.startRegistration).toBeDefined(); }); -test('should export method `startAssertion`', () => { - expect(index.startAssertion).toBeDefined(); +test('should export method `startAuthentication`', () => { + expect(index.startAuthentication).toBeDefined(); }); test('should export method `supportsWebauthn`', () => { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 4f42044..520af9a 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -3,7 +3,7 @@ * @module @simplewebauthn/browser */ import startRegistration from './methods/startRegistration'; -import startAssertion from './methods/startAssertion'; +import startAuthentication from './methods/startAuthentication'; import supportsWebauthn from './helpers/supportsWebauthn'; -export { startRegistration, startAssertion, supportsWebauthn }; +export { startRegistration, startAuthentication, supportsWebauthn }; diff --git a/packages/browser/src/methods/startAssertion.test.ts b/packages/browser/src/methods/startAssertion.test.ts deleted file mode 100644 index f005a26..0000000 --- a/packages/browser/src/methods/startAssertion.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -import { - AssertionCredential, - PublicKeyCredentialRequestOptionsJSON, - AuthenticationExtensionsClientInputs, - AuthenticationExtensionsClientOutputs, -} from '@simplewebauthn/typescript-types'; - -import supportsWebauthn from '../helpers/supportsWebauthn'; -import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; -import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; - -import startAssertion from './startAssertion'; - -jest.mock('../helpers/supportsWebauthn'); - -const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; -const mockSupportsWebauthn = supportsWebauthn as jest.Mock; - -const mockAuthenticatorData = 'mockAuthenticatorData'; -const mockClientDataJSON = 'mockClientDataJSON'; -const mockSignature = 'mockSignature'; -const mockUserHandle = 'mockUserHandle'; - -// With ASCII challenge -const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), - allowCredentials: [ - { - id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', - type: 'public-key', - transports: ['nfc'], - }, - ], - timeout: 1, -}; - -// With UTF-8 challenge -const goodOpts2UTF8: PublicKeyCredentialRequestOptionsJSON = { - challenge: bufferToBase64URLString(utf8StringToBuffer('やれやれだぜ')), - allowCredentials: [], - timeout: 1, -}; - -beforeEach(() => { - // Stub out a response so the method won't throw - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve({ - response: {}, - getClientExtensionResults: () => ({}), - }); - }); - }, - ); - - mockSupportsWebauthn.mockReturnValue(true); -}); - -afterEach(() => { - mockNavigatorGet.mockReset(); - mockSupportsWebauthn.mockReset(); -}); - -test('should convert options before passing to navigator.credentials.get(...)', async done => { - await startAssertion(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])); - // Make sure the credential ID is an ArrayBuffer with a length of 64 - expect(credId instanceof ArrayBuffer).toEqual(true); - expect(credId.byteLength).toEqual(64); - - done(); -}); - -test('should support optional allowCredential', async () => { - await startAssertion({ - 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 () => { - await startAssertion({ - challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), - timeout: 1, - allowCredentials: [], - }); - expect(mockNavigatorGet.mock.calls[0][0].allowCredentials).toEqual(undefined); -}); - -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', - }); - }); - }, - ); - - const response = await startAssertion(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'); - - done(); -}); - -test("should throw error if WebAuthn isn't supported", async done => { - mockSupportsWebauthn.mockReturnValue(false); - - await expect(startAssertion(goodOpts1)).rejects.toThrow( - 'WebAuthn is not supported in this browser', - ); - - done(); -}); - -test('should throw error if assertion is cancelled for some reason', async done => { - mockNavigatorGet.mockImplementation( - (): Promise => { - return new Promise(resolve => { - resolve(null); - }); - }, - ); - - await expect(startAssertion(goodOpts1)).rejects.toThrow('Assertion was not completed'); - - done(); -}); - -test('should handle UTF-8 challenges', async done => { - await startAssertion(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, - ]), - ); - - done(); -}); - -test('should send extensions to authenticator if present in options', async done => { - const extensions: AuthenticationExtensionsClientInputs = { - credProps: true, - appid: 'appidHere', - uvm: true, - appidExclude: 'appidExcludeHere', - }; - const optsWithExts: PublicKeyCredentialRequestOptionsJSON = { - ...goodOpts1, - extensions, - }; - await startAssertion(optsWithExts); - - const argsExtensions = mockNavigatorGet.mock.calls[0][0].publicKey.extensions; - - expect(argsExtensions).toEqual(extensions); - - done(); -}); - -test('should not set any extensions if not present in options', async done => { - await startAssertion(goodOpts1); - - const argsExtensions = mockNavigatorGet.mock.calls[0][0].publicKey.extensions; - - expect(argsExtensions).toEqual(undefined); - - done(); -}); - -test('should include extension results', async done => { - const extResults: AuthenticationExtensionsClientOutputs = { - appid: true, - credProps: { - rk: true, - }, - }; - - // Mock extension return values from authenticator - 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); - - expect(response.clientExtensionResults).toEqual(extResults); - - done(); -}); - -test('should include extension results when no extensions specified', async done => { - const response = await startAssertion(goodOpts1); - - expect(response.clientExtensionResults).toEqual({}); - - done(); -}); diff --git a/packages/browser/src/methods/startAssertion.ts b/packages/browser/src/methods/startAssertion.ts deleted file mode 100644 index 786d55b..0000000 --- a/packages/browser/src/methods/startAssertion.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { - PublicKeyCredentialRequestOptionsJSON, - AssertionCredential, - AssertionCredentialJSON, -} from '@simplewebauthn/typescript-types'; - -import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; -import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; -import bufferToUTF8String from '../helpers/bufferToUTF8String'; -import supportsWebauthn from '../helpers/supportsWebauthn'; -import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; - -/** - * Begin authenticator "login" via WebAuthn assertion - * - * @param requestOptionsJSON Output from @simplewebauthn/server's generateAssertionOptions(...) - */ -export default async function startAssertion( - requestOptionsJSON: PublicKeyCredentialRequestOptionsJSON, -): Promise { - if (!supportsWebauthn()) { - throw new Error('WebAuthn is not supported in this browser'); - } - - // We need to avoid passing empty array to avoid blocking retrieval - // of public key - let allowCredentials; - if (requestOptionsJSON.allowCredentials?.length !== 0) { - allowCredentials = requestOptionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor); - } - - // We need to convert some values to Uint8Arrays before passing the credentials to the navigator - const publicKey: PublicKeyCredentialRequestOptions = { - ...requestOptionsJSON, - challenge: base64URLStringToBuffer(requestOptionsJSON.challenge), - allowCredentials, - }; - - // Wait for the user to complete assertion - const credential = (await navigator.credentials.get({ publicKey })) as AssertionCredential; - - if (!credential) { - throw new Error('Assertion was not completed'); - } - - const { id, rawId, response, type } = credential; - - let userHandle = undefined; - if (response.userHandle) { - userHandle = bufferToUTF8String(response.userHandle); - } - - // Convert values to base64 to make it easier to send back to the server - return { - id, - rawId: bufferToBase64URLString(rawId), - response: { - authenticatorData: bufferToBase64URLString(response.authenticatorData), - clientDataJSON: bufferToBase64URLString(response.clientDataJSON), - signature: bufferToBase64URLString(response.signature), - userHandle, - }, - type, - clientExtensionResults: credential.getClientExtensionResults(), - }; -} diff --git a/packages/browser/src/methods/startAuthentication.test.ts b/packages/browser/src/methods/startAuthentication.test.ts new file mode 100644 index 0000000..96b140c --- /dev/null +++ b/packages/browser/src/methods/startAuthentication.test.ts @@ -0,0 +1,219 @@ +import { + AuthenticationCredential, + PublicKeyCredentialRequestOptionsJSON, + AuthenticationExtensionsClientInputs, + AuthenticationExtensionsClientOutputs, +} from '@simplewebauthn/typescript-types'; + +import supportsWebauthn from '../helpers/supportsWebauthn'; +import utf8StringToBuffer from '../helpers/utf8StringToBuffer'; +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; + +import startAuthentication from './startAuthentication'; + +jest.mock('../helpers/supportsWebauthn'); + +const mockNavigatorGet = window.navigator.credentials.get as jest.Mock; +const mockSupportsWebauthn = supportsWebauthn as jest.Mock; + +const mockAuthenticatorData = 'mockAuthenticatorData'; +const mockClientDataJSON = 'mockClientDataJSON'; +const mockSignature = 'mockSignature'; +const mockUserHandle = 'mockUserHandle'; + +// With ASCII challenge +const goodOpts1: PublicKeyCredentialRequestOptionsJSON = { + challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + allowCredentials: [ + { + id: 'C0VGlvYFratUdAV1iCw-ULpUW8E-exHPXQChBfyVeJZCMfjMFcwDmOFgoMUz39LoMtCJUBW8WPlLkGT6q8qTCg', + type: 'public-key', + transports: ['nfc'], + }, + ], + timeout: 1, +}; + +// With UTF-8 challenge +const goodOpts2UTF8: PublicKeyCredentialRequestOptionsJSON = { + challenge: bufferToBase64URLString(utf8StringToBuffer('やれやれだぜ')), + allowCredentials: [], + timeout: 1, +}; + +beforeEach(() => { + // Stub out a response so the method won't throw + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve({ + response: {}, + getClientExtensionResults: () => ({}), + }); + }); + }); + + mockSupportsWebauthn.mockReturnValue(true); +}); + +afterEach(() => { + mockNavigatorGet.mockReset(); + mockSupportsWebauthn.mockReset(); +}); + +test('should convert options before passing to navigator.credentials.get(...)', async done => { + 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])); + // Make sure the credential ID is an ArrayBuffer with a length of 64 + expect(credId instanceof ArrayBuffer).toEqual(true); + expect(credId.byteLength).toEqual(64); + + done(); +}); + +test('should support optional allowCredential', async () => { + await startAuthentication({ + 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 () => { + await startAuthentication({ + challenge: bufferToBase64URLString(utf8StringToBuffer('fizz')), + timeout: 1, + allowCredentials: [], + }); + expect(mockNavigatorGet.mock.calls[0][0].allowCredentials).toEqual(undefined); +}); + +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', + }); + }); + }); + + 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'); + + done(); +}); + +test("should throw error if WebAuthn isn't supported", async done => { + mockSupportsWebauthn.mockReturnValue(false); + + await expect(startAuthentication(goodOpts1)).rejects.toThrow( + 'WebAuthn is not supported in this browser', + ); + + done(); +}); + +test('should throw error if assertion is cancelled for some reason', async done => { + mockNavigatorGet.mockImplementation((): Promise => { + return new Promise(resolve => { + resolve(null); + }); + }); + + await expect(startAuthentication(goodOpts1)).rejects.toThrow('Assertion was not completed'); + + done(); +}); + +test('should handle UTF-8 challenges', async done => { + 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, + ]), + ); + + done(); +}); + +test('should send extensions to authenticator if present in options', async done => { + const extensions: AuthenticationExtensionsClientInputs = { + credProps: true, + appid: 'appidHere', + uvm: true, + appidExclude: 'appidExcludeHere', + }; + const optsWithExts: PublicKeyCredentialRequestOptionsJSON = { + ...goodOpts1, + extensions, + }; + await startAuthentication(optsWithExts); + + const argsExtensions = mockNavigatorGet.mock.calls[0][0].publicKey.extensions; + + expect(argsExtensions).toEqual(extensions); + + done(); +}); + +test('should not set any extensions if not present in options', async done => { + await startAuthentication(goodOpts1); + + const argsExtensions = mockNavigatorGet.mock.calls[0][0].publicKey.extensions; + + expect(argsExtensions).toEqual(undefined); + + done(); +}); + +test('should include extension results', async done => { + const extResults: AuthenticationExtensionsClientOutputs = { + appid: true, + credProps: { + rk: true, + }, + }; + + // Mock extension return values from authenticator + 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 startAuthentication(goodOpts1); + + expect(response.clientExtensionResults).toEqual(extResults); + + done(); +}); + +test('should include extension results when no extensions specified', async done => { + const response = await startAuthentication(goodOpts1); + + expect(response.clientExtensionResults).toEqual({}); + + done(); +}); diff --git a/packages/browser/src/methods/startAuthentication.ts b/packages/browser/src/methods/startAuthentication.ts new file mode 100644 index 0000000..1764ff1 --- /dev/null +++ b/packages/browser/src/methods/startAuthentication.ts @@ -0,0 +1,66 @@ +import { + PublicKeyCredentialRequestOptionsJSON, + AuthenticationCredential, + AuthenticationCredentialJSON, +} from '@simplewebauthn/typescript-types'; + +import bufferToBase64URLString from '../helpers/bufferToBase64URLString'; +import base64URLStringToBuffer from '../helpers/base64URLStringToBuffer'; +import bufferToUTF8String from '../helpers/bufferToUTF8String'; +import supportsWebauthn from '../helpers/supportsWebauthn'; +import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDescriptor'; + +/** + * Begin authenticator "login" via WebAuthn assertion + * + * @param requestOptionsJSON Output from @simplewebauthn/server's generateAssertionOptions(...) + */ +export default async function startAssertion( + requestOptionsJSON: PublicKeyCredentialRequestOptionsJSON, +): Promise { + if (!supportsWebauthn()) { + throw new Error('WebAuthn is not supported in this browser'); + } + + // We need to avoid passing empty array to avoid blocking retrieval + // of public key + let allowCredentials; + if (requestOptionsJSON.allowCredentials?.length !== 0) { + allowCredentials = requestOptionsJSON.allowCredentials?.map(toPublicKeyCredentialDescriptor); + } + + // We need to convert some values to Uint8Arrays before passing the credentials to the navigator + const publicKey: PublicKeyCredentialRequestOptions = { + ...requestOptionsJSON, + challenge: base64URLStringToBuffer(requestOptionsJSON.challenge), + allowCredentials, + }; + + // Wait for the user to complete assertion + const credential = (await navigator.credentials.get({ publicKey })) as AuthenticationCredential; + + if (!credential) { + throw new Error('Assertion was not completed'); + } + + const { id, rawId, response, type } = credential; + + let userHandle = undefined; + if (response.userHandle) { + userHandle = bufferToUTF8String(response.userHandle); + } + + // Convert values to base64 to make it easier to send back to the server + return { + id, + rawId: bufferToBase64URLString(rawId), + response: { + authenticatorData: bufferToBase64URLString(response.authenticatorData), + clientDataJSON: bufferToBase64URLString(response.clientDataJSON), + signature: bufferToBase64URLString(response.signature), + userHandle, + }, + type, + clientExtensionResults: credential.getClientExtensionResults(), + }; +} diff --git a/packages/server/src/assertion/verifyAssertionResponse.test.ts b/packages/server/src/assertion/verifyAssertionResponse.test.ts index 705f3cb..b1eeebb 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.test.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.test.ts @@ -4,7 +4,10 @@ import verifyAssertionResponse from './verifyAssertionResponse'; import * as decodeClientDataJSON from '../helpers/decodeClientDataJSON'; import * as parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import toHash from '../helpers/toHash'; -import { AuthenticatorDevice, AssertionCredentialJSON } from '@simplewebauthn/typescript-types'; +import { + AuthenticatorDevice, + AuthenticationCredentialJSON, +} from '@simplewebauthn/typescript-types'; let mockDecodeClientData: jest.SpyInstance; let mockParseAuthData: jest.SpyInstance; @@ -261,7 +264,7 @@ test('should throw an error if RP ID not in list of possible RP IDs', async () = * Assertion examples below */ -const assertionResponse: AssertionCredentialJSON = { +const assertionResponse: AuthenticationCredentialJSON = { id: 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew', rawId: 'KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew', response: { @@ -293,7 +296,7 @@ const authenticator: AuthenticatorDevice = { /** * Represented a device that's being used on the website for the first time */ -const assertionFirstTimeUsedResponse: AssertionCredentialJSON = { +const assertionFirstTimeUsedResponse: AuthenticationCredentialJSON = { id: 'wSisR0_4hlzw3Y1tj4uNwwifIhRa-ZxWJwWbnfror0pVK9qPdBPO5pW3gasPqn6wXHb0LNhXB_IrA1nFoSQJ9A', rawId: 'wSisR0_4hlzw3Y1tj4uNwwifIhRa-ZxWJwWbnfror0pVK9qPdBPO5pW3gasPqn6wXHb0LNhXB_IrA1nFoSQJ9A', response: { diff --git a/packages/server/src/assertion/verifyAssertionResponse.ts b/packages/server/src/assertion/verifyAssertionResponse.ts index 2203360..12d5a9d 100644 --- a/packages/server/src/assertion/verifyAssertionResponse.ts +++ b/packages/server/src/assertion/verifyAssertionResponse.ts @@ -1,6 +1,6 @@ import base64url from 'base64url'; import { - AssertionCredentialJSON, + AuthenticationCredentialJSON, AuthenticatorDevice, UserVerificationRequirement, } from '@simplewebauthn/typescript-types'; @@ -13,7 +13,7 @@ import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import isBase64URLString from '../helpers/isBase64URLString'; export type VerifyAssertionResponseOpts = { - credential: AssertionCredentialJSON; + credential: AuthenticationCredentialJSON; expectedChallenge: string; expectedOrigin: string | string[]; expectedRPID: string | string[]; diff --git a/packages/typescript-types/src/index.ts b/packages/typescript-types/src/index.ts index 0fed2f8..a930861 100644 --- a/packages/typescript-types/src/index.ts +++ b/packages/typescript-types/src/index.ts @@ -60,7 +60,7 @@ export interface RegistrationCredential extends PublicKeyCredential { } /** - * A slightly-modified AttestationCredential to simplify working with ArrayBuffers that + * A slightly-modified RegistrationCredential to simplify working with ArrayBuffers that * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. */ export interface RegistrationCredentialJSON @@ -74,16 +74,16 @@ export interface RegistrationCredentialJSON /** * The value returned from navigator.credentials.get() */ -export interface AssertionCredential extends PublicKeyCredential { +export interface AuthenticationCredential extends PublicKeyCredential { response: AuthenticatorAssertionResponse; } /** - * A slightly-modified AssertionCredential to simplify working with ArrayBuffers that + * A slightly-modified AuthenticationCredential to simplify working with ArrayBuffers that * are Base64URL-encoded in the browser so that they can be sent as JSON to the server. */ -export interface AssertionCredentialJSON - extends Omit { +export interface AuthenticationCredentialJSON + extends Omit { rawId: Base64URLString; response: AuthenticatorAssertionResponseJSON; clientExtensionResults: AuthenticationExtensionsClientOutputs; -- cgit v1.2.3 From 2bb27c6febdbacbd7bbe4356318a6b3fa6fd84db Mon Sep 17 00:00:00 2001 From: Jarrett Helton Date: Tue, 24 Aug 2021 16:05:03 -0400 Subject: rename default exports --- packages/browser/src/methods/startAuthentication.ts | 4 ++-- packages/browser/src/methods/startRegistration.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'packages/browser/src') diff --git a/packages/browser/src/methods/startAuthentication.ts b/packages/browser/src/methods/startAuthentication.ts index 1764ff1..1f431f5 100644 --- a/packages/browser/src/methods/startAuthentication.ts +++ b/packages/browser/src/methods/startAuthentication.ts @@ -15,7 +15,7 @@ import toPublicKeyCredentialDescriptor from '../helpers/toPublicKeyCredentialDes * * @param requestOptionsJSON Output from @simplewebauthn/server's generateAssertionOptions(...) */ -export default async function startAssertion( +export default async function startAuthentication( requestOptionsJSON: PublicKeyCredentialRequestOptionsJSON, ): Promise { if (!supportsWebauthn()) { @@ -40,7 +40,7 @@ export default async function startAssertion( const credential = (await navigator.credentials.get({ publicKey })) as AuthenticationCredential; if (!credential) { - throw new Error('Assertion was not completed'); + throw new Error('Authentication was not completed'); } const { id, rawId, response, type } = credential; diff --git a/packages/browser/src/methods/startRegistration.ts b/packages/browser/src/methods/startRegistration.ts index be97a1a..1bb5c34 100644 --- a/packages/browser/src/methods/startRegistration.ts +++ b/packages/browser/src/methods/startRegistration.ts @@ -37,7 +37,7 @@ export default async function startRegistration( const credential = (await navigator.credentials.create({ publicKey })) as RegistrationCredential; if (!credential) { - throw new Error('Attestation was not completed'); + throw new Error('Registration was not completed'); } const { id, rawId, response, type } = credential; -- cgit v1.2.3 From f4dff6a0b42dca2c06fd744c087d3c4a2e03577e Mon Sep 17 00:00:00 2001 From: Jarrett Helton Date: Tue, 24 Aug 2021 18:45:31 -0400 Subject: fix unit tests --- packages/browser/src/methods/startAuthentication.test.ts | 2 +- packages/browser/src/methods/startRegistration.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/browser/src') diff --git a/packages/browser/src/methods/startAuthentication.test.ts b/packages/browser/src/methods/startAuthentication.test.ts index acd97c3..5e74789 100644 --- a/packages/browser/src/methods/startAuthentication.test.ts +++ b/packages/browser/src/methods/startAuthentication.test.ts @@ -138,7 +138,7 @@ test('should throw error if assertion is cancelled for some reason', async done }); }); - await expect(startAuthentication(goodOpts1)).rejects.toThrow('Assertion was not completed'); + await expect(startAuthentication(goodOpts1)).rejects.toThrow('Authentication was not completed'); done(); }); diff --git a/packages/browser/src/methods/startRegistration.test.ts b/packages/browser/src/methods/startRegistration.test.ts index 2569a99..e11718e 100644 --- a/packages/browser/src/methods/startRegistration.test.ts +++ b/packages/browser/src/methods/startRegistration.test.ts @@ -124,7 +124,7 @@ test('should throw error if attestation is cancelled for some reason', async don }); }); - await expect(startRegistration(goodOpts1)).rejects.toThrow('Attestation was not completed'); + await expect(startRegistration(goodOpts1)).rejects.toThrow('Registration was not completed'); done(); }); -- cgit v1.2.3