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

[EC-598] feat: add support for user verifiction using MP during assertion

This commit is contained in:
Andreas Coroiu
2023-04-20 16:34:53 +02:00
parent 757050430d
commit 3a1b56860e
6 changed files with 60 additions and 60 deletions

View File

@@ -1,17 +1,6 @@
<ng-container *ngIf="data$ | async as data"> <ng-container *ngIf="data$ | async as data">
<div class="auth-wrapper"> <div class="auth-wrapper">
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i> <i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!loading" aria-hidden="true"></i>
<ng-container *ngIf="data.type == 'ConfirmCredentialRequest'">
A site is asking for authentication using the following credential:
<div class="box list">
<div class="box-content">
<app-cipher-row [cipher]="ciphers[0]"></app-cipher-row>
</div>
</div>
<button type="button" class="btn btn-outline-secondary" (click)="confirm()">
Authenticate
</button>
</ng-container>
<ng-container <ng-container
*ngIf=" *ngIf="
data.type == 'PickCredentialRequest' || data.type == 'PickCredentialRequest' ||
@@ -36,7 +25,7 @@
<app-cipher-row [cipher]="ciphers[0]"></app-cipher-row> <app-cipher-row [cipher]="ciphers[0]"></app-cipher-row>
</div> </div>
</div> </div>
<button type="button" class="btn btn-outline-secondary" (click)="confirmNew()">Create</button> <button type="button" class="btn btn-outline-secondary" (click)="confirm()">Create</button>
</ng-container> </ng-container>
<ng-container *ngIf="data.type == 'InformExcludedCredentialRequest'"> <ng-container *ngIf="data.type == 'InformExcludedCredentialRequest'">
A passkey already exists in Bitwarden for this account A passkey already exists in Bitwarden for this account

View File

