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:
parent
f0b8d32ee6
commit
132c3fe04d
@ -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({
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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> {
|
||||
|
Loading…
Reference in New Issue
Block a user