From 597bc0b1977380910c6941fdf96281e073148a6e Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Wed, 29 Mar 2023 16:23:19 +0200 Subject: [PATCH] [EC-598] feat: add user confirmation test to assertion also rewrite to use cipher views in tests --- .../fido2-authenticator.service.spec.ts | 101 ++++++++++++------ .../services/fido2-authenticator.service.ts | 15 +-- 2 files changed, 76 insertions(+), 40 deletions(-) diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts index 20b032ec93..ca2d5d5be0 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -7,8 +7,8 @@ import { Utils } from "../../misc/utils"; import { CipherService } from "../../vault/abstractions/cipher.service"; import { CipherType } from "../../vault/enums/cipher-type"; import { Cipher } from "../../vault/models/domain/cipher"; -import { Login } from "../../vault/models/domain/login"; import { CipherView } from "../../vault/models/view/cipher.view"; +import { LoginView } from "../../vault/models/view/login.view"; import { Fido2AutenticatorErrorCode, Fido2AuthenticatorGetAssertionParams, @@ -19,7 +19,7 @@ import { NewCredentialParams, } from "../abstractions/fido2-user-interface.service.abstraction"; import { Fido2Utils } from "../abstractions/fido2-utils"; -import { Fido2Key } from "../models/domain/fido2-key"; +import { Fido2KeyView } from "../models/view/fido2-key.view"; import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service"; @@ -93,25 +93,26 @@ describe("FidoAuthenticatorService", () => { describe.skip("when extensions parameter is present", () => undefined); describe("vault contains excluded non-discoverable credential", () => { - let excludedCipherView: CipherView; + let excludedCipher: CipherView; let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - const excludedCipher = createCipher({ type: CipherType.Login }); - excludedCipherView = await excludedCipher.decrypt(); - excludedCipherView.fido2Key.nonDiscoverableId = Utils.newGuid(); + excludedCipher = createCipherView( + { type: CipherType.Login }, + { nonDiscoverableId: Utils.newGuid() } + ); params = await createParams({ excludeCredentialDescriptorList: [ { - id: Utils.guidToRawFormat(excludedCipherView.fido2Key.nonDiscoverableId), + id: Utils.guidToRawFormat(excludedCipher.fido2Key.nonDiscoverableId), type: "public-key", }, ], }); cipherService.get.mockImplementation(async (id) => - id === excludedCipher.id ? excludedCipher : undefined + id === excludedCipher.id ? ({ decrypt: () => excludedCipher } as any) : undefined ); - cipherService.getAllDecrypted.mockResolvedValue([excludedCipherView]); + cipherService.getAllDecrypted.mockResolvedValue([excludedCipher]); }); /** @@ -161,8 +162,8 @@ describe("FidoAuthenticatorService", () => { let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - const excludedCipher = createCipher(); - excludedCipherView = await excludedCipher.decrypt(); + const excludedCipher = createCipherView(); + excludedCipherView = await excludedCipher; params = await createParams({ excludeCredentialDescriptorList: [ { id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" }, @@ -300,19 +301,16 @@ describe("FidoAuthenticatorService", () => { }); describe("creation of non-discoverable credential", () => { - let existingCipherView: CipherView; + let existingCipher: CipherView; let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - const existingCipher = createCipher({ type: CipherType.Login }); - existingCipher.login = new Login(); - existingCipher.fido2Key = undefined; - existingCipherView = await existingCipher.decrypt(); + existingCipher = createCipherView({ type: CipherType.Login }); params = await createParams(); cipherService.get.mockImplementation(async (id) => - id === existingCipher.id ? existingCipher : undefined + id === existingCipher.id ? ({ decrypt: () => existingCipher } as any) : undefined ); - cipherService.getAllDecrypted.mockResolvedValue([existingCipherView]); + cipherService.getAllDecrypted.mockResolvedValue([existingCipher]); }); /** @@ -320,7 +318,7 @@ describe("FidoAuthenticatorService", () => { * Deviation: Only `rpEntity.name` and `userEntity.name` is shown. * */ it("should request confirmation from user", async () => { - userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); + userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id); await authenticator.makeCredential(params); @@ -332,7 +330,7 @@ describe("FidoAuthenticatorService", () => { it("should save credential to vault if request confirmed by user", async () => { const encryptedCipher = Symbol(); - userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); + userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); await authenticator.makeCredential(params); @@ -341,7 +339,7 @@ describe("FidoAuthenticatorService", () => { expect(saved).toEqual( expect.objectContaining({ type: CipherType.Login, - name: existingCipherView.name, + name: existingCipher.name, fido2Key: expect.objectContaining({ nonDiscoverableId: expect.anything(), @@ -372,7 +370,7 @@ describe("FidoAuthenticatorService", () => { /** Spec: If any error occurred while creating the new credential object, return an error code equivalent to "UnknownError" and terminate the operation. */ it("should throw unkown error if creation fails", async () => { const encryptedCipher = Symbol(); - userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); + userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); @@ -399,14 +397,14 @@ describe("FidoAuthenticatorService", () => { let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - const cipher = createCipher({ id: cipherId, type: CipherType.Login }); + const cipher = createCipherView({ id: cipherId, type: CipherType.Login }); params = await createParams({ requireResidentKey }); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(cipherId); userInterface.confirmNewCredential.mockResolvedValue(true); cipherService.get.mockImplementation(async (cipherId) => - cipherId === cipher.id ? cipher : undefined + cipherId === cipher.id ? ({ decrypt: () => cipher } as any) : undefined ); - cipherService.getAllDecrypted.mockResolvedValue([await cipher.decrypt()]); + cipherService.getAllDecrypted.mockResolvedValue([await cipher]); cipherService.encrypt.mockImplementation(async (cipher) => { cipher.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability return {} as any; @@ -537,14 +535,14 @@ describe("FidoAuthenticatorService", () => { }); describe("vault is missing non-discoverable credential", () => { - let excludedId: string; + let credentialId: string; let params: Fido2AuthenticatorGetAssertionParams; beforeEach(async () => { - excludedId = Utils.newGuid(); + credentialId = Utils.newGuid(); params = await createParams({ allowCredentialDescriptorList: [ - { id: Utils.guidToRawFormat(excludedId), type: "public-key" }, + { id: Utils.guidToRawFormat(credentialId), type: "public-key" }, ], rpId: RpId, }); @@ -560,8 +558,8 @@ describe("FidoAuthenticatorService", () => { }); it("should throw error if credential exists but rpId does not match", async () => { - const cipher = await createCipher({ type: CipherType.Login }).decrypt(); - cipher.fido2Key.nonDiscoverableId = excludedId; + const cipher = await createCipherView({ type: CipherType.Login }); + cipher.fido2Key.nonDiscoverableId = credentialId; cipher.fido2Key.rpId = "mismatch-rpid"; cipherService.getAllDecrypted.mockResolvedValue([cipher]); @@ -590,6 +588,36 @@ describe("FidoAuthenticatorService", () => { }); }); + describe("assertion of non-discoverable credential", () => { + let credentialIds: string[]; + let ciphers: CipherView[]; + let params: Fido2AuthenticatorGetAssertionParams; + + beforeEach(async () => { + credentialIds = [Utils.newGuid(), Utils.newGuid()]; + ciphers = await Promise.all( + credentialIds.map((id) => + createCipherView({ type: CipherType.Login }, { nonDiscoverableId: id, rpId: RpId }) + ) + ); + params = await createParams({ + allowCredentialDescriptorList: credentialIds.map((credentialId) => ({ + id: Utils.guidToRawFormat(credentialId), + type: "public-key", + })), + rpId: RpId, + }); + cipherService.getAllDecrypted.mockResolvedValue(ciphers); + }); + + /** Spec: Prompt the user to select a public key credential source selectedCredential from credentialOptions. */ + it("should request confirmation from the user", async () => { + await authenticator.getAssertion(params); + + expect(userInterface.pickCredential).toHaveBeenCalledWith(ciphers.map((c) => c.id)); + }); + }); + async function createParams( params: Partial = {} ): Promise { @@ -616,12 +644,17 @@ describe("FidoAuthenticatorService", () => { }); }); -function createCipher(data: Partial = {}): Cipher { - const cipher = new Cipher(); +function createCipherView( + data: Partial> = {}, + fido2Key: Partial = {} +): CipherView { + const cipher = new CipherView(); cipher.id = data.id ?? Utils.newGuid(); cipher.type = data.type ?? CipherType.Fido2Key; - cipher.login = data.type ?? data.type === CipherType.Login ? new Login() : null; - cipher.fido2Key = data.fido2Key ?? new Fido2Key(); + cipher.login = data.type ?? data.type === CipherType.Login ? new LoginView() : null; + cipher.fido2Key = new Fido2KeyView(); + cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId; + cipher.fido2Key.rpId = fido2Key.rpId; return cipher; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index 1fbda09cd2..e6786c4b28 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -146,25 +146,28 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); } - let credentialOptions: Fido2KeyView[]; + let credentialOptions: CipherView[]; // eslint-disable-next-line no-empty if (params.allowCredentialDescriptorList?.length > 0) { - const ciphers = await this.findNonDiscoverableCredentials( + credentialOptions = await this.findNonDiscoverableCredentials( params.allowCredentialDescriptorList, params.rpId ); - credentialOptions = ciphers.map((c) => c.fido2Key); } else { - const ciphers = await this.findDiscoverableCredentials(params.rpId); - credentialOptions = ciphers.map((c) => c.fido2Key); + credentialOptions = await this.findDiscoverableCredentials(params.rpId); } if (credentialOptions.length === 0) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); } - throw new Error("Not implemented"); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const selectedCredential = await this.userInterface.pickCredential( + credentialOptions.map((cipher) => cipher.id) + ); + + return null; } private async vaultContainsCredentials(