1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 04:33:38 +00:00

create DesktopAuthRequestAnsweringService

This commit is contained in:
rr-bw
2025-10-17 16:13:49 -07:00
parent 02a04de1d5
commit ad43e635b6
13 changed files with 124 additions and 219 deletions

View File

@@ -5,7 +5,6 @@ import { Router } from "@angular/router";
import { Subject, merge } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { LoginApprovalDialogComponentServiceAbstraction } from "@bitwarden/angular/auth/login-approval";
import { SetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
import {
@@ -45,12 +44,14 @@ import {
AccountService,
AccountService as AccountServiceAbstraction,
} from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import {
AuthService,
AuthService as AuthServiceAbstraction,
} from "@bitwarden/common/auth/abstractions/auth.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { ClientType } from "@bitwarden/common/enums";
@@ -59,7 +60,10 @@ import { KeyGenerationService } from "@bitwarden/common/key-management/crypto";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { WebCryptoFunctionService } from "@bitwarden/common/key-management/crypto/services/web-crypto-function.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import {
InternalMasterPasswordServiceAbstraction,
MasterPasswordServiceAbstraction,
} from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
import {
@@ -110,8 +114,8 @@ import { LockComponentService } from "@bitwarden/key-management-ui";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service";
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
import { DesktopAuthRequestAnsweringService } from "../../auth/services/auth-request-answering/desktop-auth-request-answering.service";
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
@@ -447,11 +451,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultSsoComponentService,
deps: [],
}),
safeProvider({
provide: LoginApprovalDialogComponentServiceAbstraction,
useClass: DesktopLoginApprovalDialogComponentService,
deps: [I18nServiceAbstraction],
}),
safeProvider({
provide: SshImportPromptService,
useClass: DefaultSshImportPromptService,
@@ -476,6 +475,18 @@ const safeProviders: SafeProvider[] = [
useClass: DesktopAutotypeDefaultSettingPolicy,
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
}),
safeProvider({
provide: AuthRequestAnsweringService,
useClass: DesktopAuthRequestAnsweringService,
deps: [
AccountServiceAbstraction,
AuthService,
MasterPasswordServiceAbstraction,
MessagingServiceAbstraction,
PendingAuthRequestsStateService,
I18nServiceAbstraction,
],
}),
];
@NgModule({

View File

@@ -1,91 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { Subject } from "rxjs";
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
import { DesktopLoginApprovalDialogComponentService } from "./desktop-login-approval-dialog-component.service";
describe("DesktopLoginApprovalDialogComponentService", () => {
let service: DesktopLoginApprovalDialogComponentService;
let i18nService: MockProxy<I18nServiceAbstraction>;
let originalIpc: any;
beforeEach(() => {
originalIpc = (global as any).ipc;
(global as any).ipc = {
auth: {
loginRequest: jest.fn(),
},
platform: {
isWindowVisible: jest.fn(),
},
};
i18nService = mock<I18nServiceAbstraction>({
t: jest.fn(),
userSetLocale$: new Subject<string>(),
locale$: new Subject<string>(),
});
TestBed.configureTestingModule({
providers: [
DesktopLoginApprovalDialogComponentService,
{ provide: I18nServiceAbstraction, useValue: i18nService },
],
});
service = TestBed.inject(DesktopLoginApprovalDialogComponentService);
});
afterEach(() => {
jest.clearAllMocks();
(global as any).ipc = originalIpc;
});
it("is created successfully", () => {
expect(service).toBeTruthy();
});
it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => {
const title = "Log in requested";
const email = "test@bitwarden.com";
const message = `Confirm access attempt for ${email}`;
const closeText = "Close";
const loginApprovalDialogComponent = { email } as LoginApprovalDialogComponent;
i18nService.t.mockImplementation((key: string) => {
switch (key) {
case "accountAccessRequested":
return title;
case "confirmAccessAttempt":
return message;
case "close":
return closeText;
default:
return "";
}
});
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(false);
jest.spyOn(ipc.auth, "loginRequest").mockResolvedValue();
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).toHaveBeenCalledWith(title, message, closeText);
});
it("does not call ipc.auth.loginRequest when window is visible", async () => {
const loginApprovalDialogComponent = {
email: "test@bitwarden.com",
} as LoginApprovalDialogComponent;
jest.spyOn(ipc.platform, "isWindowVisible").mockResolvedValue(true);
jest.spyOn(ipc.auth, "loginRequest");
await service.showLoginRequestedAlertIfWindowNotVisible(loginApprovalDialogComponent.email);
expect(ipc.auth.loginRequest).not.toHaveBeenCalled();
});
});

View File