@@ -77,9 +77,6 @@ export class Fido2Component implements OnInit, OnDestroy {
cipher.fido2Key = new Fido2KeyView(); cipher.fido2Key = new Fido2KeyView();
cipher.fido2Key.userName = data.userName; cipher.fido2Key.userName = data.userName;
this.ciphers = [cipher]; 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") { } else if (data?.type === "PickCredentialRequest") {
this.ciphers = await Promise.all( this.ciphers = await Promise.all(
data.cipherIds.map(async (cipherId) => { data.cipherIds.map(async (cipherId) => {
@@ -117,10 +114,16 @@ export class Fido2Component implements OnInit, OnDestroy {
async pick(cipher: CipherView) { async pick(cipher: CipherView) {
const data = this.data$.value; const data = this.data$.value;
if (data?.type === "PickCredentialRequest") { if (data?.type === "PickCredentialRequest") {
let userVerified = false;
if (data.userVerification) {
userVerified = await this.passwordRepromptService.showPasswordPrompt();
}
this.send({ this.send({
sessionId: this.sessionId, sessionId: this.sessionId,
cipherId: cipher.id, cipherId: cipher.id,
type: "PickCredentialResponse", type: "PickCredentialResponse",
userVerified,
}); });
} else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") { } else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
let userVerified = false; let userVerified = false;
@@ -139,15 +142,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.loading = true; this.loading = true;
} }
confirm() { async confirm() {
this.send({
sessionId: this.sessionId,
type: "ConfirmCredentialResponse",
});
this.loading = true;
}
async confirmNew() {
const data = this.data$.value; const data = this.data$.value;
if (data.type !== "ConfirmNewCredentialRequest") { if (data.type !== "ConfirmNewCredentialRequest") {
return; return;

View File

@@ -15,6 +15,7 @@ import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
Fido2UserInterfaceSession, Fido2UserInterfaceSession,
NewCredentialParams, NewCredentialParams,
PickCredentialParams,
} from "@bitwarden/common/fido2/abstractions/fido2-user-interface.service.abstraction"; } from "@bitwarden/common/fido2/abstractions/fido2-user-interface.service.abstraction";
import { Utils } from "@bitwarden/common/misc/utils"; import { Utils } from "@bitwarden/common/misc/utils";
@@ -46,17 +47,12 @@ export type BrowserFido2Message = { sessionId: string } & (
| { | {
type: "PickCredentialRequest"; type: "PickCredentialRequest";
cipherIds: string[]; cipherIds: string[];
userVerification: boolean;
} }
| { | {
type: "PickCredentialResponse"; type: "PickCredentialResponse";
cipherId?: string; cipherId?: string;
} userVerified: boolean;
| {
type: "ConfirmCredentialRequest";
cipherId: string;
}
| {
type: "ConfirmCredentialResponse";
} }
| { | {
type: "ConfirmNewCredentialRequest"; type: "ConfirmNewCredentialRequest";
@@ -179,30 +175,21 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
return this.abortController.signal.aborted; return this.abortController.signal.aborted;
} }
async confirmCredential(cipherId: string): Promise<boolean> { async pickCredential({
const data: BrowserFido2Message = { cipherIds,
type: "ConfirmCredentialRequest", userVerification,
cipherId, }: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
sessionId: this.sessionId,
};
await this.send(data);
await this.receive("ConfirmCredentialResponse");
return true;
}
async pickCredential(cipherIds: string[]): Promise<string> {
const data: BrowserFido2Message = { const data: BrowserFido2Message = {
type: "PickCredentialRequest", type: "PickCredentialRequest",
cipherIds, cipherIds,
sessionId: this.sessionId, sessionId: this.sessionId,
userVerification,
}; };
await this.send(data); await this.send(data);
const response = await this.receive("PickCredentialResponse"); const response = await this.receive("PickCredentialResponse");
return response.cipherId; return { cipherId: response.cipherId, userVerified: response.userVerified };
} }
async confirmNewCredential({ async confirmNewCredential({

View File

@@ -4,6 +4,11 @@ export interface NewCredentialParams {
userVerification: boolean; userVerification: boolean;
} }
export interface PickCredentialParams {
cipherIds: string[];
userVerification: boolean;
}
export abstract class Fido2UserInterfaceService { export abstract class Fido2UserInterfaceService {
newSession: (abortController?: AbortController) => Promise<Fido2UserInterfaceSession>; newSession: (abortController?: AbortController) => Promise<Fido2UserInterfaceSession>;
} }
@@ -12,8 +17,10 @@ export abstract class Fido2UserInterfaceSession {
fallbackRequested = false; fallbackRequested = false;
aborted = false; aborted = false;
confirmCredential: (cipherId: string, abortController?: AbortController) => Promise<boolean>; pickCredential: (
pickCredential: (cipherIds: string[], abortController?: AbortController) => Promise<string>; params: PickCredentialParams,
abortController?: AbortController
) => Promise<{ cipherId: string; userVerified: boolean }>;
confirmNewCredential: ( confirmNewCredential: (
params: NewCredentialParams, params: NewCredentialParams,
abortController?: AbortController abortController?: AbortController

View File

@@ -716,18 +716,30 @@ describe("FidoAuthenticatorService", () => {
cipherService.getAllDecrypted.mockResolvedValue(ciphers); cipherService.getAllDecrypted.mockResolvedValue(ciphers);
}); });
/** Spec: Prompt the user to select a public key credential source selectedCredential from credentialOptions. */ for (const userVerification of [true, false]) {
it("should request confirmation from the user", async () => { /** Spec: Prompt the user to select a public key credential source selectedCredential from credentialOptions. */
userInterfaceSession.pickCredential.mockResolvedValue(ciphers[0].id); 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. */ /** Spec: If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation. */
it("should throw error", async () => { it("should throw error", async () => {
userInterfaceSession.pickCredential.mockResolvedValue(undefined); userInterfaceSession.pickCredential.mockResolvedValue({
cipherId: undefined,
userVerified: false,
});
const result = async () => await authenticator.getAssertion(params); const result = async () => await authenticator.getAssertion(params);
@@ -783,7 +795,10 @@ describe("FidoAuthenticatorService", () => {
}); });
} }
cipherService.getAllDecrypted.mockResolvedValue(ciphers); cipherService.getAllDecrypted.mockResolvedValue(ciphers);
userInterfaceSession.pickCredential.mockResolvedValue(ciphers[0].id); userInterfaceSession.pickCredential.mockResolvedValue({
cipherId: ciphers[0].id,
userVerified: false,
});
}; };
beforeEach(init); beforeEach(init);

View File

@@ -202,15 +202,22 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }
const selectedCipherId = await userInterfaceSession.pickCredential( const response = await userInterfaceSession.pickCredential({
cipherOptions.map((cipher) => cipher.id) 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); const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId);
if (selectedCipher === undefined) { if (selectedCipher === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }
if (params.requireUserVerification && !userVerified) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
}
try { try {
const selectedFido2Key = const selectedFido2Key =
selectedCipher.type === CipherType.Login selectedCipher.type === CipherType.Login
@@ -235,7 +242,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
credentialId: Utils.guidToRawFormat(selectedCredentialId), credentialId: Utils.guidToRawFormat(selectedCredentialId),
counter: selectedFido2Key.counter, counter: selectedFido2Key.counter,
userPresence: true, userPresence: true,
userVerification: false, userVerification: userVerified,
}); });
const signature = await generateSignature({ const signature = await generateSignature({