1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00: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
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 { LayoutModule } from "@angular/cdk/layout";
import { OverlayModule } from "@angular/cdk/overlay"; import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling"; 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 localeAr from "@angular/common/locales/ar";
import localeAz from "@angular/common/locales/az"; import localeAz from "@angular/common/locales/az";
import localeBe from "@angular/common/locales/be"; import localeBe from "@angular/common/locales/be";
@@ -175,6 +175,7 @@ registerLocaleData(localeZhTw, "zh-TW");
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule,
A11yModule, A11yModule,
AppRoutingModule, AppRoutingModule,
BitwardenToastModule.forRoot({ BitwardenToastModule.forRoot({

View File

@@ -1,20 +1,32 @@
<ng-container *ngIf="data"> <ng-container *ngIf="data">
<div class="auth-wrapper"> <div class="auth-wrapper">
<ng-container *ngIf="data.type == 'VerifyUserRequest'"> <ng-container *ngIf="data.type == 'PickCredentialRequest'">
A site is asking for authentication A site is asking for authentication, please choose one of the following credentials to use
</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">
<div class="box list"> <div class="box list">
<div class="box-content"> <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>
</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>
<button type="button" class="btn btn-outline-secondary" (click)="accept()">
<ng-container *ngIf="data.type == 'VerifyUserRequest'">Authenticate</ng-container> <ng-container *ngIf="data.type == 'VerifyUserRequest'">Authenticate</ng-container>
<ng-container *ngIf="data.type == 'ConfirmNewCredentialRequest'">Create</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> </button>
</ng-container>
<button type="button" class="btn btn-outline-secondary" (click)="cancel(true)"> <button type="button" class="btn btn-outline-secondary" (click)="cancel(true)">
Use browser built-in Use browser built-in
</button> </button>

View File

@@ -1,7 +1,8 @@
import { Component, HostListener, OnDestroy, OnInit } from "@angular/core"; import { Component, HostListener, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; 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 { CipherType } from "@bitwarden/common/enums/cipherType";
import { CipherView } from "@bitwarden/common/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/models/view/cipher.view";
import { Fido2KeyView } from "@bitwarden/common/models/view/fido2-key.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>(); private destroy$ = new Subject<void>();
protected data?: BrowserFido2Message; protected data?: BrowserFido2Message;
protected cipher?: CipherView; protected ciphers?: CipherView[] = [];
constructor(private activatedRoute: ActivatedRoute) {} constructor(private activatedRoute: ActivatedRoute, private cipherService: CipherService) {}
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.queryParamMap.pipe(takeUntil(this.destroy$)).subscribe((queryParamMap) => { this.activatedRoute.queryParamMap
.pipe(
concatMap(async (queryParamMap) => {
this.data = JSON.parse(queryParamMap.get("data")); this.data = JSON.parse(queryParamMap.get("data"));
if (this.data?.type === "ConfirmNewCredentialRequest") { if (this.data?.type === "ConfirmNewCredentialRequest") {
this.cipher = new CipherView(); const cipher = new CipherView();
this.cipher.name = this.data.name; cipher.name = this.data.name;
this.cipher.type = CipherType.Fido2Key; cipher.type = CipherType.Fido2Key;
this.cipher.fido2Key = new Fido2KeyView(); 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() { async pick(cipher: CipherView) {
const data = this.data;
if (data.type === "VerifyUserRequest") {
BrowserFido2UserInterfaceService.sendMessage({ BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId, requestId: this.data.requestId,
type: "VerifyUserResponse", cipherId: cipher.id,
type: "PickCredentialResponse",
}); });
} else if (data.type === "ConfirmNewCredentialRequest") {
window.close();
}
confirm() {
BrowserFido2UserInterfaceService.sendMessage({ BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId, requestId: this.data.requestId,
type: "ConfirmNewCredentialResponse", type: "ConfirmNewCredentialResponse",
}); });
} else {
BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId,
type: "RequestCancelled",
fallbackRequested: true,
});
}
window.close(); window.close();
} }

View File

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

View File

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

View File

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

View File

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