@@ -1,28 +0,0 @@
import { Injectable } from "@angular/core";
import {
DefaultLoginApprovalDialogComponentService,
LoginApprovalDialogComponentServiceAbstraction,
} from "@bitwarden/angular/auth/login-approval";
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
@Injectable()
export class DesktopLoginApprovalDialogComponentService
extends DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
constructor(private i18nService: I18nServiceAbstraction) {
super();
}
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
const isVisible = await ipc.platform.isWindowVisible();
if (!isVisible) {
await ipc.auth.loginRequest(
this.i18nService.t("accountAccessRequested"),
this.i18nService.t("confirmAccessAttempt", email),
this.i18nService.t("close"),
);
}
}
}

View File

@@ -0,0 +1,96 @@
import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service";
import {
PendingAuthRequestsStateService,
PendingAuthUserMarker,
} from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
import { UserId } from "@bitwarden/user-core";
export class DesktopAuthRequestAnsweringService
extends DefaultAuthRequestAnsweringService
implements AuthRequestAnsweringService
{
constructor(
protected readonly accountService: AccountService,
protected readonly authService: AuthService,
protected readonly masterPasswordService: MasterPasswordServiceAbstraction,
protected readonly messagingService: MessagingService,
protected readonly pendingAuthRequestsState: PendingAuthRequestsStateService,
private readonly i18nService: I18nService,
) {
super(
accountService,
authService,
masterPasswordService,
messagingService,
pendingAuthRequestsState,
);
}
override async receivedPendingAuthRequest(userId: UserId): Promise<void> {
// Always persist the pending marker for this user to global state.
await this.pendingAuthRequestsState.add(userId);
const userIsAvailableToViewDialog = await this.userMeetsConditionsToShowApprovalDialog(userId);
if (userIsAvailableToViewDialog) {
// Send message to open dialog immediately for this request
this.messagingService.send("openLoginApproval");
} else {
// Create a system notification
const accounts = await firstValueFrom(this.accountService.accounts$);
const emailForUser = accounts[userId].email;
await ipc.auth.loginRequest(
this.i18nService.t("accountAccessRequested"),
this.i18nService.t("confirmAccessAttempt", emailForUser),
this.i18nService.t("close"),
);
}
}
async userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise<boolean> {
const meetsBasicConditions = await super.userMeetsConditionsToShowApprovalDialog(userId);
// To show an approval dialog immediately on Desktop, the window must be open.
const isWindowVisible = await ipc.platform.isWindowVisible();
const meetsExtensionConditions = meetsBasicConditions && isWindowVisible;
return meetsExtensionConditions;
}
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent) {
throw new Error("handleAuthRequestNotificationClicked() not implemented for this client");
}
async processPendingAuthRequests(): Promise<void> {
// Prune any stale pending requests (older than 15 minutes)
// This comes from GlobalSettings.cs
// public TimeSpan UserRequestExpiration { get; set; } = TimeSpan.FromMinutes(15);
const fifteenMinutesMs = 15 * 60 * 1000;
await this.pendingAuthRequestsState.pruneOlderThan(fifteenMinutesMs);
const pendingAuthRequestsInState: PendingAuthUserMarker[] =
(await firstValueFrom(this.pendingAuthRequestsState.getAll$())) ?? [];
if (pendingAuthRequestsInState.length > 0) {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const pendingAuthRequestsForActiveUser = pendingAuthRequestsInState.some(
(e) => e.userId === activeUserId,
);
if (pendingAuthRequestsForActiveUser) {
this.messagingService.send("openLoginApproval");
}
}
}
}

View File

@@ -320,14 +320,14 @@ export class VaultV2Component<C extends CipherViewLike>
this.searchBarService.setEnabled(true);
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
const authRequests = await firstValueFrom(
this.authRequestService.getLatestPendingAuthRequest$()!,
);
if (authRequests != null) {
this.messagingService.send("openLoginApproval", {
notificationId: authRequests.id,
});
}
// const authRequests = await firstValueFrom(
// this.authRequestService.getLatestPendingAuthRequest$()!,
// );
// if (authRequests != null) {
// this.messagingService.send("openLoginApproval", {
// notificationId: authRequests.id,
// });
// }
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getUserId),

View File

@@ -1,30 +0,0 @@
import { TestBed } from "@angular/core/testing";
import { DefaultLoginApprovalDialogComponentService } from "./default-login-approval-dialog-component.service";
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
describe("DefaultLoginApprovalDialogComponentService", () => {
let service: DefaultLoginApprovalDialogComponentService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [DefaultLoginApprovalDialogComponentService],
});
service = TestBed.inject(DefaultLoginApprovalDialogComponentService);
});
it("is created successfully", () => {
expect(service).toBeTruthy();
});
it("has showLoginRequestedAlertIfWindowNotVisible method that is a no-op", async () => {
const loginApprovalDialogComponent = {} as LoginApprovalDialogComponent;
const result = await service.showLoginRequestedAlertIfWindowNotVisible(
loginApprovalDialogComponent.email,
);
expect(result).toBeUndefined();
});
});

