mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
Autofill/pm 9034 implement passkey for unlocked accounts (#13826)
* Passkey stuff Co-authored-by: Anders Åberg <github@andersaberg.com> * Ugly hacks * Work On Modal State Management * Applying modalStyles * modal * Improved hide/show * fixed promise * File name * fix prettier * Protecting against null API's and undefined data * Only show fake popup to devs * cleanup mock code * rename minmimal-app to modal-app * Added comment * Added comment * removed old comment * Avoided changing minimum size * Add small comment * Rename component * adress feedback * Fixed uppercase file * Fixed build * Added codeowners * added void * commentary * feat: reset setting on app start * Moved reset to be in main / process launch * Add comment to create window * Added a little bit of styling * Use Messaging service to loadUrl * Enable passkeysautofill * Add logging * halfbaked * Integration working * And now it works without extra delay * Clean up * add note about messaging * lb * removed console.logs * Cleanup and adress review feedback * This hides the swift UI * add modal components * update modal with correct ciphers and functionality * add create screen * pick credential, draft * Remove logger * a whole lot of wiring * not working * Improved wiring * Cancel after 90s * Introduced observable * update cipher handling * update to use matchesUri * Launching bitwarden if its not running * Passing position from native to electron * Rename inModalMode to modalMode * remove tap * revert spaces * added back isDev * cleaned up a bit * Cleanup swift file * tweaked logging * clean up * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Update apps/desktop/src/platform/main/autofill/native-autofill.main.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Update apps/desktop/src/platform/services/desktop-settings.service.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * adress position feedback * Update apps/desktop/macos/autofill-extension/CredentialProviderViewController.swift Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Removed extra logging * Adjusted error logging * Use .error to log errors * remove dead code * Update desktop-autofill.service.ts * use parseCredentialId instead of guidToRawFormat * Update apps/desktop/src/autofill/services/desktop-autofill.service.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Change windowXy to a Record instead of [number,number] * Update apps/desktop/src/autofill/services/desktop-fido2-user-interface.service.ts Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com> * Remove unsued dep and comment * changed timeout to be spec recommended maxium, 10 minutes, for now. * Correctly assume UP * Removed extra cancelRequest in deinint * Add timeout and UV to confirmChoseCipher UV is performed by UI, not the service * Improved docs regarding undefined cipherId * cleanup: UP is no longer undefined * Run completeError if ipc messages conversion failed * don't throw, instead return undefined * Disabled passkey provider * Throw error if no activeUserId was found * removed comment * Fixed lint * removed unsued service * reset entitlement formatting * Update entitlements.mas.plist * Fix build issues * Fix import issues * Update route names to use `fido2` * Fix being unable to select a passkey * Fix linting issues * Followup to fix merge issues and other comments * Update `userHandle` value * Add error handling for missing session or other errors * Remove unused route * Fix linting issues * Simplify updateCredential method * Followup to remove comments and timeouts and handle errors * Address lint issue by using `takeUntilDestroyed` * PR Followup for typescript and vault concerns * Add try block for cipher creation * Make userId manditory for cipher service --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: Anders Åberg <github@andersaberg.com> Co-authored-by: Anders Åberg <anders@andersaberg.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com> Co-authored-by: Evan Bassler <evanbassler@Mac.attlocal.net> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
@@ -55,9 +55,10 @@ import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { Fido2CreateComponent } from "../modal/passkeys/create/fido2-create.component";
|
||||
import { Fido2VaultComponent } from "../modal/passkeys/fido2-vault.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
|
||||
/**
|
||||
@@ -179,12 +180,12 @@ const routes: Routes = [
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
path: "fido2-assertion",
|
||||
component: Fido2VaultComponent,
|
||||
},
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
path: "fido2-creation",
|
||||
component: Fido2CreateComponent,
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
|
||||
@@ -97,7 +97,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
|
||||
// userVerification: true,
|
||||
// });
|
||||
|
||||
this.session.notifyConfirmNewCredential(true);
|
||||
this.session.notifyConfirmCreateCredential(true);
|
||||
|
||||
// Not sure this clean up should happen here or in session.
|
||||
// The session currently toggles modal on and send us here
|
||||
@@ -113,7 +113,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
|
||||
this.session.notifyConfirmNewCredential(false);
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
// little bit hacky:
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
|
||||
@@ -60,10 +60,6 @@ export class DesktopAutofillService implements OnDestroy {
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((enabled) => {
|
||||
// if (!enabled) {
|
||||
// return EMPTY;
|
||||
// }
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
filter((userId): userId is UserId => userId != null),
|
||||
|
||||
@@ -94,9 +94,12 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
) {}
|
||||
|
||||
private confirmCredentialSubject = new Subject<boolean>();
|
||||
private createdCipher: Cipher;
|
||||
|
||||
private availableCipherIdsSubject = new BehaviorSubject<string[]>(null);
|
||||
private createdCipher: Cipher = new Cipher();
|
||||
private updatedCipher: CipherView = new CipherView();
|
||||
|
||||
private rpId = new BehaviorSubject<string>("");
|
||||
private availableCipherIdsSubject = new BehaviorSubject<string[]>([""]);
|
||||
/**
|
||||
* Observable that emits available cipher IDs once they're confirmed by the UI
|
||||
*/
|
||||
@@ -136,15 +139,15 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
// make the cipherIds available to the UI.
|
||||
this.availableCipherIdsSubject.next(cipherIds);
|
||||
|
||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
||||
await this.showUi("/fido2-assertion", this.windowObject.windowXy);
|
||||
|
||||
const chosenCipherResponse = await this.waitForUiChosenCipher();
|
||||
|
||||
this.logService.debug("Received chosen cipher", chosenCipherResponse);
|
||||
|
||||
return {
|
||||
cipherId: chosenCipherResponse.cipherId,
|
||||
userVerified: chosenCipherResponse.userVerified,
|
||||
cipherId: chosenCipherResponse?.cipherId,
|
||||
userVerified: chosenCipherResponse?.userVerified,
|
||||
};
|
||||
} finally {
|
||||
// Make sure to clean up so the app is never stuck in modal mode?
|
||||
@@ -152,6 +155,15 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
}
|
||||
}
|
||||
|
||||
async getRpId(): Promise<string> {
|
||||
return lastValueFrom(
|
||||
this.rpId.pipe(
|
||||
filter((id) => id != null),
|
||||
take(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
confirmChosenCipher(cipherId: string, userVerified: boolean = false): void {
|
||||
this.chosenCipherSubject.next({ cipherId, userVerified });
|
||||
this.chosenCipherSubject.complete();
|
||||
@@ -159,7 +171,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
|
||||
private async waitForUiChosenCipher(
|
||||
timeoutMs: number = 60000,
|
||||
): Promise<{ cipherId: string; userVerified: boolean } | undefined> {
|
||||
): Promise<{ cipherId?: string; userVerified: boolean } | undefined> {
|
||||
try {
|
||||
return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs)));
|
||||
} catch {
|
||||
@@ -174,7 +186,10 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
/**
|
||||
* Notifies the Fido2UserInterfaceSession that the UI operations has completed and it can return to the OS.
|
||||
*/
|
||||
notifyConfirmNewCredential(confirmed: boolean): void {
|
||||
notifyConfirmCreateCredential(confirmed: boolean, updatedCipher?: CipherView): void {
|
||||
if (updatedCipher) {
|
||||
this.updatedCipher = updatedCipher;
|
||||
}
|
||||
this.confirmCredentialSubject.next(confirmed);
|
||||
this.confirmCredentialSubject.complete();
|
||||
}
|
||||
@@ -195,42 +210,43 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
async confirmNewCredential({
|
||||
credentialName,
|
||||
userName,
|
||||
userHandle,
|
||||
userVerification,
|
||||
rpId,
|
||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
||||
}: NewCredentialParams): Promise<{ cipherId?: string; userVerified: boolean }> {
|
||||
this.logService.warning(
|
||||
"confirmNewCredential",
|
||||
credentialName,
|
||||
userName,
|
||||
userHandle,
|
||||
userVerification,
|
||||
rpId,
|
||||
);
|
||||
this.rpId.next(rpId);
|
||||
|
||||
try {
|
||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
||||
await this.showUi("/fido2-creation", this.windowObject.windowXy);
|
||||
|
||||
// Wait for the UI to wrap up
|
||||
const confirmation = await this.waitForUiNewCredentialConfirmation();
|
||||
if (!confirmation) {
|
||||
return { cipherId: undefined, userVerified: false };
|
||||
}
|
||||
// Create the credential
|
||||
await this.createCredential({
|
||||
credentialName,
|
||||
userName,
|
||||
rpId,
|
||||
userHandle: "",
|
||||
userVerification,
|
||||
});
|
||||
|
||||
// wait for 10ms to help RXJS catch up(?)
|
||||
// We sometimes get a race condition from this.createCredential not updating cipherService in time
|
||||
//console.log("waiting 10ms..");
|
||||
//await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
//console.log("Just waited 10ms");
|
||||
|
||||
// Return the new cipher (this.createdCipher)
|
||||
return { cipherId: this.createdCipher.id, userVerified: userVerification };
|
||||
if (this.updatedCipher) {
|
||||
await this.updateCredential(this.updatedCipher);
|
||||
return { cipherId: this.updatedCipher.id, userVerified: userVerification };
|
||||
} else {
|
||||
// Create the credential
|
||||
await this.createCipher({
|
||||
credentialName,
|
||||
userName,
|
||||
rpId,
|
||||
userHandle,
|
||||
userVerification,
|
||||
});
|
||||
return { cipherId: this.createdCipher.id, userVerified: userVerification };
|
||||
}
|
||||
} finally {
|
||||
// Make sure to clean up so the app is never stuck in modal mode?
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
@@ -240,15 +256,16 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
private async showUi(route: string, position?: { x: number; y: number }): Promise<void> {
|
||||
// Load the UI:
|
||||
await this.desktopSettingsService.setModalMode(true, position);
|
||||
await this.router.navigate(["/passkeys"]);
|
||||
await this.router.navigate([route]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called by the UI to create a new credential with user input etc.
|
||||
* @param param0
|
||||
*/
|
||||
async createCredential({ credentialName, userName, rpId }: NewCredentialParams): Promise<Cipher> {
|
||||
async createCipher({ credentialName, userName, rpId }: NewCredentialParams): Promise<Cipher> {
|
||||
// Store the passkey on a new cipher to avoid replacing something important
|
||||
|
||||
const cipher = new CipherView();
|
||||
cipher.name = credentialName;
|
||||
|
||||
@@ -267,12 +284,34 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!activeUserId) {
|
||||
throw new Error("No active user ID found!");
|
||||
}
|
||||
|
||||
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||
|
||||
this.createdCipher = createdCipher;
|
||||
try {
|
||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||
this.createdCipher = createdCipher;
|
||||
|
||||
return createdCipher;
|
||||
return createdCipher;
|
||||
} catch {
|
||||
throw new Error("Unable to create cipher");
|
||||
}
|
||||
}
|
||||
|
||||
async updateCredential(cipher: CipherView): Promise<void> {
|
||||
this.logService.warning("updateCredential");
|
||||
await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
map(async (a) => {
|
||||
if (a) {
|
||||
const encCipher = await this.cipherService.encrypt(cipher, a.id);
|
||||
await this.cipherService.updateWithServer(encCipher);
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
||||
|
||||
@@ -797,6 +797,12 @@
|
||||
"unexpectedError": {
|
||||
"message": "An unexpected error has occurred."
|
||||
},
|
||||
"unexpectedErrorShort": {
|
||||
"message": "Unexpected error"
|
||||
},
|
||||
"closeThisBitwardenWindow": {
|
||||
"message": "Close this Bitwarden window and try again."
|
||||
},
|
||||
"itemInformation": {
|
||||
"message": "Item information"
|
||||
},
|
||||
@@ -3559,6 +3565,21 @@
|
||||
"changeAcctEmail": {
|
||||
"message": "Change account email"
|
||||
},
|
||||
"passkeyLogin": {
|
||||
"message": "Log in with passkey?"
|
||||
},
|
||||
"savePasskeyQuestion": {
|
||||
"message": "Save passkey?"
|
||||
},
|
||||
"saveNewPasskey": {
|
||||
"message": "Save as new login"
|
||||
},
|
||||
"unableToSavePasskey": {
|
||||
"message": "Unable to save passkey"
|
||||
},
|
||||
"closeBitwarden": {
|
||||
"message": "Close Bitwarden"
|
||||
},
|
||||
"allowScreenshots": {
|
||||
"message": "Allow screen capture"
|
||||
},
|
||||
|
||||
@@ -53,9 +53,14 @@ export class TrayMain {
|
||||
},
|
||||
{
|
||||
visible: isDev(),
|
||||
label: "Fake Popup",
|
||||
label: "Fake Popup Select",
|
||||
click: () => this.fakePopup(),
|
||||
},
|
||||
{
|
||||
visible: isDev(),
|
||||
label: "Fake Popup Create",
|
||||
click: () => this.fakePopupCreate(),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: this.i18nService.t("exit"),
|
||||
@@ -218,4 +223,8 @@ export class TrayMain {
|
||||
private async fakePopup() {
|
||||
await this.messagingService.send("loadurl", { url: "/passkeys", modal: true });
|
||||
}
|
||||
|
||||
private async fakePopupCreate() {
|
||||
await this.messagingService.send("loadurl", { url: "/create-passkey", modal: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full">
|
||||
<bit-section
|
||||
disableMargin
|
||||
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<bit-section-header class="tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">
|
||||
{{ "savePasskeyQuestion" | i18n }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
slot="end"
|
||||
class="tw-mb-4 tw-mr-2"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</bit-section-header>
|
||||
</bit-section>
|
||||
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
|
||||
<bit-item *ngFor="let c of ciphers$ | async" class="">
|
||||
<button type="button" bit-item-content (click)="addPasskeyToCipher(c)">
|
||||
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
|
||||
<button bitLink [title]="c.name" type="button">
|
||||
{{ c.name }}
|
||||
</button>
|
||||
<span slot="secondary">{{ c.subTitle }}</span>
|
||||
<span bitBadge slot="end">Save</span>
|
||||
</button>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
<bit-section class="tw-bg-background-alt tw-p-4">
|
||||
<bit-item class="">
|
||||
<button bitLink linkType="primary" type="button" bit-item-content (click)="confirmPasskey()">
|
||||
<a bitLink linkType="primary" class="tw-font-medium tw-text-base">
|
||||
{{ "saveNewPasskey" | i18n }}
|
||||
</a>
|
||||
</button>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
</div>
|
||||
143
apps/desktop/src/modal/passkeys/create/fido2-create.component.ts
Normal file
143
apps/desktop/src/modal/passkeys/create/fido2-create.component.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { RouterModule, Router } from "@angular/router";
|
||||
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DialogService,
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { DesktopSettingsService } from "../../../platform/services/desktop-settings.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
ItemModule,
|
||||
BadgeModule,
|
||||
],
|
||||
templateUrl: "fido2-create.component.html",
|
||||
})
|
||||
export class Fido2CreateComponent implements OnInit {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
|
||||
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
|
||||
readonly Icons = { BitwardenShield };
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly dialogService: DialogService,
|
||||
private readonly domainSettingsService: DomainSettingsService,
|
||||
private readonly logService: LogService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
const rpid = await this.session.getRpId();
|
||||
const equivalentDomains = await firstValueFrom(
|
||||
this.domainSettingsService.getUrlEquivalentDomains(rpid),
|
||||
);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
this.cipherService
|
||||
.getAllDecrypted(activeUserId)
|
||||
.then((ciphers) => {
|
||||
const relevantCiphers = ciphers.filter((cipher) => {
|
||||
if (!cipher.login || !cipher.login.hasUris) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
cipher.login.matchesUri(rpid, equivalentDomains) &&
|
||||
(!cipher.login.fido2Credentials || cipher.login.fido2Credentials.length === 0)
|
||||
);
|
||||
});
|
||||
this.ciphersSubject.next(relevantCiphers);
|
||||
})
|
||||
.catch((error) => this.logService.error(error));
|
||||
}
|
||||
|
||||
async addPasskeyToCipher(cipher: CipherView) {
|
||||
this.session.notifyConfirmCreateCredential(true, cipher);
|
||||
}
|
||||
|
||||
async confirmPasskey() {
|
||||
try {
|
||||
// Retrieve the current UI session to control the flow
|
||||
if (!this.session) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "unexpectedErrorShort" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeBitwarden" },
|
||||
cancelButtonText: null,
|
||||
});
|
||||
if (confirmed) {
|
||||
await this.closeModal();
|
||||
}
|
||||
} else {
|
||||
this.session.notifyConfirmCreateCredential(true);
|
||||
}
|
||||
|
||||
// Not sure this clean up should happen here or in session.
|
||||
// The session currently toggles modal on and send us here
|
||||
// But if this route is somehow opened outside of session we want to make sure we clean up?
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
} catch {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "unableToSavePasskey" },
|
||||
content: { key: "closeThisBitwardenWindow" },
|
||||
type: "danger",
|
||||
acceptButtonText: { key: "closeBitwarden" },
|
||||
cancelButtonText: null,
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
await this.closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async closeModal() {
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
}
|
||||
36
apps/desktop/src/modal/passkeys/fido2-vault.component.html
Normal file
36
apps/desktop/src/modal/passkeys/fido2-vault.component.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<div class="tw-flex tw-flex-col tw-h-full">
|
||||
<bit-section
|
||||
disableMargin
|
||||
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
|
||||
>
|
||||
<bit-section-header class="tw-bg-background">
|
||||
<div class="tw-flex tw-items-center">
|
||||
<bit-icon [icon]="Icons.BitwardenShield" class="tw-w-10 tw-mt-2 tw-ml-2"></bit-icon>
|
||||
|
||||
<h2 bitTypography="h4" class="tw-font-semibold tw-text-lg">{{ "passkeyLogin" | i18n }}</h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-close"
|
||||
slot="end"
|
||||
class="tw-mb-4 tw-mr-2"
|
||||
(click)="closeModal()"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</bit-section-header>
|
||||
</bit-section>
|
||||
|
||||
<bit-section class="tw-bg-background-alt tw-p-4 tw-flex tw-flex-col tw-grow">
|
||||
<bit-item *ngFor="let c of ciphers$ | async" class="">
|
||||
<button type="button" bit-item-content (click)="chooseCipher(c.id)">
|
||||
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
|
||||
<button bitLink [title]="c.name" type="button">
|
||||
{{ c.name }}
|
||||
</button>
|
||||
<span slot="secondary">{{ c.subTitle }}</span>
|
||||
<span bitBadge slot="end">Select</span>
|
||||
</button>
|
||||
</bit-item>
|
||||
</bit-section>
|
||||
</div>
|
||||
102
apps/desktop/src/modal/passkeys/fido2-vault.component.ts
Normal file
102
apps/desktop/src/modal/passkeys/fido2-vault.component.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterModule, Router } from "@angular/router";
|
||||
import { firstValueFrom, map, BehaviorSubject, Observable } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { BitwardenShield } from "@bitwarden/auth/angular";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
BadgeModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
IconModule,
|
||||
ItemModule,
|
||||
SectionComponent,
|
||||
TableModule,
|
||||
BitIconButtonComponent,
|
||||
SectionHeaderComponent,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import {
|
||||
DesktopFido2UserInterfaceService,
|
||||
DesktopFido2UserInterfaceSession,
|
||||
} from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SectionHeaderComponent,
|
||||
BitIconButtonComponent,
|
||||
TableModule,
|
||||
JslibModule,
|
||||
IconModule,
|
||||
ButtonModule,
|
||||
DialogModule,
|
||||
SectionComponent,
|
||||
ItemModule,
|
||||
BadgeModule,
|
||||
],
|
||||
templateUrl: "fido2-vault.component.html",
|
||||
})
|
||||
export class Fido2VaultComponent implements OnInit, OnDestroy {
|
||||
session?: DesktopFido2UserInterfaceSession = null;
|
||||
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
|
||||
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
|
||||
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
|
||||
cipherIds$: Observable<string[]>;
|
||||
readonly Icons = { BitwardenShield };
|
||||
|
||||
constructor(
|
||||
private readonly desktopSettingsService: DesktopSettingsService,
|
||||
private readonly fido2UserInterfaceService: DesktopFido2UserInterfaceService,
|
||||
private readonly cipherService: CipherService,
|
||||
private readonly accountService: AccountService,
|
||||
private readonly logService: LogService,
|
||||
private readonly router: Router,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
this.session = this.fido2UserInterfaceService.getCurrentSession();
|
||||
this.cipherIds$ = this.session?.availableCipherIds$;
|
||||
|
||||
this.cipherIds$.pipe(takeUntilDestroyed()).subscribe((cipherIds) => {
|
||||
this.cipherService
|
||||
.getAllDecrypted(activeUserId)
|
||||
.then((ciphers) => {
|
||||
this.ciphersSubject.next(ciphers.filter((cipher) => cipherIds.includes(cipher.id)));
|
||||
})
|
||||
.catch((error) => this.logService.error(error));
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject
|
||||
}
|
||||
|
||||
async chooseCipher(cipherId: string) {
|
||||
this.session?.confirmChosenCipher(cipherId, true);
|
||||
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
}
|
||||
|
||||
async closeModal() {
|
||||
await this.router.navigate(["/"]);
|
||||
await this.desktopSettingsService.setModalMode(false);
|
||||
|
||||
this.session.notifyConfirmCreateCredential(false);
|
||||
this.session.confirmChosenCipher(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user