1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 13:40:06 +00:00

Improved wiring

This commit is contained in:
Anders Åberg
2025-02-14 15:38:28 +01:00
parent f6ef7a3bef
commit 0cb5935634
3 changed files with 140 additions and 20 deletions

View File

@@ -1,5 +1,7 @@
import { Component, OnInit } from "@angular/core";
import { CommonModule } from "@angular/common"; // Add this
import { Component, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { BehaviorSubject, Observable } from "rxjs";
import {
DesktopFido2UserInterfaceService,
@@ -9,11 +11,26 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings
@Component({
standalone: true,
imports: [CommonModule], // Add this
template: `
<div
style="background:white; display:flex; justify-content: center; align-items: center; flex-direction: column"
>
<h1 style="color: black">Select your passkey</h1>
<div *ngFor="let item of cipherIds$ | async">
<button
style="color:black; padding: 10px 20px; border: 1px solid blue; margin: 10px"
bitButton
type="button"
buttonType="secondary"
(click)="chooseCipher(item)"
>
{{ item }}
</button>
</div>
<br />
<button
style="color:black; padding: 10px 20px; border: 1px solid black; margin: 10px"
@@ -36,16 +53,36 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings
</div>
`,
})
export class Fido2PlaceholderComponent implements OnInit {
export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
session?: DesktopFido2UserInterfaceSession = null;
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
cipherIds$: Observable<string[]> = this.cipherIdsSubject.asObservable();
constructor(
private readonly desktopSettingsService: DesktopSettingsService,
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
private readonly router: Router,
) {}
ngOnInit(): void {
async ngOnInit(): Promise<void> {
this.session = this.fido2UserInterfaceService.getCurrentSession();
const cipherIds = await this.session?.getAvailableCipherIds();
this.cipherIdsSubject.next(cipherIds || []);
// eslint-disable-next-line no-console
console.log("Available cipher IDs", cipherIds);
}
async chooseCipher(cipherId: string) {
this.session?.confirmChosenCipher(cipherId);
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
}
ngOnDestroy() {
this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject
}
async confirmPasskey() {
@@ -65,7 +102,7 @@ export class Fido2PlaceholderComponent implements OnInit {
// userVerification: true,
// });
this.session.notifyConfirmCredential(true);
this.session.notifyConfirmNewCredential(true);
// Not sure this clean up should happen here or in session.
// The session currently toggles modal on and send us here
@@ -81,6 +118,8 @@ export class Fido2PlaceholderComponent implements OnInit {
await this.router.navigate(["/"]);
await this.desktopSettingsService.setInModalMode(false);
this.session.notifyConfirmCredential(false);
this.session.notifyConfirmNewCredential(false);
// little bit hacky:
this.session.confirmChosenCipher(null);
}
}

View File

@@ -159,7 +159,12 @@ export class DesktopAutofillService implements OnDestroy {
ipc.autofill.listenPasskeyAssertionWithoutUserInterface(
async (clientId, sequenceNumber, request, callback) => {
this.logService.warning("listenPasskeyAssertion", clientId, sequenceNumber, request);
this.logService.warning(
"listenPasskeyAssertion without user interface",
clientId,
sequenceNumber,
request,
);
// TODO: For some reason the credentialId is passed as an empty array in the request, so we need to
// get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
@@ -193,7 +198,7 @@ export class DesktopAutofillService implements OnDestroy {
const controller = new AbortController();
void this.fido2AuthenticatorService
.getAssertion(this.convertAssertionRequest(request), null, controller)
.getAssertion(this.convertAssertionRequest(request, true), null, controller)
.then((response) => {
callback(null, this.convertAssertionResponse(request, response));
})
@@ -261,10 +266,17 @@ export class DesktopAutofillService implements OnDestroy {
};
}
/**
*
* @param request
* @param assumeUserPresence For WithoutUserInterface requests, we assume the user is present
* @returns
*/
private convertAssertionRequest(
request:
| autofill.PasskeyAssertionRequest
| autofill.PasskeyAssertionWithoutUserInterfaceRequest,
assumeUserPresence: boolean = false,
): Fido2AuthenticatorGetAssertionParams {
let allowedCredentials;
if ("credentialId" in request) {
@@ -289,6 +301,7 @@ export class DesktopAutofillService implements OnDestroy {
requireUserVerification:
request.userVerification === "required" || request.userVerification === "preferred",
fallbackSupported: false,
assumeUserPresence: assumeUserPresence,
};
}

View File

@@ -1,5 +1,14 @@
import { Router } from "@angular/router";
import { lastValueFrom, firstValueFrom, map, Subject } from "rxjs";
import {
lastValueFrom,
firstValueFrom,
map,
Subject,
filter,
take,
timeout,
BehaviorSubject,
} from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
@@ -79,28 +88,87 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
private confirmCredentialSubject = new Subject<boolean>();
private createdCipher: Cipher;
private availableCipherIds = new BehaviorSubject<string[]>(null);
private chosenCipherSubject = new Subject<string>();
// Method implementation
async pickCredential(
params: PickCredentialParams,
): Promise<{ cipherId: string; userVerified: boolean }> {
this.logService.warning("pickCredential desktop function", params);
async pickCredential({
cipherIds,
userVerification,
assumeUserPresence,
masterPasswordRepromptRequired,
}: PickCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
this.logService.warning("pickCredential desktop function", {
cipherIds,
userVerification,
assumeUserPresence,
masterPasswordRepromptRequired,
});
try {
await this.showUi();
// Check if we can return the credential without user interaction
// TODO: Assume user presence is undefined
if (cipherIds.length === 1 && !masterPasswordRepromptRequired) {
this.logService.debug(
"shortcut - Assuming user presence and returning cipherId",
cipherIds[0],
);
return { cipherId: cipherIds[0], userVerified: userVerification };
}
await this.waitForUiCredentialConfirmation();
this.logService.debug("Could not shortcut, showing UI");
return { cipherId: params.cipherIds[0], userVerified: true };
// make the cipherIds available to the UI.
// Not sure if the UI also need to know about masterPasswordRepromptRequired -- probably not, otherwise we can send all of the params.
this.availableCipherIds.next(cipherIds);
await this.showUi("/passkeys");
const chosenCipherId = await this.waitForUiChosenCipher();
this.logService.debug("Received chosen cipher", chosenCipherId);
if (!chosenCipherId) {
throw new Error("User cancelled");
}
const resultCipherId = cipherIds.find((id) => id === chosenCipherId);
// TODO: perform userverification
return { cipherId: resultCipherId, userVerified: true };
} finally {
// Make sure to clean up so the app is never stuck in modal mode?
await this.desktopSettingsService.setInModalMode(false);
}
}
/**
* Returns once the UI has confirmed and completed the operation
* @returns
*/
async getAvailableCipherIds(): Promise<string[]> {
return lastValueFrom(
this.availableCipherIds.pipe(
filter((ids) => ids != null),
take(1),
timeout(50000),
),
);
}
confirmChosenCipher(cipherId: string): void {
this.chosenCipherSubject.next(cipherId);
this.chosenCipherSubject.complete();
}
private async waitForUiChosenCipher(): Promise<string> {
return lastValueFrom(this.chosenCipherSubject);
}
/**
* Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS.
*/
notifyConfirmCredential(confirmed: boolean): void {
notifyConfirmNewCredential(confirmed: boolean): void {
this.confirmCredentialSubject.next(confirmed);
this.confirmCredentialSubject.complete();
}
@@ -109,7 +177,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
* Returns once the UI has confirmed and completed the operation
* @returns
*/
private async waitForUiCredentialConfirmation(): Promise<boolean> {
private async waitForUiNewCredentialConfirmation(): Promise<boolean> {
return lastValueFrom(this.confirmCredentialSubject);
}
@@ -133,10 +201,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
);
try {
await this.showUi();
await this.showUi("/passkeys");
// Wait for the UI to wrap up
const confirmation = await this.waitForUiCredentialConfirmation();
const confirmation = await this.waitForUiNewCredentialConfirmation();
if (!confirmation) {
throw new Error("User cancelled");
}
@@ -163,7 +231,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
}
}
private async showUi() {
private async showUi(route: string) {
// Load the UI:
// maybe toggling to modal mode shouldn't be done here?
await this.desktopSettingsService.setInModalMode(true);