1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-20 14:59:32 +01:00

[EC-598] feat: allow user to pick which credential to use

This commit is contained in:
Andreas Coroiu 2023-01-05 15:07:07 +01:00
parent f0b8d32ee6
commit 132c3fe04d
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
7 changed files with 101 additions and 81 deletions

View File

@ -3,7 +3,7 @@ import { DragDropModule } from "@angular/cdk/drag-drop";
import { LayoutModule } from "@angular/cdk/layout";
import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { CurrencyPipe, DatePipe, registerLocaleData } from "@angular/common";
import { CommonModule, CurrencyPipe, DatePipe, registerLocaleData } from "@angular/common";
import localeAr from "@angular/common/locales/ar";
import localeAz from "@angular/common/locales/az";
import localeBe from "@angular/common/locales/be";
@ -175,6 +175,7 @@ registerLocaleData(localeZhTw, "zh-TW");
@NgModule({
imports: [
CommonModule,
A11yModule,
AppRoutingModule,
BitwardenToastModule.forRoot({

View File

@ -1,20 +1,32 @@
<ng-container *ngIf="data">
<div class="auth-wrapper">
<ng-container *ngIf="data.type == 'VerifyUserRequest'">
A site is asking for authentication
</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">
<ng-container *ngIf="data.type == 'PickCredentialRequest'">
A site is asking for authentication, please choose one of the following credentials to use
<div class="box list">
<div class="box-content">
<app-cipher-row [cipher]="cipher"></app-cipher-row>
<app-cipher-row
*ngFor="let cipher of ciphers"
[cipher]="cipher"
(onSelected)="pick(cipher)"
></app-cipher-row>
</div>
</div>
A site wants to create a new passkey in your vault
<!-- <button type="button" class="btn btn-outline-secondary" (click)="accept()">
<ng-container *ngIf="data.type == 'VerifyUserRequest'">Authenticate</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">Create</ng-container>
</button> -->
</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">
A site wants to create the following passkey in your vault
<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()">
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">Create</ng-container>
</button>
</ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="accept()">
<ng-container *ngIf="data.type == 'VerifyUserRequest'">Authenticate</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">Create</ng-container>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel(true)">
Use browser built-in
</button>

View File

@ -1,7 +1,8 @@
import { Component, HostListener, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { Subject, takeUntil } from "rxjs";
import { concatMap, Subject, takeUntil } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { Fido2KeyView } from "@bitwarden/common/models/view/fido2-key.view";
@ -20,44 +21,51 @@ export class Fido2Component implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
protected data?: BrowserFido2Message;
protected cipher?: CipherView;
protected ciphers?: CipherView[] = [];
constructor(private activatedRoute: ActivatedRoute) {}
constructor(private activatedRoute: ActivatedRoute, private cipherService: CipherService) {}
ngOnInit(): void {
this.activatedRoute.queryParamMap.pipe(takeUntil(this.destroy$)).subscribe((queryParamMap) => {
this.data = JSON.parse(queryParamMap.get("data"));
this.activatedRoute.queryParamMap
.pipe(
concatMap(async (queryParamMap) => {
this.data = JSON.parse(queryParamMap.get("data"));
if (this.data?.type === "ConfirmNewCredentialRequest") {
this.cipher = new CipherView();
this.cipher.name = this.data.name;
this.cipher.type = CipherType.Fido2Key;
this.cipher.fido2Key = new Fido2KeyView();
}
});
if (this.data?.type === "ConfirmNewCredentialRequest") {
const cipher = new CipherView();
cipher.name = this.data.name;
cipher.type = CipherType.Fido2Key;
cipher.fido2Key = new Fido2KeyView();
this.ciphers = [cipher];
} else if (this.data?.type === "PickCredentialRequest") {
this.ciphers = await Promise.all(
this.data.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId);
return cipher.decrypt();
})
);
}
}),
takeUntil(this.destroy$)
)
.subscribe();
}
async accept() {
const data = this.data;
async pick(cipher: CipherView) {
BrowserFido2UserInterfaceService.sendMessage({
requestId: this.data.requestId,
cipherId: cipher.id,
type: "PickCredentialResponse",
});
if (data.type === "VerifyUserRequest") {
BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId,
type: "VerifyUserResponse",
});
} else if (data.type === "ConfirmNewCredentialRequest") {
BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId,
type: "ConfirmNewCredentialResponse",
});
} else {
BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId,
type: "RequestCancelled",
fallbackRequested: true,
});
}
window.close();
}
confirm() {
BrowserFido2UserInterfaceService.sendMessage({
requestId: this.data.requestId,
type: "ConfirmNewCredentialResponse",
});
window.close();
}

View File

