1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-20 21:01:29 +01:00

[EC-598] feat: ability to abort from page script

This commit is contained in:
Andreas Coroiu 2023-01-31 11:10:36 +01:00
parent 1ad0bc547a
commit c7e7ce832b
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
5 changed files with 101 additions and 30 deletions

View File

@ -1,3 +1,5 @@
import { Observable } from "rxjs";
import BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import { TabMessage } from "../types/tab-messages";
@ -146,6 +148,14 @@ export class BrowserApi {
);
}
static messageListener$() {
return new Observable<unknown>((subscriber) => {
const handler = (message: unknown) => subscriber.next(message);
chrome.runtime.onMessage.addListener(handler);
return () => chrome.runtime.onMessage.removeListener(handler);
});
}
static sendMessage(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
return chrome.runtime.sendMessage(message);

View File

@ -1,6 +1,6 @@
import { Component, HostListener, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { concatMap, Subject, takeUntil } from "rxjs";
import { concatMap, Subject, switchMap, takeUntil } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/enums/cipherType";
@ -53,6 +53,16 @@ export class Fido2Component implements OnInit, OnDestroy {
takeUntil(this.destroy$)
)
.subscribe();
this.activatedRoute.queryParamMap
.pipe(
switchMap((queryParamMap) => {
const data = JSON.parse(queryParamMap.get("data"));
return BrowserFido2UserInterfaceService.onAbort$(data.requestId);
}),
takeUntil(this.destroy$)
)
.subscribe(() => this.cancel(false));
}
async pick(cipher: CipherView) {
@ -91,7 +101,7 @@ export class Fido2Component implements OnInit, OnDestroy {
const data = this.data;
BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId,
type: "RequestCancelled",
type: "AbortResponse",
fallbackRequested: fallback,
});
}

View File

@ -1,4 +1,4 @@
import { filter, first, lastValueFrom, Subject, takeUntil } from "rxjs";
import { filter, first, lastValueFrom, Observable, Subject, takeUntil } from "rxjs";
import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
@ -37,7 +37,10 @@ export type BrowserFido2Message = { requestId: string } & (
type: "ConfirmNewCredentialResponse";
}
| {
type: "RequestCancelled";
type: "AbortRequest";
}
| {
type: "AbortResponse";
fallbackRequested: boolean;
}
);
@ -51,17 +54,31 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
BrowserApi.sendMessage(BrowserFido2MessageName, msg);
}
private messages$ = new Subject<BrowserFido2Message>();
private destroy$ = new Subject<void>();
constructor(private popupUtilsService: PopupUtilsService) {
BrowserApi.messageListener(BrowserFido2MessageName, this.processMessage.bind(this));
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()
);
}
async confirmCredential(cipherId: string): Promise<boolean> {
private messages$ = BrowserApi.messageListener$() as Observable<BrowserFido2Message>;
private destroy$ = new Subject<void>();
constructor(private popupUtilsService: PopupUtilsService) {}
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}`,
@ -80,17 +97,27 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
return true;
}
if (response.type === "RequestCancelled") {
if (response.type === "AbortResponse") {
throw new RequestAbortedError(response.fallbackRequested);
}
abortController.signal.removeEventListener("abort", abortHandler);
return false;
}
async pickCredential(cipherIds: string[]): Promise<string> {
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}`,
@ -105,7 +132,7 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
)
);
if (response.type === "RequestCancelled") {
if (response.type === "AbortResponse") {
throw new RequestAbortedError(response.fallbackRequested);
}
@ -113,10 +140,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
throw new RequestAbortedError();
}
abortController.signal.removeEventListener("abort", abortHandler);
return response.cipherId;
}
async confirmNewCredential({ credentialName, userName }: NewCredentialParams): Promise<boolean> {
async confirmNewCredential(
{ credentialName, userName }: NewCredentialParams,
abortController = new AbortController()
): Promise<boolean> {
const requestId = Utils.newGuid();
const data: BrowserFido2Message = {
type: "ConfirmNewCredentialRequest",
@ -125,6 +157,11 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
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}`,
@ -143,14 +180,12 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
return true;
}
if (response.type === "RequestCancelled") {
if (response.type === "AbortResponse") {
throw new RequestAbortedError(response.fallbackRequested);
}
abortController.signal.removeEventListener("abort", abortHandler);
return false;
}
private processMessage(msg: BrowserFido2Message) {
this.messages$.next(msg);
}
}

View File

@ -4,7 +4,10 @@ export interface NewCredentialParams {
}
export abstract class Fido2UserInterfaceService {
confirmCredential: (cipherId: string) => Promise<boolean>;
pickCredential: (cipherIds: string[]) => Promise<string>;
confirmNewCredential: (params: NewCredentialParams) => Promise<boolean>;
confirmCredential: (cipherId: string, abortController?: AbortController) => Promise<boolean>;
pickCredential: (cipherIds: string[], abortController?: AbortController) => Promise<string>;
confirmNewCredential: (
params: NewCredentialParams,
abortController?: AbortController
) => Promise<boolean>;
}

View File

@ -48,10 +48,13 @@ export class Fido2Service implements Fido2ServiceAbstraction {
params: CredentialRegistrationParams,
abortController?: AbortController
): Promise<CredentialRegistrationResult> {
const presence = await this.fido2UserInterfaceService.confirmNewCredential({
credentialName: params.rp.name,
userName: params.user.displayName,
});
const presence = await this.fido2UserInterfaceService.confirmNewCredential(
{
credentialName: params.rp.name,
userName: params.user.displayName,
},
abortController
);
const attestationFormat = STANDARD_ATTESTATION_FORMAT;
const encoder = new TextEncoder();
@ -122,7 +125,10 @@ export class Fido2Service implements Fido2ServiceAbstraction {
};
}
async assertCredential(params: CredentialAssertParams): Promise<CredentialAssertResult> {
async assertCredential(
params: CredentialAssertParams,
abortController?: AbortController
): Promise<CredentialAssertResult> {
let credential: BitCredential | undefined;
if (params.allowedCredentialIds && params.allowedCredentialIds.length > 0) {
@ -138,7 +144,10 @@ export class Fido2Service implements Fido2ServiceAbstraction {
// throw new OriginMismatchError();
// }
await this.fido2UserInterfaceService.confirmCredential(credential.credentialId.encoded);
await this.fido2UserInterfaceService.confirmCredential(
credential.credentialId.encoded,
abortController
);
} else {
// We're looking for a resident key
const credentials = await this.getCredentialsByRp(params.rpId);
@ -148,7 +157,8 @@ export class Fido2Service implements Fido2ServiceAbstraction {
}
const pickedId = await this.fido2UserInterfaceService.pickCredential(
credentials.map((c) => c.credentialId.encoded)
credentials.map((c) => c.credentialId.encoded),
abortController
);
credential = credentials.find((c) => c.credentialId.encoded === pickedId);
}
@ -184,7 +194,10 @@ export class Fido2Service implements Fido2ServiceAbstraction {
};
}
private async getCredential(allowedCredentialIds: string[]): Promise<BitCredential | undefined> {
private async getCredential(
allowedCredentialIds: string[],
abortController?: AbortController
): Promise<BitCredential | undefined> {
let cipher: Cipher | undefined;
for (const allowedCredential of allowedCredentialIds) {
cipher = await this.cipherService.get(allowedCredential);