mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 00:33:44 +00:00
[EC-598] feat: fully refactored user interface
Now uses sessions instead of single request-response style communcation
This commit is contained in:
@@ -1,6 +1,17 @@
|
|||||||
import { filter, first, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
EmptyError,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
fromEvent,
|
||||||
|
Observable,
|
||||||
|
Subject,
|
||||||
|
take,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
import { UserRequestedFallbackAbortReason } from "@bitwarden/common/webauthn/abstractions/fido2-client.service.abstraction";
|
||||||
import {
|
import {
|
||||||
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
|
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
|
||||||
Fido2UserInterfaceSession,
|
Fido2UserInterfaceSession,
|
||||||
@@ -12,19 +23,26 @@ import { PopupUtilsService } from "../../popup/services/popup-utils.service";
|
|||||||
|
|
||||||
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
|
const BrowserFido2MessageName = "BrowserFido2UserInterfaceServiceMessage";
|
||||||
|
|
||||||
export class Fido2Error extends Error {
|
export class SessionClosedError extends Error {
|
||||||
constructor(message: string, readonly fallbackRequested = false) {
|
constructor() {
|
||||||
super(message);
|
super("Fido2UserInterfaceSession was closed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RequestAbortedError extends Fido2Error {
|
export type BrowserFido2Message = { sessionId: string } & (
|
||||||
constructor(fallbackRequested = false) {
|
| /**
|
||||||
super("Fido2 request was aborted", fallbackRequested);
|
* This message is used by popouts to announce that they are ready
|
||||||
|
* to recieve messages.
|
||||||
|
**/ {
|
||||||
|
type: "ConnectResponse";
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This message is used to announce the creation of a new session.
|
||||||
|
* It iss used by popouts to know when to close.
|
||||||
|
**/
|
||||||
|
| {
|
||||||
|
type: "NewSessionCreatedRequest";
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BrowserFido2Message = { requestId: string } & (
|
|
||||||
| {
|
| {
|
||||||
type: "PickCredentialRequest";
|
type: "PickCredentialRequest";
|
||||||
cipherIds: string[];
|
cipherIds: string[];
|
||||||
@@ -66,228 +84,77 @@ export type BrowserFido2Message = { requestId: string } & (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface BrowserFido2UserInterfaceRequestData {
|
|
||||||
requestId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServiceAbstraction {
|
||||||
static sendMessage(msg: BrowserFido2Message) {
|
|
||||||
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static onAbort$(requestId: string): Observable<BrowserFido2Message> {
|
|
||||||
const messages$ = BrowserApi.messageListener$() as Observable<BrowserFido2Message>;
|
|
||||||
return messages$.pipe(
|
|
||||||
filter((message) => message.type === "AbortRequest" && message.requestId === requestId),
|
|
||||||
first()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private messages$ = BrowserApi.messageListener$() as Observable<BrowserFido2Message>;
|
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
|
|
||||||
constructor(private popupUtilsService: PopupUtilsService) {}
|
constructor(private popupUtilsService: PopupUtilsService) {}
|
||||||
|
|
||||||
async newSession(abortController?: AbortController): Promise<Fido2UserInterfaceSession> {
|
async newSession(abortController?: AbortController): Promise<Fido2UserInterfaceSession> {
|
||||||
return await BrowserFido2UserInterfaceSession.create(this, abortController);
|
return await BrowserFido2UserInterfaceSession.create(this.popupUtilsService, abortController);
|
||||||
}
|
|
||||||
|
|
||||||
async confirmCredential(
|
|
||||||
cipherId: string,
|
|
||||||
abortController = new AbortController()
|
|
||||||
): Promise<boolean> {
|
|
||||||
const requestId = Utils.newGuid();
|
|
||||||
const data: BrowserFido2Message = { type: "ConfirmCredentialRequest", cipherId, requestId };
|
|
||||||
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
|
|
||||||
|
|
||||||
const abortHandler = () =>
|
|
||||||
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
|
|
||||||
abortController.signal.addEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
this.popupUtilsService.popOut(
|
|
||||||
null,
|
|
||||||
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
|
||||||
{ center: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await lastValueFrom(
|
|
||||||
this.messages$.pipe(
|
|
||||||
filter((msg) => msg.requestId === requestId),
|
|
||||||
first(),
|
|
||||||
takeUntil(this.destroy$)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.type === "ConfirmCredentialResponse") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.type === "AbortResponse") {
|
|
||||||
throw new RequestAbortedError(response.fallbackRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
abortController.signal.removeEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async pickCredential(
|
|
||||||
cipherIds: string[],
|
|
||||||
abortController = new AbortController()
|
|
||||||
): Promise<string> {
|
|
||||||
const requestId = Utils.newGuid();
|
|
||||||
const data: BrowserFido2Message = { type: "PickCredentialRequest", cipherIds, requestId };
|
|
||||||
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
|
|
||||||
|
|
||||||
const abortHandler = () =>
|
|
||||||
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
|
|
||||||
abortController.signal.addEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
this.popupUtilsService.popOut(
|
|
||||||
null,
|
|
||||||
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
|
||||||
{ center: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await lastValueFrom(
|
|
||||||
this.messages$.pipe(
|
|
||||||
filter((msg) => msg.requestId === requestId),
|
|
||||||
first(),
|
|
||||||
takeUntil(this.destroy$)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.type === "AbortResponse") {
|
|
||||||
throw new RequestAbortedError(response.fallbackRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.type !== "PickCredentialResponse") {
|
|
||||||
throw new RequestAbortedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
abortController.signal.removeEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
return response.cipherId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirmNewCredential(
|
|
||||||
{ credentialName, userName }: NewCredentialParams,
|
|
||||||
abortController = new AbortController()
|
|
||||||
): Promise<boolean> {
|
|
||||||
const requestId = Utils.newGuid();
|
|
||||||
const data: BrowserFido2Message = {
|
|
||||||
type: "ConfirmNewCredentialRequest",
|
|
||||||
requestId,
|
|
||||||
credentialName,
|
|
||||||
userName,
|
|
||||||
};
|
|
||||||
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
|
|
||||||
|
|
||||||
const abortHandler = () =>
|
|
||||||
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
|
|
||||||
abortController.signal.addEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
this.popupUtilsService.popOut(
|
|
||||||
null,
|
|
||||||
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
|
||||||
{ center: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await lastValueFrom(
|
|
||||||
this.messages$.pipe(
|
|
||||||
filter((msg) => msg.requestId === requestId),
|
|
||||||
first(),
|
|
||||||
takeUntil(this.destroy$)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.type === "ConfirmNewCredentialResponse") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.type === "AbortResponse") {
|
|
||||||
throw new RequestAbortedError(response.fallbackRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
abortController.signal.removeEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async confirmNewNonDiscoverableCredential(
|
|
||||||
{ credentialName, userName }: NewCredentialParams,
|
|
||||||
abortController?: AbortController
|
|
||||||
): Promise<string> {
|
|
||||||
const requestId = Utils.newGuid();
|
|
||||||
const data: BrowserFido2Message = {
|
|
||||||
type: "ConfirmNewNonDiscoverableCredentialRequest",
|
|
||||||
requestId,
|
|
||||||
credentialName,
|
|
||||||
userName,
|
|
||||||
};
|
|
||||||
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
|
|
||||||
|
|
||||||
const abortHandler = () =>
|
|
||||||
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
|
|
||||||
abortController.signal.addEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
this.popupUtilsService.popOut(
|
|
||||||
null,
|
|
||||||
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
|
||||||
{ center: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await lastValueFrom(
|
|
||||||
this.messages$.pipe(
|
|
||||||
filter((msg) => msg.requestId === requestId),
|
|
||||||
first(),
|
|
||||||
takeUntil(this.destroy$)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.type === "ConfirmNewNonDiscoverableCredentialResponse") {
|
|
||||||
return response.cipherId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.type === "AbortResponse") {
|
|
||||||
throw new RequestAbortedError(response.fallbackRequested);
|
|
||||||
}
|
|
||||||
|
|
||||||
abortController.signal.removeEventListener("abort", abortHandler);
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async informExcludedCredential(
|
|
||||||
existingCipherIds: string[],
|
|
||||||
newCredential: NewCredentialParams,
|
|
||||||
abortController?: AbortController
|
|
||||||
): Promise<void> {
|
|
||||||
// Not Implemented
|
|
||||||
}
|
|
||||||
|
|
||||||
private setAbortTimeout(abortController: AbortController) {
|
|
||||||
return setTimeout(() => abortController.abort());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSession {
|
||||||
static async create(
|
static async create(
|
||||||
parentService: BrowserFido2UserInterfaceService,
|
popupUtilsService: PopupUtilsService,
|
||||||
abortController?: AbortController
|
abortController?: AbortController
|
||||||
): Promise<BrowserFido2UserInterfaceSession> {
|
): Promise<BrowserFido2UserInterfaceSession> {
|
||||||
return new BrowserFido2UserInterfaceSession(parentService, abortController);
|
return new BrowserFido2UserInterfaceSession(popupUtilsService, abortController);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly abortListener: () => void;
|
static sendMessage(msg: BrowserFido2Message) {
|
||||||
|
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private closed = false;
|
||||||
|
private messages$ = (BrowserApi.messageListener$() as Observable<BrowserFido2Message>).pipe(
|
||||||
|
filter((msg) => msg.sessionId === this.sessionId)
|
||||||
|
);
|
||||||
|
private connected$ = new BehaviorSubject(false);
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
private readonly parentService: BrowserFido2UserInterfaceService,
|
private readonly popupUtilsService: PopupUtilsService,
|
||||||
readonly abortController = new AbortController(),
|
readonly abortController = new AbortController(),
|
||||||
readonly sessionId = Utils.newGuid()
|
readonly sessionId = Utils.newGuid()
|
||||||
) {
|
) {
|
||||||
this.abortListener = () => this.abort();
|
this.messages$
|
||||||
abortController.signal.addEventListener("abort", this.abortListener);
|
.pipe(
|
||||||
|
filter((msg) => msg.type === "ConnectResponse"),
|
||||||
|
take(1),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
this.connected$.next(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle session aborted by RP
|
||||||
|
fromEvent(abortController.signal, "abort")
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.close();
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
|
type: "AbortRequest",
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle session aborted by user
|
||||||
|
this.messages$
|
||||||
|
.pipe(
|
||||||
|
filter((msg) => msg.type === "AbortResponse"),
|
||||||
|
take(1),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe((msg) => {
|
||||||
|
if (msg.type === "AbortResponse") {
|
||||||
|
this.close();
|
||||||
|
this.abortController.abort(UserRequestedFallbackAbortReason);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
|
type: "NewSessionCreatedRequest",
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackRequested = false;
|
fallbackRequested = false;
|
||||||
@@ -296,26 +163,61 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
return this.abortController.signal.aborted;
|
return this.abortController.signal.aborted;
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmCredential(cipherId: string, abortController?: AbortController): Promise<boolean> {
|
async confirmCredential(cipherId: string): Promise<boolean> {
|
||||||
return this.parentService.confirmCredential(cipherId, this.abortController);
|
const data: BrowserFido2Message = {
|
||||||
|
type: "ConfirmCredentialRequest",
|
||||||
|
cipherId,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.send(data);
|
||||||
|
await this.receive("ConfirmCredentialResponse");
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pickCredential(cipherIds: string[], abortController?: AbortController): Promise<string> {
|
async pickCredential(cipherIds: string[]): Promise<string> {
|
||||||
return this.parentService.pickCredential(cipherIds, this.abortController);
|
const data: BrowserFido2Message = {
|
||||||
|
type: "PickCredentialRequest",
|
||||||
|
cipherIds,
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.send(data);
|
||||||
|
const response = await this.receive("PickCredentialResponse");
|
||||||
|
|
||||||
|
return response.cipherId;
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmNewCredential(
|
async confirmNewCredential({ credentialName, userName }: NewCredentialParams): Promise<boolean> {
|
||||||
params: NewCredentialParams,
|
const data: BrowserFido2Message = {
|
||||||
abortController?: AbortController
|
type: "ConfirmNewCredentialRequest",
|
||||||
): Promise<boolean> {
|
sessionId: this.sessionId,
|
||||||
return this.parentService.confirmNewCredential(params, this.abortController);
|
credentialName,
|
||||||
|
userName,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.send(data);
|
||||||
|
await this.receive("ConfirmNewCredentialResponse");
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmNewNonDiscoverableCredential(
|
async confirmNewNonDiscoverableCredential({
|
||||||
params: NewCredentialParams,
|
credentialName,
|
||||||
abortController?: AbortController
|
userName,
|
||||||
): Promise<string> {
|
}: NewCredentialParams): Promise<string> {
|
||||||
return this.parentService.confirmNewNonDiscoverableCredential(params, this.abortController);
|
const data: BrowserFido2Message = {
|
||||||
|
type: "ConfirmNewNonDiscoverableCredentialRequest",
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
credentialName,
|
||||||
|
userName,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.send(data);
|
||||||
|
const response = await this.receive("ConfirmNewNonDiscoverableCredentialResponse");
|
||||||
|
|
||||||
|
return response.cipherId;
|
||||||
}
|
}
|
||||||
|
|
||||||
informExcludedCredential(
|
informExcludedCredential(
|
||||||
@@ -323,18 +225,52 @@ export class BrowserFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
newCredential: NewCredentialParams,
|
newCredential: NewCredentialParams,
|
||||||
abortController?: AbortController
|
abortController?: AbortController
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.parentService.informExcludedCredential(
|
return null;
|
||||||
existingCipherIds,
|
|
||||||
newCredential,
|
|
||||||
this.abortController
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private abort() {
|
private async send(msg: BrowserFido2Message): Promise<void> {
|
||||||
this.close();
|
if (!this.connected$.value) {
|
||||||
|
await this.connect();
|
||||||
|
}
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async receive<T extends BrowserFido2Message["type"]>(
|
||||||
|
type: T
|
||||||
|
): Promise<BrowserFido2Message & { type: T }> {
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.messages$.pipe(
|
||||||
|
filter((msg) => msg.sessionId === this.sessionId && msg.type === type),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return response as BrowserFido2Message & { type: T };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof EmptyError) {
|
||||||
|
throw new SessionClosedError();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async connect(): Promise<void> {
|
||||||
|
if (this.closed) {
|
||||||
|
throw new Error("Cannot re-open closed session");
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams({ sessionId: this.sessionId }).toString();
|
||||||
|
this.popupUtilsService.popOut(
|
||||||
|
null,
|
||||||
|
`popup/index.html?uilocation=popout#/fido2?${queryParams}`,
|
||||||
|
{ center: true }
|
||||||
|
);
|
||||||
|
await firstValueFrom(this.connected$.pipe(filter((connected) => connected === true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private close() {
|
private close() {
|
||||||
this.abortController.signal.removeEventListener("abort", this.abortListener);
|
this.closed = true;
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<ng-container *ngIf="data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<div class="auth-wrapper">
|
<div class="auth-wrapper">
|
||||||
<ng-container *ngIf="data.type == 'ConfirmCredentialRequest'">
|
<ng-container *ngIf="data.type == 'ConfirmCredentialRequest'">
|
||||||
A site is asking for authentication using the following credential:
|
A site is asking for authentication using the following credential:
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="confirmNew()">Create</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="confirmNew()">Create</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel(true)">
|
<button type="button" class="btn btn-outline-secondary" (click)="abort(true)">
|
||||||
Use browser built-in
|
Use browser built-in
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel(false)">Abort</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="abort(false)">Abort</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
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 { concatMap, Subject, switchMap, takeUntil } from "rxjs";
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
Subject,
|
||||||
|
take,
|
||||||
|
takeUntil,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { Fido2KeyView } from "@bitwarden/common/webauthn/models/view/fido2-key.view";
|
import { Fido2KeyView } from "@bitwarden/common/webauthn/models/view/fido2-key.view";
|
||||||
|
|
||||||
|
import { BrowserApi } from "../../../browser/browserApi";
|
||||||
import {
|
import {
|
||||||
BrowserFido2Message,
|
BrowserFido2Message,
|
||||||
BrowserFido2UserInterfaceService,
|
BrowserFido2UserInterfaceSession,
|
||||||
} from "../../../services/fido2/browser-fido2-user-interface.service";
|
} from "../../../services/fido2/browser-fido2-user-interface.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -20,35 +30,58 @@ import {
|
|||||||
export class Fido2Component implements OnInit, OnDestroy {
|
export class Fido2Component implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
protected data?: BrowserFido2Message;
|
protected data$ = new BehaviorSubject<BrowserFido2Message>(null);
|
||||||
|
protected sessionId?: string;
|
||||||
protected ciphers?: CipherView[] = [];
|
protected ciphers?: CipherView[] = [];
|
||||||
|
|
||||||
constructor(private activatedRoute: ActivatedRoute, private cipherService: CipherService) {}
|
constructor(private activatedRoute: ActivatedRoute, private cipherService: CipherService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.activatedRoute.queryParamMap
|
const sessionId$ = this.activatedRoute.queryParamMap.pipe(
|
||||||
.pipe(
|
take(1),
|
||||||
concatMap(async (queryParamMap) => {
|
map((queryParamMap) => queryParamMap.get("sessionId"))
|
||||||
this.data = JSON.parse(queryParamMap.get("data"));
|
);
|
||||||
|
|
||||||
if (this.data?.type === "ConfirmNewCredentialRequest") {
|
combineLatest([sessionId$, BrowserApi.messageListener$() as Observable<BrowserFido2Message>])
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(([sessionId, message]) => {
|
||||||
|
this.sessionId = sessionId;
|
||||||
|
if (message.type === "NewSessionCreatedRequest" && message.sessionId !== sessionId) {
|
||||||
|
return this.abort(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.sessionId !== sessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "AbortRequest") {
|
||||||
|
return this.abort(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data$.next(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.data$
|
||||||
|
.pipe(
|
||||||
|
concatMap(async (data) => {
|
||||||
|
if (data?.type === "ConfirmNewCredentialRequest") {
|
||||||
const cipher = new CipherView();
|
const cipher = new CipherView();
|
||||||
cipher.name = this.data.credentialName;
|
cipher.name = data.credentialName;
|
||||||
cipher.type = CipherType.Fido2Key;
|
cipher.type = CipherType.Fido2Key;
|
||||||
cipher.fido2Key = new Fido2KeyView();
|
cipher.fido2Key = new Fido2KeyView();
|
||||||
cipher.fido2Key.userName = this.data.userName;
|
cipher.fido2Key.userName = data.userName;
|
||||||
this.ciphers = [cipher];
|
this.ciphers = [cipher];
|
||||||
} else if (this.data?.type === "ConfirmCredentialRequest") {
|
} else if (data?.type === "ConfirmCredentialRequest") {
|
||||||
const cipher = await this.cipherService.get(this.data.cipherId);
|
const cipher = await this.cipherService.get(data.cipherId);
|
||||||
this.ciphers = [await cipher.decrypt()];
|
this.ciphers = [await cipher.decrypt()];
|
||||||
} else if (this.data?.type === "PickCredentialRequest") {
|
} else if (data?.type === "PickCredentialRequest") {
|
||||||
this.ciphers = await Promise.all(
|
this.ciphers = await Promise.all(
|
||||||
this.data.cipherIds.map(async (cipherId) => {
|
data.cipherIds.map(async (cipherId) => {
|
||||||
const cipher = await this.cipherService.get(cipherId);
|
const cipher = await this.cipherService.get(cipherId);
|
||||||
return cipher.decrypt();
|
return cipher.decrypt();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
} else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
||||||
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
|
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted
|
||||||
);
|
);
|
||||||
@@ -58,27 +91,25 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
this.activatedRoute.queryParamMap
|
sessionId$.pipe(takeUntil(this.destroy$)).subscribe((sessionId) => {
|
||||||
.pipe(
|
this.send({
|
||||||
switchMap((queryParamMap) => {
|
sessionId: sessionId,
|
||||||
const data = JSON.parse(queryParamMap.get("data"));
|
type: "ConnectResponse",
|
||||||
return BrowserFido2UserInterfaceService.onAbort$(data.requestId);
|
});
|
||||||
}),
|
});
|
||||||
takeUntil(this.destroy$)
|
|
||||||
)
|
|
||||||
.subscribe(() => this.cancel(false));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async pick(cipher: CipherView) {
|
async pick(cipher: CipherView) {
|
||||||
if (this.data?.type === "PickCredentialRequest") {
|
const data = this.data$.value;
|
||||||
BrowserFido2UserInterfaceService.sendMessage({
|
if (data?.type === "PickCredentialRequest") {
|
||||||
requestId: this.data.requestId,
|
this.send({
|
||||||
|
sessionId: this.sessionId,
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
type: "PickCredentialResponse",
|
type: "PickCredentialResponse",
|
||||||
});
|
});
|
||||||
} else if (this.data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
} else if (data?.type === "ConfirmNewNonDiscoverableCredentialRequest") {
|
||||||
BrowserFido2UserInterfaceService.sendMessage({
|
this.send({
|
||||||
requestId: this.data.requestId,
|
sessionId: this.sessionId,
|
||||||
cipherId: cipher.id,
|
cipherId: cipher.id,
|
||||||
type: "ConfirmNewNonDiscoverableCredentialResponse",
|
type: "ConfirmNewNonDiscoverableCredentialResponse",
|
||||||
});
|
});
|
||||||
@@ -88,31 +119,30 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
confirm() {
|
confirm() {
|
||||||
BrowserFido2UserInterfaceService.sendMessage({
|
this.send({
|
||||||
requestId: this.data.requestId,
|
sessionId: this.sessionId,
|
||||||
type: "ConfirmCredentialResponse",
|
type: "ConfirmCredentialResponse",
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
confirmNew() {
|
confirmNew() {
|
||||||
BrowserFido2UserInterfaceService.sendMessage({
|
this.send({
|
||||||
requestId: this.data.requestId,
|
sessionId: this.sessionId,
|
||||||
type: "ConfirmNewCredentialResponse",
|
type: "ConfirmNewCredentialResponse",
|
||||||
});
|
});
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(fallback: boolean) {
|
abort(fallback: boolean) {
|
||||||
this.unload(fallback);
|
this.unload(fallback);
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostListener("window:unload")
|
@HostListener("window:unload")
|
||||||
unload(fallback = true) {
|
unload(fallback = false) {
|
||||||
const data = this.data;
|
this.send({
|
||||||
BrowserFido2UserInterfaceService.sendMessage({
|
sessionId: this.sessionId,
|
||||||
requestId: data.requestId,
|
|
||||||
type: "AbortResponse",
|
type: "AbortResponse",
|
||||||
fallbackRequested: fallback,
|
fallbackRequested: fallback,
|
||||||
});
|
});
|
||||||
@@ -122,4 +152,11 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.destroy$.next();
|
this.destroy$.next();
|
||||||
this.destroy$.complete();
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private send(msg: BrowserFido2Message) {
|
||||||
|
BrowserFido2UserInterfaceSession.sendMessage({
|
||||||
|
sessionId: this.sessionId,
|
||||||
|
...msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const UserRequestedFallbackAbortReason = "UserRequestedFallback";
|
||||||
|
|
||||||
export type UserVerification = "discouraged" | "preferred" | "required";
|
export type UserVerification = "discouraged" | "preferred" | "required";
|
||||||
|
|
||||||
export abstract class Fido2ClientService {
|
export abstract class Fido2ClientService {
|
||||||
|
|||||||
Reference in New Issue
Block a user