1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00: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
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 BrowserPlatformUtilsService from "../services/browserPlatformUtils.service";
import { TabMessage } from "../types/tab-messages"; 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 = {}) { static sendMessage(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg); const message = Object.assign({}, { command: subscriber }, arg);
return chrome.runtime.sendMessage(message); return chrome.runtime.sendMessage(message);

View File

@@ -1,6 +1,6 @@
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, takeUntil } from "rxjs"; import { concatMap, Subject, switchMap, takeUntil } from "rxjs";
import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/enums/cipherType"; import { CipherType } from "@bitwarden/common/enums/cipherType";
@@ -53,6 +53,16 @@ export class Fido2Component implements OnInit, OnDestroy {
takeUntil(this.destroy$) takeUntil(this.destroy$)
) )
.subscribe(); .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) { async pick(cipher: CipherView) {
@@ -91,7 +101,7 @@ export class Fido2Component implements OnInit, OnDestroy {
const data = this.data; const data = this.data;
BrowserFido2UserInterfaceService.sendMessage({ BrowserFido2UserInterfaceService.sendMessage({
requestId: data.requestId, requestId: data.requestId,
type: "RequestCancelled", type: "AbortResponse",
fallbackRequested: fallback, 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 { import {
Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction, Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction,
@@ -37,7 +37,10 @@ export type BrowserFido2Message = { requestId: string } & (
type: "ConfirmNewCredentialResponse"; type: "ConfirmNewCredentialResponse";
} }
| { | {
type: "RequestCancelled"; type: "AbortRequest";
}
| {
type: "AbortResponse";
fallbackRequested: boolean; fallbackRequested: boolean;
} }
); );
@@ -51,17 +54,31 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
BrowserApi.sendMessage(BrowserFido2MessageName, msg); BrowserApi.sendMessage(BrowserFido2MessageName, msg);
} }
private messages$ = new Subject<BrowserFido2Message>(); static onAbort$(requestId: string): Observable<BrowserFido2Message> {
private destroy$ = new Subject<void>(); const messages$ = BrowserApi.messageListener$() as Observable<BrowserFido2Message>;
return messages$.pipe(
constructor(private popupUtilsService: PopupUtilsService) { filter((message) => message.type === "AbortRequest" && message.requestId === requestId),
BrowserApi.messageListener(BrowserFido2MessageName, this.processMessage.bind(this)); 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 requestId = Utils.newGuid();
const data: BrowserFido2Message = { type: "ConfirmCredentialRequest", cipherId, requestId }; const data: BrowserFido2Message = { type: "ConfirmCredentialRequest", cipherId, requestId };
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
const abortHandler = () =>
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
abortController.signal.addEventListener("abort", abortHandler);
this.popupUtilsService.popOut( this.popupUtilsService.popOut(
null, null,
`popup/index.html?uilocation=popout#/fido2?${queryParams}`, `popup/index.html?uilocation=popout#/fido2?${queryParams}`,
@@ -80,17 +97,27 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
return true; return true;
} }
if (response.type === "RequestCancelled") { if (response.type === "AbortResponse") {
throw new RequestAbortedError(response.fallbackRequested); throw new RequestAbortedError(response.fallbackRequested);
} }
abortController.signal.removeEventListener("abort", abortHandler);
return false; return false;
} }
async pickCredential(cipherIds: string[]): Promise<string> { async pickCredential(
cipherIds: string[],
abortController = new AbortController()
): Promise<string> {
const requestId = Utils.newGuid(); const requestId = Utils.newGuid();
const data: BrowserFido2Message = { type: "PickCredentialRequest", cipherIds, 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();
const abortHandler = () =>
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
abortController.signal.addEventListener("abort", abortHandler);
this.popupUtilsService.popOut( this.popupUtilsService.popOut(
null, null,
`popup/index.html?uilocation=popout#/fido2?${queryParams}`, `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); throw new RequestAbortedError(response.fallbackRequested);
} }
@@ -113,10 +140,15 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
throw new RequestAbortedError(); throw new RequestAbortedError();
} }
abortController.signal.removeEventListener("abort", abortHandler);
return response.cipherId; 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 requestId = Utils.newGuid();
const data: BrowserFido2Message = { const data: BrowserFido2Message = {
type: "ConfirmNewCredentialRequest", type: "ConfirmNewCredentialRequest",
@@ -125,6 +157,11 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
userName, userName,
}; };
const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString(); const queryParams = new URLSearchParams({ data: JSON.stringify(data) }).toString();
const abortHandler = () =>
BrowserFido2UserInterfaceService.sendMessage({ type: "AbortRequest", requestId });
abortController.signal.addEventListener("abort", abortHandler);
this.popupUtilsService.popOut( this.popupUtilsService.popOut(
null, null,
`popup/index.html?uilocation=popout#/fido2?${queryParams}`, `popup/index.html?uilocation=popout#/fido2?${queryParams}`,
@@ -143,14 +180,12 @@ export class BrowserFido2UserInterfaceService implements Fido2UserInterfaceServi
return true; return true;
} }
if (response.type === "RequestCancelled") { if (response.type === "AbortResponse") {
throw new RequestAbortedError(response.fallbackRequested); throw new RequestAbortedError(response.fallbackRequested);
} }
abortController.signal.removeEventListener("abort", abortHandler);
return false; return false;
} }
private processMessage(msg: BrowserFido2Message) {
this.messages$.next(msg);
}
} }

View File

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

View File

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