diff --git a/apps/browser/src/fido2/popup/fido2/fido2.component.html b/apps/browser/src/fido2/popup/fido2/fido2.component.html
index 09864a0c34..fcd4aa8621 100644
--- a/apps/browser/src/fido2/popup/fido2/fido2.component.html
+++ b/apps/browser/src/fido2/popup/fido2/fido2.component.html
@@ -1,17 +1,6 @@
-
- A site is asking for authentication using the following credential:
-
-
-
-
+
A passkey already exists in Bitwarden for this account
diff --git a/apps/browser/src/fido2/popup/fido2/fido2.component.ts b/apps/browser/src/fido2/popup/fido2/fido2.component.ts
index 919f293bc4..2e83b2fc54 100644
--- a/apps/browser/src/fido2/popup/fido2/fido2.component.ts
+++ b/apps/browser/src/fido2/popup/fido2/fido2.component.ts
@@ -77,9 +77,6 @@ export class Fido2Component implements OnInit, OnDestroy {
cipher.fido2Key = new Fido2KeyView();
cipher.fido2Key.userName = data.userName;
this.ciphers = [cipher];
- } else if (data?.type === "ConfirmCredentialRequest") {
- const cipher = await this.cipherService.get(data.cipherId);
- this.ciphers = [await cipher.decrypt()];
} else if (data?.type === "PickCredentialRequest") {
this.ciphers = await Promise.all(
data.cipherIds.map(async (cipherId) => {
@@ -117,10 +114,16 @@ export class Fido2Component implements OnInit, OnDestroy {
async pick(cipher: CipherView) {
const data = this.data$.value;
if (data?.type === "PickCredentialRequest") {
+ let userVerified = false;
+ if (data.userVerification) {
+ userVerified = await this.passwordRepromptService.showPasswordPrompt();
+ }
+
this.send({
sessionId: this.sessionId,
cipherId: cipher.id,
type: "PickCredentialResponse",
+ userVerified,
});
} else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
let userVerified = false;
@@ -139,15 +142,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.loading = true;
}
- confirm() {
- this.send({
- sessionId: this.sessionId,
- type: "ConfirmCredentialResponse",
- });
- this.loading = true;
- }
-
- async confirmNew() {
+ async confirm() {
const data = this.data$.value;
if (data.type !== "ConfirmNewCredentialRequest") {
return;
diff --git a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts
index 680d6e07f6..d420478a84 100644
--- a/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts
+++ b/apps/browser/src/services/fido2/browser-fido2-user-interface.service.ts
@@ -15,6 +15,7 @@ import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
Fido2UserInterfaceSession,
NewCredentialParams,
+ PickCredentialParams,
} from "@bitwarden/common/fido2/abstractions/fido2-user-interface.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils";
@@ -46,17 +47,12 @@ export type BrowserFido2Message = { sessionId: string } & (
| {
type: "PickCredentialRequest";
cipherIds: string[];
+ userVerification: boolean;
}
| {
type: "PickCredentialResponse";
cipherId?: string;
- }
- | {
- type: "ConfirmCredentialRequest";
- cipherId: string;
- }
- | {
- type: "ConfirmCredentialResponse";
+ userVerified: boolean;
}
| {
type: "ConfirmNewCredentialRequest";
@@ -179,30 +175,21 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
return this.abortController.signal.aborted;
}
- async confirmCredential(cipherId: string): Promise {
- const data: BrowserFido2Message = {
- type: "ConfirmCredentialRequest",
- cipherId,
- sessionId: this.sessionId,
- };
-
- await this.send(data);
- await this.receive("ConfirmCredentialResponse");
-
- return true;
- }
-
- async pickCredential(cipherIds: string[]): Promise {
+ async pickCredential({
+ cipherIds,
+ userVerification,
+ }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
const data: BrowserFido2Message = {
type: "PickCredentialRequest",
cipherIds,
sessionId: this.sessionId,
+ userVerification,
};
await this.send(data);
const response = await this.receive("PickCredentialResponse");
- return response.cipherId;
+ return { cipherId: response.cipherId, userVerified: response.userVerified };
}
async confirmNewCredential({
diff --git a/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts b/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts
index 97f6b2514e..11ab6439aa 100644
--- a/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts
+++ b/libs/common/src/fido2/abstractions/fido2-user-interface.service.abstraction.ts
@@ -4,6 +4,11 @@ export interface NewCredentialParams {
userVerification: boolean;
}
+export interface PickCredentialParams {
+ cipherIds: string[];
+ userVerification: boolean;
+}
+
export abstract class Fido2UserInterfaceService {
newSession: (abortController?: AbortController) => Promise;
}
@@ -12,8 +17,10 @@ export abstract class Fido2UserInterfaceSession {
fallbackRequested = false;
aborted = false;
- confirmCredential: (cipherId: string, abortController?: AbortController) => Promise;
- pickCredential: (cipherIds: string[], abortController?: AbortController) => Promise;
+ pickCredential: (
+ params: PickCredentialParams,
+ abortController?: AbortController
+ ) => Promise<{ cipherId: string; userVerified: boolean }>;
confirmNewCredential: (
params: NewCredentialParams,
abortController?: AbortController
diff --git a/libs/common/src/fido2/services/fido2-authenticator.service.spec.ts b/libs/common/src/fido2/services/fido2-authenticator.service.spec.ts
index 89a32e4ee3..c36e052ca5 100644
--- a/libs/common/src/fido2/services/fido2-authenticator.service.spec.ts
+++ b/libs/common/src/fido2/services/fido2-authenticator.service.spec.ts
@@ -716,18 +716,30 @@ describe("FidoAuthenticatorService", () => {
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 () => {
- userInterfaceSession.pickCredential.mockResolvedValue(ciphers[0].id);
+ for (const userVerification of [true, false]) {
+ /** Spec: Prompt the user to select a public key credential source selectedCredential from credentialOptions. */
+ it(`should request confirmation from user when user verification is ${userVerification}`, async () => {
+ params.requireUserVerification = userVerification;
+ userInterfaceSession.pickCredential.mockResolvedValue({
+ cipherId: ciphers[0].id,
+ userVerified: userVerification,
+ });
- await authenticator.getAssertion(params);
+ await authenticator.getAssertion(params);
- expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith(ciphers.map((c) => c.id));
- });
+ expect(userInterfaceSession.pickCredential).toHaveBeenCalledWith({
+ cipherIds: ciphers.map((c) => c.id),
+ userVerification,
+ });
+ });
+ }
/** Spec: If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation. */
it("should throw error", async () => {
- userInterfaceSession.pickCredential.mockResolvedValue(undefined);
+ userInterfaceSession.pickCredential.mockResolvedValue({
+ cipherId: undefined,
+ userVerified: false,
+ });
const result = async () => await authenticator.getAssertion(params);
@@ -783,7 +795,10 @@ describe("FidoAuthenticatorService", () => {
});
}
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
- userInterfaceSession.pickCredential.mockResolvedValue(ciphers[0].id);
+ userInterfaceSession.pickCredential.mockResolvedValue({
+ cipherId: ciphers[0].id,
+ userVerified: false,
+ });
};
beforeEach(init);
diff --git a/libs/common/src/fido2/services/fido2-authenticator.service.ts b/libs/common/src/fido2/services/fido2-authenticator.service.ts
index 51a30e21a6..b5b071caf7 100644
--- a/libs/common/src/fido2/services/fido2-authenticator.service.ts
+++ b/libs/common/src/fido2/services/fido2-authenticator.service.ts
@@ -202,15 +202,22 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
- const selectedCipherId = await userInterfaceSession.pickCredential(
- cipherOptions.map((cipher) => cipher.id)
- );
+ const response = await userInterfaceSession.pickCredential({
+ cipherIds: cipherOptions.map((cipher) => cipher.id),
+ userVerification: params.requireUserVerification,
+ });
+ const selectedCipherId = response.cipherId;
+ const userVerified = response.userVerified;
const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId);
if (selectedCipher === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
+ if (params.requireUserVerification && !userVerified) {
+ throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
+ }
+
try {
const selectedFido2Key =
selectedCipher.type === CipherType.Login
@@ -235,7 +242,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
credentialId: Utils.guidToRawFormat(selectedCredentialId),
counter: selectedFido2Key.counter,
userPresence: true,
- userVerification: false,
+ userVerification: userVerified,
});
const signature = await generateSignature({