summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts444
-rw-r--r--packages/server/src/helpers/decodeClientDataJSON.ts7
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.ts9
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,
+};