From f172625f2681b0899de3ff7ac1c87a28041ba7c2 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 30 Mar 2023 13:24:07 +0200 Subject: [PATCH] [EC-598] feat: start working on new `Fido2ClientService` --- .../fido2-client.service.abstraction.ts | 87 +++++++++++++++++++ .../services/fido2-client.service.spec.ts | 57 ++++++++++++ .../webauthn/services/fido2-client.service.ts | 30 +++++++ 3 files changed, 174 insertions(+) create mode 100644 libs/common/src/webauthn/abstractions/fido2-client.service.abstraction.ts create mode 100644 libs/common/src/webauthn/services/fido2-client.service.spec.ts create mode 100644 libs/common/src/webauthn/services/fido2-client.service.ts diff --git a/libs/common/src/webauthn/abstractions/fido2-client.service.abstraction.ts b/libs/common/src/webauthn/abstractions/fido2-client.service.abstraction.ts new file mode 100644 index 0000000000..06fcfadff5 --- /dev/null +++ b/libs/common/src/webauthn/abstractions/fido2-client.service.abstraction.ts @@ -0,0 +1,87 @@ +export type UserVerification = "discouraged" | "preferred" | "required"; + +export abstract class Fido2ClientService { + createCredential: ( + params: CreateCredentialParams, + abortController?: AbortController + ) => Promise; + assertCredential: ( + params: AssertCredentialParams, + abortController?: AbortController + ) => Promise; +} + +export interface CreateCredentialParams { + origin: string; + sameOriginWithAncestors: boolean; + attestation?: "direct" | "enterprise" | "indirect" | "none"; + authenticatorSelection?: { + // authenticatorAttachment?: AuthenticatorAttachment; // not used + requireResidentKey?: boolean; + residentKey?: "discouraged" | "preferred" | "required"; + userVerification?: UserVerification; + }; + challenge: string; // b64 encoded + excludeCredentials?: { + id: string; // b64 encoded + transports?: ("ble" | "internal" | "nfc" | "usb")[]; + // type: "public-key"; // not used + }[]; + extensions?: { + appid?: string; + appidExclude?: string; + credProps?: boolean; + uvm?: boolean; + }; + pubKeyCredParams: { + alg: number; + // type: "public-key"; // not used + }[]; + rp: { + id?: string; + name: string; + }; + user: { + id: string; // b64 encoded + displayName: string; + }; + timeout?: number; +} + +export interface CreateCredentialResult { + credentialId: string; + clientDataJSON: string; + attestationObject: string; + authData: string; + publicKeyAlgorithm: number; + transports: string[]; +} + +export interface AssertCredentialParams { + allowedCredentialIds: string[]; + rpId: string; + origin: string; + challenge: string; + userVerification?: UserVerification; + timeout: number; +} + +export interface AssertCredentialResult { + credentialId: string; + clientDataJSON: string; + authenticatorData: string; + signature: string; + userHandle: string; +} + +export class Fido2Error extends Error { + constructor(message: string, readonly fallbackRequested = false) { + super(message); + } +} + +export class RequestAbortedError extends Fido2Error { + constructor(fallbackRequested = false) { + super("Fido2 request was aborted", fallbackRequested); + } +} diff --git a/libs/common/src/webauthn/services/fido2-client.service.spec.ts b/libs/common/src/webauthn/services/fido2-client.service.spec.ts new file mode 100644 index 0000000000..17d77eb2cf --- /dev/null +++ b/libs/common/src/webauthn/services/fido2-client.service.spec.ts @@ -0,0 +1,57 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CreateCredentialParams } from "../abstractions/fido2-client.service.abstraction"; + +import { Fido2AuthenticatorService } from "./fido2-authenticator.service"; +import { Fido2ClientService } from "./fido2-client.service"; + +const RpId = "bitwarden.com"; + +describe("FidoAuthenticatorService", () => { + let authenticator!: MockProxy; + let client!: Fido2ClientService; + + beforeEach(async () => { + authenticator = mock(); + client = new Fido2ClientService(authenticator); + }); + + describe("createCredential", () => { + describe("invalid input parameters", () => { + /** Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException. */ + it("throw error if sameOriginWithAncestors is false", async () => { + const params = createParams({ sameOriginWithAncestors: false }); + + const result = async () => await client.createCredential(params); + + await expect(result).rejects.toThrowError(new DOMException(undefined, "NotAllowedError")); + }); + }); + + function createParams(params: Partial = {}): CreateCredentialParams { + return { + origin: params.origin ?? "bitwarden.com", + sameOriginWithAncestors: params.sameOriginWithAncestors ?? true, + attestation: params.attestation, + authenticatorSelection: params.authenticatorSelection, + challenge: params.challenge ?? "MzItYnl0ZXMtYmFzZTY0LWVuY29kZS1jaGFsbGVuZ2U", + excludeCredentials: params.excludeCredentials, + extensions: params.extensions, + pubKeyCredParams: params.pubKeyCredParams ?? [ + { + alg: -7, + }, + ], + rp: params.rp ?? { + id: RpId, + name: "Bitwarden", + }, + user: params.user ?? { + id: "YmFzZTY0LWVuY29kZWQtdXNlci1pZA", + displayName: "User Name", + }, + timeout: params.timeout, + }; + } + }); +}); diff --git a/libs/common/src/webauthn/services/fido2-client.service.ts b/libs/common/src/webauthn/services/fido2-client.service.ts new file mode 100644 index 0000000000..0353a81eeb --- /dev/null +++ b/libs/common/src/webauthn/services/fido2-client.service.ts @@ -0,0 +1,30 @@ +import { Fido2AuthenticatorService } from "../abstractions/fido2-authenticator.service.abstraction"; +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, + Fido2ClientService as Fido2ClientServiceAbstraction, +} from "../abstractions/fido2-client.service.abstraction"; + +export class Fido2ClientService implements Fido2ClientServiceAbstraction { + constructor(private authenticator: Fido2AuthenticatorService) {} + + createCredential( + params: CreateCredentialParams, + abortController?: AbortController + ): Promise { + if (!params.sameOriginWithAncestors) { + throw new DOMException(undefined, "NotAllowedError"); + } + + throw new Error("Not implemented"); + } + + assertCredential( + params: AssertCredentialParams, + abortController?: AbortController + ): Promise { + throw new Error("Not implemented"); + } +}