@ -14,10 +14,12 @@ const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
export type BrowserFido2Message = { requestId: string } & (
| {
type: "VerifyUserRequest";
type: "PickCredentialRequest";
cipherIds: string[];
}
| {
type: "VerifyUserResponse";
type: "PickCredentialResponse";
cipherId?: string;
}
| {
type: "ConfirmNewCredentialRequest";
@ -48,13 +50,9 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
BrowserApi.messageListener(BrowserFido2MessageName, this.processMessage.bind(this));
}
async verifyUser(): Promise<boolean> {
return false;
}
async verifyPresence(): Promise<boolean> {
async pickCredential(cipherIds: string[]): Promise<string> {
const requestId = Utils.newGuid();
const data: BrowserFido2Message = { type: "VerifyUserRequest", requestId };
const data: BrowserFido2Message = { type: "PickCredentialRequest", cipherIds, requestId };
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
this.popupUtilsService.popOut(
null,
@ -70,15 +68,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
)
);
if (response.type === "VerifyUserResponse") {
return true;
}
if (response.type === "RequestCancelled") {
throw new RequestAbortedError(response.fallbackRequested);
}
return false;
if (response.type !== "PickCredentialResponse") {
throw new RequestAbortedError();
}
return response.cipherId;
}
async confirmNewCredential({ name }: NewCredentialParams): Promise<boolean> {

View File

@ -3,7 +3,6 @@ export interface NewCredentialParams {
}
export abstract class Fido2UserInterfaceService {
verifyUser: () => Promise<boolean>;
verifyPresence: () => Promise<boolean>;
pickCredential: (cipherIds: string[]) => Promise<string>;
confirmNewCredential: (params: NewCredentialParams) => Promise<boolean>;
}

View File

@ -120,21 +120,30 @@ export class Fido2Service implements Fido2ServiceAbstraction {
if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) {
// We're looking for regular non-resident keys
credential = await this.getCredential(params.allowedCredentialIds);
if (credential === undefined) {
throw new NoCredentialFoundError();
}
if (credential.origin !== params.origin) {
throw new OriginMismatchError();
}
await this.fido2UserInterfaceService.pickCredential([credential.credentialId.encoded]);
} else {
// We're looking for a resident key
credential = await this.getCredentialByRp(params.rpId);
}
const credentials = await this.getCredentialsByRp(params.rpId);
if (credential === undefined) {
throw new NoCredentialFoundError();
}
if (credentials.length === 0) {
throw new NoCredentialFoundError();
}
if (credential.origin !== params.origin) {
throw new OriginMismatchError();
const pickedId = await this.fido2UserInterfaceService.pickCredential(
credentials.map((c) => c.credentialId.encoded)
);
credential = credentials.find((c) => c.credentialId.encoded === pickedId);
}
const presence = await this.fido2UserInterfaceService.verifyPresence();
const encoder = new TextEncoder();
const clientData = encoder.encode(
JSON.stringify({
@ -147,7 +156,7 @@ export class Fido2Service implements Fido2ServiceAbstraction {
const authData = await generateAuthData({
credentialId: credential.credentialId,
rpId: params.rpId,
userPresence: presence,
userPresence: true,
userVerification: true, // TODO: Change to false!
});
@ -171,7 +180,7 @@ export class Fido2Service implements Fido2ServiceAbstraction {
for (const allowedCredential of allowedCredentialIds) {
cipher = await this.cipherService.get(allowedCredential);
if (cipher.deletedDate != undefined) {
if (cipher?.deletedDate != undefined) {
cipher = undefined;
}
@ -209,17 +218,13 @@ export class Fido2Service implements Fido2ServiceAbstraction {
return new CredentialId(cipher.id);
}
private async getCredentialByRp(rpId: string): Promise<BitCredential | undefined> {
private async getCredentialsByRp(rpId: string): Promise<BitCredential[]> {
const allCipherViews = await this.cipherService.getAllDecrypted();
const cipherView = allCipherViews.find(
const cipherViews = allCipherViews.filter(
(cv) => !cv.isDeleted && cv.type === CipherType.Fido2Key && cv.fido2Key?.rpId === rpId
);
if (cipherView == undefined) {
return undefined;
}
return await mapCipherViewToBitCredential(cipherView);
return await Promise.all(cipherViews.map((view) => mapCipherViewToBitCredential(view)));
}
}

View File

@ -1,12 +1,9 @@
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
import { RequestAbortedError } from "../../abstractions/fido2/fido2.service.abstraction";
export class Fido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
async verifyUser(): Promise<boolean> {
return false;
}
async verifyPresence(): Promise<boolean> {
return false;
pickCredential(cipherIds: string[]): Promise<string> {
throw new RequestAbortedError();
}
async confirmNewCredential(): Promise<boolean> {