1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00

Autofill/pm 17444 use reprompt (#14004)

* 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

* Added support for handling a locked vault

* 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

* Add master password reprompt on passkey create

* Followup to remove comments and timeouts and handle errors

* Address lint issue by using `takeUntilDestroyed`

* Add MP prompt to cipher selection

* Change how timeout is handled

* Include `of` from rxjs

* Hide blue header for passkey popouts (#14095)

* Hide blue header for passkey popouts

* Fix issue with test

* Fix ngOnDestroy complaint

* Import OnDestroy correctly

* Only require master password if item requires it

---------

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:
Jeffrey Holland
2025-04-11 09:41:30 +02:00
committed by GitHub
parent b7c2419aed
commit b62220ace1
13 changed files with 89 additions and 19 deletions

View File

@@ -86,7 +86,7 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
<ng-template #exportVault></ng-template>
<ng-template #appGenerator></ng-template>
<ng-template #loginApproval></ng-template>
<app-header></app-header>
<app-header *ngIf="showHeader$ | async"></app-header>
<div id="container">
<div class="loading" *ngIf="loading">
@@ -112,6 +112,7 @@ export class AppComponent implements OnInit, OnDestroy {
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
loginApprovalModalRef: ViewContainerRef;
showHeader$ = this.accountService.showHeader$;
loading = false;
private lastActivity: Date = null;

View File

@@ -97,7 +97,7 @@ export class DesktopFido2UserInterfaceSession implements Fido2UserInterfaceSessi
private updatedCipher: CipherView;
private rpId = new BehaviorSubject<string>("");
private rpId = new BehaviorSubject<string>(null);
private availableCipherIdsSubject = new BehaviorSubject<string[]>([""]);
/**
* Observable that emits available cipher IDs once they're confirmed by the UI

View File

@@ -3,7 +3,7 @@
disableMargin
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
>
<bit-section-header class="tw-bg-background">
<bit-section-header class="passkey-header 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>
@@ -16,7 +16,7 @@
type="button"
bitIconButton="bwi-close"
slot="end"
class="tw-mb-4 tw-mr-2"
class="passkey-header-close tw-mb-4 tw-mr-2"
(click)="closeModal()"
>
Close

View File

@@ -1,5 +1,5 @@
import { CommonModule } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { Component, OnInit, OnDestroy } from "@angular/core";
import { RouterModule, Router } from "@angular/router";
import { BehaviorSubject, firstValueFrom, map, Observable } from "rxjs";
@@ -22,6 +22,7 @@ import {
SectionHeaderComponent,
BitIconButtonComponent,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import {
DesktopFido2UserInterfaceService,
@@ -47,7 +48,7 @@ import { DesktopSettingsService } from "../../../platform/services/desktop-setti
],
templateUrl: "fido2-create.component.html",
})
export class Fido2CreateComponent implements OnInit {
export class Fido2CreateComponent implements OnInit, OnDestroy {
session?: DesktopFido2UserInterfaceSession = null;
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
@@ -61,10 +62,12 @@ export class Fido2CreateComponent implements OnInit {
private readonly dialogService: DialogService,
private readonly domainSettingsService: DomainSettingsService,
private readonly logService: LogService,
private readonly passwordRepromptService: PasswordRepromptService,
private readonly router: Router,
) {}
async ngOnInit() {
await this.accountService.setShowHeader(false);
this.session = this.fido2UserInterfaceService.getCurrentSession();
const rpid = await this.session.getRpId();
const equivalentDomains = await firstValueFrom(
@@ -92,8 +95,16 @@ export class Fido2CreateComponent implements OnInit {
.catch((error) => this.logService.error(error));
}
async ngOnDestroy() {
await this.accountService.setShowHeader(true);
}
async addPasskeyToCipher(cipher: CipherView) {
this.session.notifyConfirmCreateCredential(true, cipher);
const userVerified = cipher.reprompt
? await this.passwordRepromptService.showPasswordPrompt()
: true;
this.session.notifyConfirmCreateCredential(userVerified, cipher);
}
async confirmPasskey() {

View File

@@ -3,7 +3,7 @@
disableMargin
class="tw-border-0 tw-border-b tw-border-solid tw-border-secondary-300"
>
<bit-section-header class="tw-bg-background">
<bit-section-header class="passkey-header 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>
@@ -13,7 +13,7 @@
type="button"
bitIconButton="bwi-close"
slot="end"
class="tw-mb-4 tw-mr-2"
class="passkey-header-close tw-mb-4 tw-mr-2"
(click)="closeModal()"
>
Close
@@ -23,7 +23,7 @@
<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)">
<button type="button" bit-item-content (click)="chooseCipher(c)">
<app-vault-icon [cipher]="c" slot="start"></app-vault-icon>
<button bitLink [title]="c.name" type="button">
{{ c.name }}

View File

@@ -1,14 +1,14 @@
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 { firstValueFrom, map, BehaviorSubject, Observable, Subject, takeUntil } 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 { CipherRepromptType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
@@ -21,6 +21,7 @@ import {
BitIconButtonComponent,
SectionHeaderComponent,
} from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import {
DesktopFido2UserInterfaceService,
@@ -48,6 +49,7 @@ import { DesktopSettingsService } from "../../platform/services/desktop-settings
})
export class Fido2VaultComponent implements OnInit, OnDestroy {
session?: DesktopFido2UserInterfaceSession = null;
private destroy$ = new Subject<void>();
private ciphersSubject = new BehaviorSubject<CipherView[]>([]);
ciphers$: Observable<CipherView[]> = this.ciphersSubject.asObservable();
private cipherIdsSubject = new BehaviorSubject<string[]>([]);
@@ -60,10 +62,12 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
private readonly cipherService: CipherService,
private readonly accountService: AccountService,
private readonly logService: LogService,
private readonly passwordRepromptService: PasswordRepromptService,
private readonly router: Router,
) {}
async ngOnInit() {
await this.accountService.setShowHeader(false);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
@@ -71,22 +75,30 @@ export class Fido2VaultComponent implements OnInit, OnDestroy {
this.session = this.fido2UserInterfaceService.getCurrentSession();
this.cipherIds$ = this.session?.availableCipherIds$;
this.cipherIds$.pipe(takeUntilDestroyed()).subscribe((cipherIds) => {
this.cipherIds$.pipe(takeUntil(this.destroy$)).subscribe((cipherIds) => {
this.cipherService
.getAllDecrypted(activeUserId)
.getAllDecryptedForIds(activeUserId, cipherIds || [])
.then((ciphers) => {
this.ciphersSubject.next(ciphers.filter((cipher) => cipherIds.includes(cipher.id)));
this.ciphersSubject.next(ciphers);
})
.catch((error) => this.logService.error(error));
});
}
ngOnDestroy() {
async ngOnDestroy() {
await this.accountService.setShowHeader(true);
this.cipherIdsSubject.complete(); // Clean up the BehaviorSubject
}
async chooseCipher(cipherId: string) {
this.session?.confirmChosenCipher(cipherId, true);
async chooseCipher(cipher: CipherView) {
if (
cipher.reprompt !== CipherRepromptType.None &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
this.session?.confirmChosenCipher(cipher.id, false);
} else {
this.session?.confirmChosenCipher(cipher.id, true);
}
await this.router.navigate(["/"]);
await this.desktopSettingsService.setModalMode(false);

View File

@@ -232,3 +232,11 @@
font-size: $font-size-small;
}
}
.passkey-header {
-webkit-app-region: drag;
}
.passkey-header-close {
-webkit-app-region: no-drag;
}

View File

@@ -37,6 +37,8 @@ export class FakeAccountService implements AccountService {
accountActivitySubject = new ReplaySubject<Record<UserId, Date>>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
accountVerifyDevicesSubject = new ReplaySubject<boolean>(1);
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
showHeaderSubject = new ReplaySubject<boolean>(1);
private _activeUserId: UserId;
get activeUserId() {
return this._activeUserId;
@@ -55,6 +57,7 @@ export class FakeAccountService implements AccountService {
}),
);
}
showHeader$ = this.showHeaderSubject.asObservable();
get nextUpAccount$(): Observable<Account> {
return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe(
map(([accounts, activeAccount, sortedUserIds]) => {
@@ -114,6 +117,10 @@ export class FakeAccountService implements AccountService {
this.accountsSubject.next(updated);
await this.mock.clean(userId);
}
async setShowHeader(value: boolean): Promise<void> {
this.showHeaderSubject.next(value);
}
}
const loggedOutInfo: AccountInfo = {

View File

@@ -49,6 +49,7 @@ export abstract class AccountService {
sortedUserIds$: Observable<UserId[]>;
/** Next account that is not the current active account */
nextUpAccount$: Observable<Account>;
showHeader$: Observable<boolean>;
/**
* Updates the `accounts$` observable with the new account data.
*
@@ -102,6 +103,11 @@ export abstract class AccountService {
* @param lastActivity
*/
abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise<void>;
/**
* Show the account switcher.
* @param value
*/
abstract setShowHeader(visible: boolean): Promise<void>;
}
export abstract class InternalAccountService extends AccountService {

View File

@@ -429,6 +429,16 @@ describe("accountService", () => {
},
);
});
describe("setShowHeader", () => {
it("should update _showHeader$ when setShowHeader is called", async () => {
expect(sut["_showHeader$"].value).toBe(true);
await sut.setShowHeader(false);
expect(sut["_showHeader$"].value).toBe(false);
});
});
});
});

