1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[EC-598] feat: add user confirmation test to assertion

also rewrite to use cipher views in tests
This commit is contained in:
Andreas Coroiu
2023-03-29 16:23:19 +02:00
parent c2ec87a3f3
commit 597bc0b197
2 changed files with 76 additions and 40 deletions

View File

@@ -7,8 +7,8 @@ import { Utils } from "../../misc/utils";
import { CipherService } from "../../vault/abstractions/cipher.service"; import { CipherService } from "../../vault/abstractions/cipher.service";
import { CipherType } from "../../vault/enums/cipher-type"; import { CipherType } from "../../vault/enums/cipher-type";
import { Cipher } from "../../vault/models/domain/cipher"; import { Cipher } from "../../vault/models/domain/cipher";
import { Login } from "../../vault/models/domain/login";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
import { LoginView } from "../../vault/models/view/login.view";
import { import {
Fido2AutenticatorErrorCode, Fido2AutenticatorErrorCode,
Fido2AuthenticatorGetAssertionParams, Fido2AuthenticatorGetAssertionParams,
@@ -19,7 +19,7 @@ import {
NewCredentialParams, NewCredentialParams,
} from "../abstractions/fido2-user-interface.service.abstraction"; } from "../abstractions/fido2-user-interface.service.abstraction";
import { Fido2Utils } from "../abstractions/fido2-utils"; 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"; import { AAGUID, Fido2AuthenticatorService } from "./fido2-authenticator.service";
@@ -93,25 +93,26 @@ describe("FidoAuthenticatorService", () => {
describe.skip("when extensions parameter is present", () => undefined); describe.skip("when extensions parameter is present", () => undefined);
describe("vault contains excluded non-discoverable credential", () => { describe("vault contains excluded non-discoverable credential", () => {
let excludedCipherView: CipherView; let excludedCipher: CipherView;
let params: Fido2AuthenticatorMakeCredentialsParams; let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => { beforeEach(async () => {
const excludedCipher = createCipher({ type: CipherType.Login }); excludedCipher = createCipherView(
excludedCipherView = await excludedCipher.decrypt(); { type: CipherType.Login },
excludedCipherView.fido2Key.nonDiscoverableId = Utils.newGuid(); { nonDiscoverableId: Utils.newGuid() }
);
params = await createParams({ params = await createParams({
excludeCredentialDescriptorList: [ excludeCredentialDescriptorList: [
{ {
id: Utils.guidToRawFormat(excludedCipherView.fido2Key.nonDiscoverableId), id: Utils.guidToRawFormat(excludedCipher.fido2Key.nonDiscoverableId),
type: "public-key", type: "public-key",
}, },
], ],
}); });
cipherService.get.mockImplementation(async (id) => 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; let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => { beforeEach(async () => {
const excludedCipher = createCipher(); const excludedCipher = createCipherView();
excludedCipherView = await excludedCipher.decrypt(); excludedCipherView = await excludedCipher;
params = await createParams({ params = await createParams({
excludeCredentialDescriptorList: [ excludeCredentialDescriptorList: [
{ id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" }, { id: Utils.guidToRawFormat(excludedCipher.id), type: "public-key" },
@@ -300,19 +301,16 @@ describe("FidoAuthenticatorService", () => {
}); });
describe("creation of non-discoverable credential", () => { describe("creation of non-discoverable credential", () => {
let existingCipherView: CipherView; let existingCipher: CipherView;
let params: Fido2AuthenticatorMakeCredentialsParams; let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => { beforeEach(async () => {
const existingCipher = createCipher({ type: CipherType.Login }); existingCipher = createCipherView({ type: CipherType.Login });
existingCipher.login = new Login();
existingCipher.fido2Key = undefined;
existingCipherView = await existingCipher.decrypt();
params = await createParams(); params = await createParams();
cipherService.get.mockImplementation(async (id) => 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. * Deviation: Only `rpEntity.name` and `userEntity.name` is shown.
* */ * */
it("should request confirmation from user", async () => { it("should request confirmation from user", async () => {
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id);
await authenticator.makeCredential(params); await authenticator.makeCredential(params);
@@ -332,7 +330,7 @@ describe("FidoAuthenticatorService", () => {
it("should save credential to vault if request confirmed by user", async () => { it("should save credential to vault if request confirmed by user", async () => {
const encryptedCipher = Symbol(); const encryptedCipher = Symbol();
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id);
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
await authenticator.makeCredential(params); await authenticator.makeCredential(params);
@@ -341,7 +339,7 @@ describe("FidoAuthenticatorService", () => {
expect(saved).toEqual( expect(saved).toEqual(
expect.objectContaining({ expect.objectContaining({
type: CipherType.Login, type: CipherType.Login,
name: existingCipherView.name, name: existingCipher.name,
fido2Key: expect.objectContaining({ fido2Key: expect.objectContaining({
nonDiscoverableId: expect.anything(), 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. */ /** 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 () => { it("should throw unkown error if creation fails", async () => {
const encryptedCipher = Symbol(); const encryptedCipher = Symbol();
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipherView.id); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(existingCipher.id);
cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher); cipherService.encrypt.mockResolvedValue(encryptedCipher as unknown as Cipher);
cipherService.updateWithServer.mockRejectedValue(new Error("Internal error")); cipherService.updateWithServer.mockRejectedValue(new Error("Internal error"));
@@ -399,14 +397,14 @@ describe("FidoAuthenticatorService", () => {
let params: Fido2AuthenticatorMakeCredentialsParams; let params: Fido2AuthenticatorMakeCredentialsParams;
beforeEach(async () => { beforeEach(async () => {
const cipher = createCipher({ id: cipherId, type: CipherType.Login }); const cipher = createCipherView({ id: cipherId, type: CipherType.Login });
params = await createParams({ requireResidentKey }); params = await createParams({ requireResidentKey });
userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(cipherId); userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(cipherId);
userInterface.confirmNewCredential.mockResolvedValue(true); userInterface.confirmNewCredential.mockResolvedValue(true);
cipherService.get.mockImplementation(async (cipherId) => 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) => { cipherService.encrypt.mockImplementation(async (cipher) => {
cipher.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability cipher.fido2Key.nonDiscoverableId = nonDiscoverableId; // Replace id for testability
return {} as any; return {} as any;
@@ -537,14 +535,14 @@ describe("FidoAuthenticatorService", () => {
}); });
describe("vault is missing non-discoverable credential", () => { describe("vault is missing non-discoverable credential", () => {
let excludedId: string; let credentialId: string;
let params: Fido2AuthenticatorGetAssertionParams; let params: Fido2AuthenticatorGetAssertionParams;
beforeEach(async () => { beforeEach(async () => {
excludedId = Utils.newGuid(); credentialId = Utils.newGuid();
params = await createParams({ params = await createParams({
allowCredentialDescriptorList: [ allowCredentialDescriptorList: [
{ id: Utils.guidToRawFormat(excludedId), type: "public-key" }, { id: Utils.guidToRawFormat(credentialId), type: "public-key" },
], ],
rpId: RpId, rpId: RpId,
}); });
@@ -560,8 +558,8 @@ describe("FidoAuthenticatorService", () => {
}); });
it("should throw error if credential exists but rpId does not match", async () => { it("should throw error if credential exists but rpId does not match", async () => {
const cipher = await createCipher({ type: CipherType.Login }).decrypt(); const cipher = await createCipherView({ type: CipherType.Login });
cipher.fido2Key.nonDiscoverableId = excludedId; cipher.fido2Key.nonDiscoverableId = credentialId;
cipher.fido2Key.rpId = "mismatch-rpid"; cipher.fido2Key.rpId = "mismatch-rpid";
cipherService.getAllDecrypted.mockResolvedValue([cipher]); 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( async function createParams(
params: Partial<Fido2AuthenticatorGetAssertionParams> = {} params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
): Promise<Fido2AuthenticatorGetAssertionParams> { ): Promise<Fido2AuthenticatorGetAssertionParams> {
@@ -616,12 +644,17 @@ describe("FidoAuthenticatorService", () => {
}); });
}); });
function createCipher(data: Partial<Cipher> = {}): Cipher { function createCipherView(
const cipher = new Cipher(); data: Partial<Omit<CipherView, "fido2Key">> = {},
fido2Key: Partial<Fido2KeyView> = {}
): CipherView {
const cipher = new CipherView();
cipher.id = data.id ?? Utils.newGuid(); cipher.id = data.id ?? Utils.newGuid();
cipher.type = data.type ?? CipherType.Fido2Key; cipher.type = data.type ?? CipherType.Fido2Key;
cipher.login = data.type ?? data.type === CipherType.Login ? new Login() : null; cipher.login = data.type ?? data.type === CipherType.Login ? new LoginView() : null;
cipher.fido2Key = data.fido2Key ?? new Fido2Key(); cipher.fido2Key = new Fido2KeyView();
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
cipher.fido2Key.rpId = fido2Key.rpId;
return cipher; return cipher;
} }

View File

@@ -146,25 +146,28 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
} }
let credentialOptions: Fido2KeyView[]; let credentialOptions: CipherView[];
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
if (params.allowCredentialDescriptorList?.length > 0) { if (params.allowCredentialDescriptorList?.length > 0) {
const ciphers = await this.findNonDiscoverableCredentials( credentialOptions = await this.findNonDiscoverableCredentials(
params.allowCredentialDescriptorList, params.allowCredentialDescriptorList,
params.rpId params.rpId
); );
credentialOptions = ciphers.map((c) => c.fido2Key);
} else { } else {
const ciphers = await this.findDiscoverableCredentials(params.rpId); credentialOptions = await this.findDiscoverableCredentials(params.rpId);
credentialOptions = ciphers.map((c) => c.fido2Key);
} }
if (credentialOptions.length === 0) { if (credentialOptions.length === 0) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); 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( private async vaultContainsCredentials(