mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 16:23:44 +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 { SetPasswordComponent } from "../auth/set-password.component";
|
||||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.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 { VaultComponent } from "../vault/app/vault/vault.component";
|
||||||
|
|
||||||
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";
|
|
||||||
import { SendComponent } from "./tools/send/send.component";
|
import { SendComponent } from "./tools/send/send.component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,12 +180,12 @@ const routes: Routes = [
|
|||||||
canActivate: [authGuard],
|
canActivate: [authGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "passkeys",
|
path: "fido2-assertion",
|
||||||
component: Fido2PlaceholderComponent,
|
component: Fido2VaultComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "passkeys",
|
path: "fido2-creation",
|
||||||
component: Fido2PlaceholderComponent,
|
component: Fido2CreateComponent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "",
|
path: "",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export class Fido2PlaceholderComponent implements OnInit, OnDestroy {
|
|||||||
// userVerification: true,
|
// userVerification: true,
|
||||||
// });
|
// });
|
||||||
|
|
||||||
this.session.notifyConfirmNewCredential(true);
|
this.session.notifyConfirmCreateCredential(true);
|
||||||
|
|
||||||
// Not sure this clean up should happen here or in session.
|
// Not sure this clean up should happen here or in session.
|
||||||
// The session currently toggles modal on and send us here
|
// 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.router.navigate(["/"]);
|
||||||
await this.desktopSettingsService.setModalMode(false);
|
await this.desktopSettingsService.setModalMode(false);
|
||||||
|
|
||||||
this.session.notifyConfirmNewCredential(false);
|
this.session.notifyConfirmCreateCredential(false);
|
||||||
// little bit hacky:
|
// little bit hacky:
|
||||||
this.session.confirmChosenCipher(null);
|
this.session.confirmChosenCipher(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ export class DesktopAutofillService implements OnDestroy {
|
|||||||
.pipe(
|
.pipe(
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
switchMap((enabled) => {
|
switchMap((enabled) => {
|
||||||
// if (!enabled) {
|
|
||||||
// return EMPTY;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.accountService.activeAccount$.pipe(
|
return this.accountService.activeAccount$.pipe(
|
||||||
map((account) => account?.id),
|
map((account) => account?.id),
|
||||||
filter((userId): userId is UserId => userId != null),
|
filter((userId): userId is UserId => userId != null),
|
||||||
|
|||||||
@@ -94,9 +94,12 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
private confirmCredentialSubject = new Subject<boolean>();
|
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
|
* 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.
|
// make the cipherIds available to the UI.
|
||||||
this.availableCipherIdsSubject.next(cipherIds);
|
this.availableCipherIdsSubject.next(cipherIds);
|
||||||
|
|
||||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
await this.showUi("/fido2-assertion", this.windowObject.windowXy);
|
||||||
|
|
||||||
const chosenCipherResponse = await this.waitForUiChosenCipher();
|
const chosenCipherResponse = await this.waitForUiChosenCipher();
|
||||||
|
|
||||||
this.logService.debug("Received chosen cipher", chosenCipherResponse);
|
this.logService.debug("Received chosen cipher", chosenCipherResponse);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cipherId: chosenCipherResponse.cipherId,
|
cipherId: chosenCipherResponse?.cipherId,
|
||||||
userVerified: chosenCipherResponse.userVerified,
|
userVerified: chosenCipherResponse?.userVerified,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// Make sure to clean up so the app is never stuck in modal mode?
|
// 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 {
|
confirmChosenCipher(cipherId: string, userVerified: boolean = false): void {
|
||||||
this.chosenCipherSubject.next({ cipherId, userVerified });
|
this.chosenCipherSubject.next({ cipherId, userVerified });
|
||||||
this.chosenCipherSubject.complete();
|
this.chosenCipherSubject.complete();
|
||||||
@@ -159,7 +171,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
|
|
||||||
private async waitForUiChosenCipher(
|
private async waitForUiChosenCipher(
|
||||||
timeoutMs: number = 60000,
|
timeoutMs: number = 60000,
|
||||||
): Promise<{ cipherId: string; userVerified: boolean } | undefined> {
|
): Promise<{ cipherId?: string; userVerified: boolean } | undefined> {
|
||||||
try {
|
try {
|
||||||
return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs)));
|
return await lastValueFrom(this.chosenCipherSubject.pipe(timeout(timeoutMs)));
|
||||||
} catch {
|
} 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.
|
* 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.next(confirmed);
|
||||||
this.confirmCredentialSubject.complete();
|
this.confirmCredentialSubject.complete();
|
||||||
}
|
}
|
||||||
@@ -195,42 +210,43 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
async confirmNewCredential({
|
async confirmNewCredential({
|
||||||
credentialName,
|
credentialName,
|
||||||
userName,
|
userName,
|
||||||
|
userHandle,
|
||||||
userVerification,
|
userVerification,
|
||||||
rpId,
|
rpId,
|
||||||
}: NewCredentialParams): Promise<{ cipherId: string; userVerified: boolean }> {
|
}: NewCredentialParams): Promise<{ cipherId?: string; userVerified: boolean }> {
|
||||||
this.logService.warning(
|
this.logService.warning(
|
||||||
"confirmNewCredential",
|
"confirmNewCredential",
|
||||||
credentialName,
|
credentialName,
|
||||||
userName,
|
userName,
|
||||||
|
userHandle,
|
||||||
userVerification,
|
userVerification,
|
||||||
rpId,
|
rpId,
|
||||||
);
|
);
|
||||||
|
this.rpId.next(rpId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.showUi("/passkeys", this.windowObject.windowXy);
|
await this.showUi("/fido2-creation", this.windowObject.windowXy);
|
||||||
|
|
||||||
// Wait for the UI to wrap up
|
// Wait for the UI to wrap up
|
||||||
const confirmation = await this.waitForUiNewCredentialConfirmation();
|
const confirmation = await this.waitForUiNewCredentialConfirmation();
|
||||||
if (!confirmation) {
|
if (!confirmation) {
|
||||||
return { cipherId: undefined, userVerified: false };
|
return { cipherId: undefined, userVerified: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.updatedCipher) {
|
||||||
|
await this.updateCredential(this.updatedCipher);
|
||||||
|
return { cipherId: this.updatedCipher.id, userVerified: userVerification };
|
||||||
|
} else {
|
||||||
// Create the credential
|
// Create the credential
|
||||||
await this.createCredential({
|
await this.createCipher({
|
||||||
credentialName,
|
credentialName,
|
||||||
userName,
|
userName,
|
||||||
rpId,
|
rpId,
|
||||||
userHandle: "",
|
userHandle,
|
||||||
userVerification,
|
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 };
|
return { cipherId: this.createdCipher.id, userVerified: userVerification };
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Make sure to clean up so the app is never stuck in modal mode?
|
// Make sure to clean up so the app is never stuck in modal mode?
|
||||||
await this.desktopSettingsService.setModalMode(false);
|
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> {
|
private async showUi(route: string, position?: { x: number; y: number }): Promise<void> {
|
||||||
// Load the UI:
|
// Load the UI:
|
||||||
await this.desktopSettingsService.setModalMode(true, position);
|
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.
|
* Can be called by the UI to create a new credential with user input etc.
|
||||||
* @param param0
|
* @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
|
// Store the passkey on a new cipher to avoid replacing something important
|
||||||
|
|
||||||
const cipher = new CipherView();
|
const cipher = new CipherView();
|
||||||
cipher.name = credentialName;
|
cipher.name = credentialName;
|
||||||
|
|
||||||
@@ -267,12 +284,34 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
|
|||||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
if (!activeUserId) {
|
||||||
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
throw new Error("No active user ID found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
const encCipher = await this.cipherService.encrypt(cipher, activeUserId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const createdCipher = await this.cipherService.createWithServer(encCipher);
|
||||||
this.createdCipher = createdCipher;
|
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> {
|
async informExcludedCredential(existingCipherIds: string[]): Promise<void> {
|
||||||
|
|||||||
@@ -797,6 +797,12 @@
|
|||||||
"unexpectedError": {
|
"unexpectedError": {
|
||||||
"message": "An unexpected error has occurred."
|
"message": "An unexpected error has occurred."
|
||||||
},
|
},
|
||||||
|
"unexpectedErrorShort": {
|
||||||
|
"message": "Unexpected error"
|
||||||
|
},
|
||||||
|
"closeThisBitwardenWindow": {
|
||||||
|
"message": "Close this Bitwarden window and try again."
|
||||||
|
},
|
||||||
"itemInformation": {
|
"itemInformation": {
|
||||||
"message": "Item information"
|
"message": "Item information"
|
||||||
},
|
},
|
||||||
@@ -3559,6 +3565,21 @@
|
|||||||
"changeAcctEmail": {
|
"changeAcctEmail": {
|
||||||
"message": "Change account email"
|
"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": {
|
"allowScreenshots": {
|
||||||
"message": "Allow screen capture"
|
"message": "Allow screen capture"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,9 +53,14 @@ export class TrayMain {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
visible: isDev(),
|
visible: isDev(),
|
||||||
label: "Fake Popup",
|
label: "Fake Popup Select",
|
||||||
click: () => this.fakePopup(),
|
click: () => this.fakePopup(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
visible: isDev(),
|
||||||
|
label: "Fake Popup Create",
|
||||||
|
click: () => this.fakePopupCreate(),
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: this.i18nService.t("exit"),
|
label: this.i18nService.t("exit"),
|
||||||
@@ -218,4 +223,8 @@ export class TrayMain {
|
|||||||
private async fakePopup() {
|
private async fakePopup() {
|
||||||
await this.messagingService.send("loadurl", { url: "/passkeys", modal: true });
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,7 +97,7 @@ export abstract class Fido2UserInterfaceSession {
|
|||||||
*/
|
*/
|
||||||
confirmNewCredential: (
|
confirmNewCredential: (
|
||||||
params: NewCredentialParams,
|
params: NewCredentialParams,
|
||||||
) => Promise<{ cipherId: string; userVerified: boolean }>;
|
) => Promise<{ cipherId?: string; userVerified: boolean }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make sure that the vault is unlocked.
|
* Make sure that the vault is unlocked.
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./icon-button.module";
|
export * from "./icon-button.module";
|
||||||
|
export * from "./icon-button.component";
|
||||||
|
|||||||
Reference in New Issue
Block a user