View File

@@ -6,6 +6,7 @@ import {
distinctUntilChanged,
shareReplay,
combineLatest,
BehaviorSubject,
Observable,
switchMap,
filter,
@@ -84,6 +85,7 @@ export const getOptionalUserId = map<Account | null, UserId | null>(
export class AccountServiceImplementation implements InternalAccountService {
private accountsState: GlobalState<Record<UserId, AccountInfo>>;
private activeAccountIdState: GlobalState<UserId | undefined>;
private _showHeader$ = new BehaviorSubject<boolean>(true);
accounts$: Observable<Record<UserId, AccountInfo>>;
activeAccount$: Observable<Account | null>;
@@ -91,6 +93,7 @@ export class AccountServiceImplementation implements InternalAccountService {
accountVerifyNewDeviceLogin$: Observable<boolean>;
sortedUserIds$: Observable<UserId[]>;
nextUpAccount$: Observable<Account>;
showHeader$ = this._showHeader$.asObservable();
constructor(
private messagingService: MessagingService,
@@ -260,6 +263,10 @@ export class AccountServiceImplementation implements InternalAccountService {
}
}
async setShowHeader(visible: boolean): Promise<void> {
this._showHeader$.next(visible);
}
private async setAccountInfo(userId: UserId, update: Partial<AccountInfo>): Promise<void> {
function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo {
return { ...oldAccountInfo, ...update };

View File

@@ -56,6 +56,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchStrategySetting,
): Promise<CipherView[]>;
getAllDecryptedForIds: (userId: UserId, ids: string[]) => Promise<CipherView[]>;
abstract filterCiphersForUrl(
ciphers: CipherView[],
url: string,

View File

@@ -513,7 +513,7 @@ export class CipherService implements CipherServiceAbstraction {
async getAllDecryptedForUrl(
url: string,
userId: UserId,
userId?: UserId,
includeOtherTypes?: CipherType[],
defaultMatch: UriMatchStrategySetting = null,
): Promise<CipherView[]> {
@@ -521,6 +521,13 @@ export class CipherService implements CipherServiceAbstraction {
return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch);
}
async getAllDecryptedForIds(userId: UserId, ids: string[]): Promise<CipherView[]> {
if (userId) {
const ciphers = await this.getAllDecrypted(userId);
return ciphers.filter((cipher) => ids.includes(cipher.id));
}
}
async filterCiphersForUrl(
ciphers: CipherView[],
url: string,