mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
[EC-598] feat: allow user to pick which credential to use
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user