mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 13:40:06 +00:00
Improved wiring
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user