View File

@@ -1,16 +0,0 @@
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
/**
* Default implementation of the LoginApprovalDialogComponentServiceAbstraction.
*/
export class DefaultLoginApprovalDialogComponentService
implements LoginApprovalDialogComponentServiceAbstraction
{
/**
* No-op implementation of the showLoginRequestedAlertIfWindowNotVisible method.
* @returns
*/
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
return;
}
}

View File

@@ -1,3 +1 @@
export * from "./login-approval-dialog.component";
export * from "./login-approval-dialog-component.service.abstraction";
export * from "./default-login-approval-dialog-component.service";

View File

@@ -1,9 +0,0 @@
/**
* Abstraction for the LoginApprovalDialogComponent service.
*/
export abstract class LoginApprovalDialogComponentServiceAbstraction {
/**
* Shows a login requested alert if the window is not visible.
*/
abstract showLoginRequestedAlertIfWindowNotVisible: (email?: string) => Promise<void>;
}

View File

@@ -15,7 +15,6 @@ import { UserId } from "@bitwarden/common/types/guid";
import { DialogRef, DIALOG_DATA, ToastService } from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
import { LoginApprovalDialogComponent } from "./login-approval-dialog.component";
describe("LoginApprovalDialogComponent", () => {
@@ -67,10 +66,6 @@ describe("LoginApprovalDialogComponent", () => {
{ provide: LogService, useValue: logService },
{ provide: ToastService, useValue: toastService },
{ provide: ValidationService, useValue: validationService },
{
provide: LoginApprovalDialogComponentServiceAbstraction,
useValue: mock<LoginApprovalDialogComponentServiceAbstraction>(),
},
],
}).compileComponents();

View File

@@ -24,8 +24,6 @@ import {
} from "@bitwarden/components";
import { LogService } from "@bitwarden/logging";
import { LoginApprovalDialogComponentServiceAbstraction } from "./login-approval-dialog-component.service.abstraction";
const RequestTimeOut = 60000 * 15; // 15 Minutes
const RequestTimeUpdate = 60000 * 5; // 5 Minutes
@@ -55,7 +53,6 @@ export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
private devicesService: DevicesServiceAbstraction,
private dialogRef: DialogRef,
private i18nService: I18nService,
private loginApprovalDialogComponentService: LoginApprovalDialogComponentServiceAbstraction,
private logService: LogService,
private toastService: ToastService,
private validationService: ValidationService,
@@ -111,10 +108,6 @@ export class LoginApprovalDialogComponent implements OnInit, OnDestroy {
this.updateTimeText();
}, RequestTimeUpdate);
await this.loginApprovalDialogComponentService.showLoginRequestedAlertIfWindowNotVisible(
this.email,
);
this.loading = false;
}

View File

@@ -368,8 +368,6 @@ import {
VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core";
import { DefaultLoginApprovalDialogComponentService } from "../auth/login-approval/default-login-approval-dialog-component.service";
import { LoginApprovalDialogComponentServiceAbstraction } from "../auth/login-approval/login-approval-dialog-component.service.abstraction";
import { DefaultSetInitialPasswordService } from "../auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
import { SetInitialPasswordService } from "../auth/password-management/set-initial-password/set-initial-password.service.abstraction";
import { DeviceTrustToastService as DeviceTrustToastServiceAbstraction } from "../auth/services/device-trust-toast.service.abstraction";
@@ -1581,11 +1579,6 @@ const safeProviders: SafeProvider[] = [
useClass: DefaultSendPasswordService,
deps: [CryptoFunctionServiceAbstraction],
}),
safeProvider({
provide: LoginApprovalDialogComponentServiceAbstraction,
useClass: DefaultLoginApprovalDialogComponentService,
deps: [],
}),
safeProvider({
provide: LoginDecryptionOptionsService,
useClass: DefaultLoginDecryptionOptionsService,

View File

@@ -27,14 +27,7 @@ export class DefaultAuthRequestAnsweringService implements AuthRequestAnsweringS
) {}
async receivedPendingAuthRequest(userId: UserId): Promise<void> {
// Always persist the pending marker for this user to global state.
await this.pendingAuthRequestsState.add(userId);
const userIsAvailableToViewDialog = await this.userMeetsConditionsToShowApprovalDialog(userId);
if (userIsAvailableToViewDialog) {
// Send message to open dialog immediately for this request
this.messagingService.send("openLoginApproval");
}
throw new Error("receivedPendingAuthRequest() not implemented for this client");
}
async userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise<boolean> {