diff options
Diffstat (limited to 'packages/server/src')
3 files changed, 267 insertions, 193 deletions
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 44f6d73..44aeffd 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -1,34 +1,28 @@ +import { + assert, + assertEquals, + assertExists, + assertRejects, +} from "https://deno.land/std@0.198.0/assert/mod.ts"; +import { + returnsNext, + stub, +} from "https://deno.land/std@0.198.0/testing/mock.ts"; + import { verifyAuthenticationResponse } from "./verifyAuthenticationResponse.ts"; -import * as esmDecodeClientDataJSON from "../helpers/decodeClientDataJSON.ts"; -import * as esmParseAuthenticatorData from "../helpers/parseAuthenticatorData.ts"; -import { toHash } from "../helpers/toHash.ts"; +import { _decodeClientDataJSONInternals } from "../helpers/decodeClientDataJSON.ts"; import { - AuthenticationResponseJSON, - AuthenticatorDevice, -} from "@simplewebauthn/typescript-types"; + _parseAuthenticatorDataInternals, + parseAuthenticatorData, +} from "../helpers/parseAuthenticatorData.ts"; +import { toHash } from "../helpers/toHash.ts"; +import { AuthenticationResponseJSON, AuthenticatorDevice } from "../deps.ts"; import { isoBase64URL, isoUint8Array } from "../helpers/iso/index.ts"; +import { assertObjectMatch } from "https://deno.land/std@0.198.0/assert/assert_object_match.ts"; +import { assertFalse } from "https://deno.land/std@0.198.0/assert/assert_false.ts"; -let mockDecodeClientData: jest.SpyInstance; -let mockParseAuthData: jest.SpyInstance; - -beforeEach(() => { - mockDecodeClientData = jest.spyOn( - esmDecodeClientDataJSON, - "decodeClientDataJSON", - ); - mockParseAuthData = jest.spyOn( - esmParseAuthenticatorData, - "parseAuthenticatorData", - ); -}); - -afterEach(() => { - mockDecodeClientData.mockRestore(); - mockParseAuthData.mockRestore(); -}); - -test("should verify an assertion response", async () => { +Deno.test("should verify an assertion response", async () => { const verification = await verifyAuthenticationResponse({ response: assertionResponse, expectedChallenge: assertionChallenge, @@ -38,10 +32,10 @@ test("should verify an assertion response", async () => { requireUserVerification: false, }); - expect(verification.verified).toEqual(true); + assertEquals(verification.verified, true); }); -test("should return authenticator info after verification", async () => { +Deno.test("should return authenticator info after verification", async () => { const verification = await verifyAuthenticationResponse({ response: assertionResponse, expectedChallenge: assertionChallenge, @@ -51,75 +45,106 @@ test("should return authenticator info after verification", async () => { requireUserVerification: false, }); - expect(verification.authenticationInfo.newCounter).toEqual(144); - expect(verification.authenticationInfo.credentialID).toEqual( + assertEquals(verification.authenticationInfo.newCounter, 144); + assertEquals( + verification.authenticationInfo.credentialID, authenticator.credentialID, ); - expect(verification.authenticationInfo?.origin).toEqual(assertionOrigin); - expect(verification.authenticationInfo?.rpID).toEqual("dev.dontneeda.pw"); + assertEquals(verification.authenticationInfo?.origin, assertionOrigin); + assertEquals(verification.authenticationInfo?.rpID, "dev.dontneeda.pw"); }); -test("should throw when response challenge is not expected value", async () => { - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: "shouldhavebeenthisvalue", - expectedOrigin: "https://different.address", - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/authentication response challenge/i); +Deno.test("should throw when response challenge is not expected value", async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: "shouldhavebeenthisvalue", + expectedOrigin: "https://different.address", + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "authentication response challenge", + ); }); -test("should throw when response origin is not expected value", async () => { - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: "https://different.address", - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/authentication response origin/i); +Deno.test("should throw when response origin is not expected value", async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: "https://different.address", + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "authentication response origin", + ); }); -test("should throw when assertion type is not webauthn.create", async () => { - // @ts-ignore 2345 - mockDecodeClientData.mockReturnValue({ - origin: assertionOrigin, - type: "webauthn.badtype", - challenge: assertionChallenge, - }); +Deno.test("should throw when assertion type is not webauthn.create", async () => { + const mockDecodeClientData = stub( + _decodeClientDataJSONInternals, + "stubThis", + returnsNext([ + { + origin: assertionOrigin, + type: "webauthn.badtype", + challenge: assertionChallenge, + }, + ]), + ); + + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "authentication response type", + ); - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/authentication response type/i); + mockDecodeClientData.restore(); }); -test("should throw error if user was not present", async () => { - mockParseAuthData.mockReturnValue({ - rpIdHash: await toHash(Buffer.from("dev.dontneeda.pw", "ascii")), - flags: 0, - }); +Deno.test("should throw error if user was not present", async () => { + const mockParseAuthData = stub( + _parseAuthenticatorDataInternals, + "stubThis", + // @ts-ignore: Only return the values that matter + returnsNext([ + { + rpIdHash: await toHash( + isoUint8Array.fromASCIIString("dev.dontneeda.pw"), + ), + flags: { up: false }, + }, + ]), + ); + + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "not present", + ); - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/not present/i); + mockParseAuthData.restore(); }); -test("should throw error if previous counter value is not less than in response", async () => { +Deno.test("should throw error if previous counter value is not less than in response", async () => { // This'll match the `counter` value in `assertionResponse`, simulating a potential replay attack const badCounter = 144; const badDevice = { @@ -127,36 +152,51 @@ test("should throw error if previous counter value is not less than in response" counter: badCounter, }; - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: badDevice, - requireUserVerification: false, - }), - ).rejects.toThrow(/counter value/i); + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: badDevice, + requireUserVerification: false, + }), + Error, + "counter value", + ); }); -test("should throw error if assertion RP ID is unexpected value", async () => { - mockParseAuthData.mockReturnValue({ - rpIdHash: await toHash(Buffer.from("bad.url", "ascii")), - flags: 0, - }); +Deno.test("should throw error if assertion RP ID is unexpected value", async () => { + const mockParseAuthData = stub( + _parseAuthenticatorDataInternals, + "stubThis", + // @ts-ignore: Only return the values that matter + returnsNext([ + { + rpIdHash: await toHash(isoUint8Array.fromASCIIString("bad.url")), + flags: 0, + }, + ]), + ); + + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "RP ID", + ); - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/rp id/i); + mockParseAuthData.restore(); }); -test("should not compare counters if both are 0", async () => { +Deno.test("should not compare counters if both are 0", async () => { const verification = await verifyAuthenticationResponse({ response: assertionFirstTimeUsedResponse, expectedChallenge: assertionFirstTimeUsedChallenge, @@ -166,38 +206,50 @@ test("should not compare counters if both are 0", async () => { requireUserVerification: false, }); - expect(verification.verified).toEqual(true); + assertEquals(verification.verified, true); }); -test("should throw an error if user verification is required but user was not verified", async () => { - const actualData = esmParseAuthenticatorData.parseAuthenticatorData( +Deno.test("should throw an error if user verification is required but user was not verified", async () => { + const actualData = parseAuthenticatorData( isoBase64URL.toBuffer(assertionResponse.response.authenticatorData), ); - mockParseAuthData.mockReturnValue({ - ...actualData, - flags: { - up: true, - uv: false, - }, - }); + const mockParseAuthData = stub( + _parseAuthenticatorDataInternals, + "stubThis", + // @ts-ignore: Only return the values that matter + returnsNext([ + { + ...actualData, + flags: { + up: true, + uv: false, + }, + }, + ]), + ); + + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + requireUserVerification: true, + }), + Error, + "user could not be verified", + ); - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - requireUserVerification: true, - }), - ).rejects.toThrow(/user could not be verified/i); + mockParseAuthData.restore(); }); // TODO: Get a real TPM authentication response in here -test.skip("should verify TPM assertion", async () => { +Deno.test("should verify TPM assertion", { ignore: true }, async () => { const expectedChallenge = "dG90YWxseVVuaXF1ZVZhbHVlRXZlcnlBc3NlcnRpb24"; - jest.spyOn(isoBase64URL, "toString").mockReturnValueOnce(expectedChallenge); + // jest.spyOn(isoBase64URL, "toString").mockReturnValueOnce(expectedChallenge); const verification = await verifyAuthenticationResponse({ response: { id: "YJ8FMM-AmcUt73XPX341WXWd7ypBMylGjjhu0g3VzME", @@ -225,10 +277,10 @@ test.skip("should verify TPM assertion", async () => { }, }); - expect(verification.verified).toEqual(true); + assert(verification.verified); }); -test("should support multiple possible origins", async () => { +Deno.test("should support multiple possible origins", async () => { const verification = await verifyAuthenticationResponse({ response: assertionResponse, expectedChallenge: assertionChallenge, @@ -238,23 +290,26 @@ test("should support multiple possible origins", async () => { requireUserVerification: false, }); - expect(verification.verified).toEqual(true); - expect(verification.authenticationInfo?.origin).toEqual(assertionOrigin); + assert(verification.verified); + assertEquals(verification.authenticationInfo?.origin, assertionOrigin); }); -test("should throw an error if origin not in list of expected origins", async () => { - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: ["https://simplewebauthn.dev", "https://fizz.buzz"], - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/unexpected authentication response origin/i); +Deno.test("should throw an error if origin not in list of expected origins", async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: ["https://simplewebauthn.dev", "https://fizz.buzz"], + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "Unexpected authentication response origin", + ); }); -test("should support multiple possible RP IDs", async () => { +Deno.test("should support multiple possible RP IDs", async () => { const verification = await verifyAuthenticationResponse({ response: assertionResponse, expectedChallenge: assertionChallenge, @@ -264,23 +319,26 @@ test("should support multiple possible RP IDs", async () => { requireUserVerification: false, }); - expect(verification.verified).toEqual(true); - expect(verification.authenticationInfo?.rpID).toEqual("dev.dontneeda.pw"); + assert(verification.verified); + assertEquals(verification.authenticationInfo?.rpID, "dev.dontneeda.pw"); }); -test("should throw an error if RP ID not in list of possible RP IDs", async () => { - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: ["simplewebauthn.dev"], - authenticator: authenticator, - }), - ).rejects.toThrow(/unexpected rp id/i); +Deno.test("should throw an error if RP ID not in list of possible RP IDs", async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: ["simplewebauthn.dev"], + authenticator: authenticator, + }), + Error, + "Unexpected RP ID", + ); }); -test("should pass verification if custom challenge verifier returns true", async () => { +Deno.test("should pass verification if custom challenge verifier returns true", async () => { const verification = await verifyAuthenticationResponse({ response: { id: @@ -321,22 +379,25 @@ test("should pass verification if custom challenge verifier returns true", async }, }); - expect(verification.verified).toEqual(true); + assert(verification.verified); }); -test("should fail verification if custom challenge verifier returns false", async () => { - await expect( - verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: (challenge) => challenge === "willNeverMatch", - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - }), - ).rejects.toThrow(/custom challenge verifier returned false/i); +Deno.test("should fail verification if custom challenge verifier returns false", async () => { + await assertRejects( + () => + verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: (challenge) => challenge === "willNeverMatch", + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + }), + Error, + "Custom challenge verifier returned false", + ); }); -test("should return authenticator extension output", async () => { +Deno.test("should return authenticator extension output", async () => { const verification = await verifyAuthenticationResponse({ response: { response: { @@ -369,8 +430,9 @@ test("should return authenticator extension output", async () => { }, }); - expect(verification.authenticationInfo?.authenticatorExtensionResults) - .toMatchObject({ + assertObjectMatch( + verification.authenticationInfo!.authenticatorExtensionResults!, + { devicePubKey: { dpk: isoUint8Array.fromHex( "A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA", @@ -382,10 +444,11 @@ test("should return authenticator extension output", async () => { scope: isoUint8Array.fromHex("00"), aaguid: isoUint8Array.fromHex("B93FD961F2E6462FB12282002247DE78"), }, - }); + }, + ); }); -test("should return credential backup info", async () => { +Deno.test("should return credential backup info", async () => { const verification = await verifyAuthenticationResponse({ response: assertionResponse, expectedChallenge: assertionChallenge, @@ -395,10 +458,25 @@ test("should return credential backup info", async () => { requireUserVerification: false, }); - expect(verification.authenticationInfo?.credentialDeviceType).toEqual( + assertEquals( + verification.authenticationInfo?.credentialDeviceType, "singleDevice", ); - expect(verification.authenticationInfo?.credentialBackedUp).toEqual(false); + assertEquals(verification.authenticationInfo?.credentialBackedUp, false); +}); + +Deno.test("should return user verified flag after successful auth", async () => { + const verification = await verifyAuthenticationResponse({ + response: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: "dev.dontneeda.pw", + authenticator: authenticator, + requireUserVerification: false, + }); + + assertExists(verification.authenticationInfo?.userVerified); + assertFalse(verification.authenticationInfo?.userVerified); }); /** @@ -469,17 +547,3 @@ const authenticatorFirstTimeUsed: AuthenticatorDevice = { ), counter: 0, }; - -test("should return user verified flag after successful auth", async () => { - const verification = await verifyAuthenticationResponse({ - response: assertionResponse, - expectedChallenge: assertionChallenge, - expectedOrigin: assertionOrigin, - expectedRPID: "dev.dontneeda.pw", - authenticator: authenticator, - requireUserVerification: false, - }); - - expect(verification.authenticationInfo?.userVerified).toBeDefined(); - expect(verification.authenticationInfo?.userVerified).toEqual(false); -}); diff --git a/packages/server/src/helpers/decodeClientDataJSON.ts b/packages/server/src/helpers/decodeClientDataJSON.ts index bb878dc..fa09181 100644 --- a/packages/server/src/helpers/decodeClientDataJSON.ts +++ b/packages/server/src/helpers/decodeClientDataJSON.ts @@ -7,7 +7,7 @@ export function decodeClientDataJSON(data: string): ClientDataJSON { const toString = isoBase64URL.toString(data); const clientData: ClientDataJSON = JSON.parse(toString); - return clientData; + return _decodeClientDataJSONInternals.stubThis(clientData); } export type ClientDataJSON = { @@ -20,3 +20,8 @@ export type ClientDataJSON = { status: "present" | "supported" | "not-supported"; }; }; + +// Make it possible to stub the return value during testing +export const _decodeClientDataJSONInternals = { + stubThis: (value: ClientDataJSON) => value, +}; diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts index 750818a..9e02037 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.ts @@ -79,7 +79,7 @@ export function parseAuthenticatorData( throw new Error("Leftover bytes detected while parsing authenticator data"); } - return { + return _parseAuthenticatorDataInternals.stubThis({ rpIdHash, flagsBuf, flags, @@ -90,7 +90,7 @@ export function parseAuthenticatorData( credentialPublicKey, extensionsData, extensionsDataBuffer, - }; + }); } export type ParsedAuthenticatorData = { @@ -113,3 +113,8 @@ export type ParsedAuthenticatorData = { extensionsData?: AuthenticationExtensionsAuthenticatorOutputs; extensionsDataBuffer?: Uint8Array; }; + +// Make it possible to stub the return value during testing +export const _parseAuthenticatorDataInternals = { + stubThis: (value: ParsedAuthenticatorData) => value, +}; |