diff --git a/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts new file mode 100644 index 00000000000..4d07ab3c767 --- /dev/null +++ b/apps/browser/src/auth/services/auth-request-answering/extension-auth-request-answering.service.ts @@ -0,0 +1,94 @@ +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 { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; +import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service"; +import { PendingAuthRequestsStateService } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ActionsService } from "@bitwarden/common/platform/actions"; +import { + ButtonLocation, + SystemNotificationEvent, + SystemNotificationsService, +} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; +import { UserId } from "@bitwarden/user-core"; + +export class ExtensionAuthRequestAnsweringService + 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 actionService: ActionsService, + private readonly i18nService: I18nService, + private readonly platformUtilsService: PlatformUtilsService, + private readonly systemNotificationsService: SystemNotificationsService, + ) { + super( + accountService, + authService, + masterPasswordService, + messagingService, + pendingAuthRequestsState, + ); + } + + override async receivedPendingAuthRequest(userId: UserId, authRequestId?: string): Promise { + if (!authRequestId) { + throw new Error("authRequestId not found."); + } + + // 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 this.systemNotificationsService.create({ + id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`, // the underscore is an important delimiter. + title: this.i18nService.t("accountAccessRequested"), + body: this.i18nService.t("confirmAccessAttempt", emailForUser), + buttons: [], + }); + } + } + + async userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise { + const meetsBasicConditions = await super.userMeetsConditionsToShowApprovalDialog(userId); + + // To show an approval dialog immediately on Extension, the popup must be open. + const isPopupOpen = await this.platformUtilsService.isPopupOpen(); + const meetsExtensionConditions = meetsBasicConditions && isPopupOpen; + + return meetsExtensionConditions; + } + + /** + * When a system notification is clicked, this function is used to process that event. + * + * @param event The event passed in. Check initNotificationSubscriptions in main.background.ts. + */ + async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise { + if (event.buttonIdentifier === ButtonLocation.NotificationButton) { + await this.systemNotificationsService.clear({ + id: `${event.id}`, + }); + await this.actionService.openPopup(); + } + } +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 4cd61ebead1..ba785b23a66 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -39,7 +39,6 @@ import { DefaultPolicyService } from "@bitwarden/common/admin-console/services/p import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; @@ -51,7 +50,6 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from "@ import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; -import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; @@ -265,6 +263,7 @@ import { VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +import { ExtensionAuthRequestAnsweringService } from "../auth/services/auth-request-answering/extension-auth-request-answering.service"; import { AuthStatusBadgeUpdaterService } from "../auth/services/auth-status-badge-updater.service"; import { OverlayNotificationsBackground as OverlayNotificationsBackgroundInterface } from "../autofill/background/abstractions/overlay-notifications.background"; import { OverlayBackground as OverlayBackgroundInterface } from "../autofill/background/abstractions/overlay.background"; @@ -375,7 +374,7 @@ export default class MainBackground { serverNotificationsService: ServerNotificationsService; systemNotificationService: SystemNotificationsService; actionsService: ActionsService; - authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction; + authRequestAnsweringService: ExtensionAuthRequestAnsweringService; stateService: StateServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction; @@ -1173,14 +1172,14 @@ export default class MainBackground { this.pendingAuthRequestStateService = new PendingAuthRequestsStateService(this.stateProvider); - this.authRequestAnsweringService = new AuthRequestAnsweringService( + this.authRequestAnsweringService = new ExtensionAuthRequestAnsweringService( this.accountService, - this.actionsService, this.authService, - this.i18nService, this.masterPasswordService, this.messagingService, this.pendingAuthRequestStateService, + this.actionsService, + this.i18nService, this.platformUtilsService, this.systemNotificationService, ); diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 998531488d3..3cd0b072841 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -37,7 +37,7 @@ import { UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +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 { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -114,7 +114,7 @@ export class AppComponent implements OnInit, OnDestroy { private logService: LogService, private authRequestService: AuthRequestServiceAbstraction, private pendingAuthRequestsState: PendingAuthRequestsStateService, - private authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction, + private authRequestAnsweringService: AuthRequestAnsweringService, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 7a10dc2343f..4f5174c6a2e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -37,6 +37,7 @@ import { SsoUrlService, LogoutService, } from "@bitwarden/auth/common"; +import { ExtensionAuthRequestAnsweringService } from "@bitwarden/browser/auth/services/auth-request-answering/extension-auth-request-answering.service"; import { ExtensionNewDeviceVerificationComponentService } from "@bitwarden/browser/auth/services/new-device-verification/extension-new-device-verification-component.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; @@ -45,13 +46,12 @@ import { AccountService, AccountService as AccountServiceAbstraction, } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +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 { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AutofillSettingsService, @@ -484,16 +484,16 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: AuthRequestAnsweringServiceAbstraction, - useClass: AuthRequestAnsweringService, + provide: AuthRequestAnsweringService, + useClass: ExtensionAuthRequestAnsweringService, deps: [ AccountServiceAbstraction, - ActionsService, AuthService, - I18nServiceAbstraction, MasterPasswordServiceAbstraction, MessagingService, PendingAuthRequestsStateService, + ActionsService, + I18nServiceAbstraction, PlatformUtilsService, SystemNotificationsService, ], diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index 1c2d3aa464d..94c31079da9 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -31,6 +31,7 @@ import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; import { + AuthRequestServiceAbstraction, DESKTOP_SSO_CALLBACK, LogoutReason, UserDecryptionOptionsServiceAbstraction, @@ -44,6 +45,7 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service" import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; import { KeyConnectorService } from "@bitwarden/common/key-management/key-connector/abstractions/key-connector.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; @@ -133,6 +135,7 @@ export class AppComponent implements OnInit, OnDestroy { private isIdle = false; private activeUserId: UserId = null; private activeSimpleDialog: DialogRef = null; + private processingPendingAuth = false; private destroy$ = new Subject(); @@ -179,6 +182,8 @@ export class AppComponent implements OnInit, OnDestroy { private restrictedItemTypesService: RestrictedItemTypesService, private readonly tokenService: TokenService, private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy, + private pendingAuthRequestsState: PendingAuthRequestsStateService, + private authRequestService: AuthRequestServiceAbstraction, ) { this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe(); @@ -489,13 +494,49 @@ export class AppComponent implements OnInit, OnDestroy { await this.checkForSystemTimeout(VaultTimeoutStringType.OnIdle); break; case "openLoginApproval": - if (message.notificationId != null) { - this.dialogService.closeAll(); - const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { - notificationId: message.notificationId, - }); - await firstValueFrom(dialogRef.closed); + if (this.processingPendingAuth) { + return; } + + this.processingPendingAuth = true; + + try { + // Always query server for all pending requests and open a dialog for each + const pendingList = await firstValueFrom( + this.authRequestService.getPendingAuthRequests$(), + ); + if (Array.isArray(pendingList) && pendingList.length > 0) { + const respondedIds = new Set(); + for (const req of pendingList) { + if (req?.id == null) { + continue; + } + const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { + notificationId: req.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + respondedIds.add(req.id); + if (respondedIds.size === pendingList.length && this.activeUserId != null) { + await this.pendingAuthRequestsState.clear(this.activeUserId); + } + } + } + } + } finally { + this.processingPendingAuth = false; + } + + // if (message.notificationId != null) { + // this.dialogService.closeAll(); + // const dialogRef = LoginApprovalDialogComponent.open(this.dialogService, { + // notificationId: message.notificationId, + // }); + // await firstValueFrom(dialogRef.closed); + // } + break; case "redrawMenu": await this.updateAppMenu(); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 29b84ddc382..6dff930f9df 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -51,9 +51,11 @@ import { } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/account-api.service"; 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 { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-auth-request-answering.service"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; @@ -410,6 +412,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebVaultPremiumUpgradePromptService, deps: [DialogService, Router], }), + safeProvider({ + provide: AuthRequestAnsweringService, + useClass: NoopAuthRequestAnsweringService, + deps: [], + }), ]; @NgModule({ diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 164f125a5de..068b318ce2e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -89,7 +89,7 @@ import { InternalAccountService, } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; @@ -108,7 +108,7 @@ import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/aut import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; -import { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-auth-request-answering.service"; +import { DefaultAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/default-auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; @@ -992,9 +992,15 @@ const safeProviders: SafeProvider[] = [ deps: [StateProvider], }), safeProvider({ - provide: AuthRequestAnsweringServiceAbstraction, - useClass: NoopAuthRequestAnsweringService, - deps: [], + provide: AuthRequestAnsweringService, + useClass: DefaultAuthRequestAnsweringService, + deps: [ + AccountServiceAbstraction, + AuthServiceAbstraction, + MasterPasswordServiceAbstraction, + MessagingServiceAbstraction, + PendingAuthRequestsStateService, + ], }), safeProvider({ provide: ServerNotificationsService, @@ -1012,7 +1018,7 @@ const safeProviders: SafeProvider[] = [ SignalRConnectionService, AuthServiceAbstraction, WebPushConnectionService, - AuthRequestAnsweringServiceAbstraction, + AuthRequestAnsweringService, ConfigService, ], }), diff --git a/libs/common/src/auth/abstractions/auth-request-answering/README.md b/libs/common/src/auth/abstractions/auth-request-answering/README.md index 9a24f095d70..edc368fd91b 100644 --- a/libs/common/src/auth/abstractions/auth-request-answering/README.md +++ b/libs/common/src/auth/abstractions/auth-request-answering/README.md @@ -1,7 +1,6 @@ # Auth Request Answering Service -This feature is to allow for the taking of auth requests that are received via websockets by the background service to -be acted on when the user loads up a client. Currently only implemented with the browser client. +This feature is to allow for the taking of auth requests that are received via websockets to be acted on when the user loads up a client. See diagram for the high level picture of how this is wired up. diff --git a/libs/common/src/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction.ts b/libs/common/src/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction.ts index f45cb34496e..4f6ff8c17a8 100644 --- a/libs/common/src/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction.ts @@ -1,7 +1,6 @@ -import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { UserId } from "@bitwarden/user-core"; -export abstract class AuthRequestAnsweringServiceAbstraction { +export abstract class AuthRequestAnsweringService { /** * Tries to either display the dialog for the user or will preserve its data and show it at a * later time. Even in the event the dialog is shown immediately, this will write to global state @@ -13,14 +12,20 @@ export abstract class AuthRequestAnsweringServiceAbstraction { * @param userId The UserId that the auth request is for. * @param authRequestId The id of the auth request that is to be processed. */ - abstract receivedPendingAuthRequest(userId: UserId, authRequestId: string): Promise; + abstract receivedPendingAuthRequest(userId: UserId, authRequestId?: string): Promise; /** - * When a system notification is clicked, this function is used to process that event. + * Confirms whether or not the user meets the conditions required to show an approval + * dialog immediately. Those conditions are: + * - User must be Unlocked + * - User must be the active user + * - User must not be required to set/change their password * - * @param event The event passed in. Check initNotificationSubscriptions in main.background.ts. + * @param userId the UserId that the auth request is for. + * @returns boolean stating whether or not the user meets conditions necessary to show + * an approval dialog immediately. */ - abstract handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise; + abstract userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise; /** * Process notifications that have been received but didn't meet the conditions to display the diff --git a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts b/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts index 0b12e1cb661..d19979c5b43 100644 --- a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts +++ b/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.spec.ts @@ -17,7 +17,8 @@ import { } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { UserId } from "@bitwarden/user-core"; -import { AuthRequestAnsweringService } from "./auth-request-answering.service"; +import { AuthRequestAnsweringService } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; + import { PendingAuthRequestsStateService } from "./pending-auth-requests.state"; describe("AuthRequestAnsweringService", () => { diff --git a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.ts b/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.ts deleted file mode 100644 index 834d6ac7bcc..00000000000 --- a/libs/common/src/auth/services/auth-request-answering/auth-request-answering.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { firstValueFrom } from "rxjs"; - -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; -import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service"; -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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ActionsService } from "@bitwarden/common/platform/actions"; -import { - ButtonLocation, - SystemNotificationEvent, - SystemNotificationsService, -} from "@bitwarden/common/platform/system-notifications/system-notifications.service"; -import { UserId } from "@bitwarden/user-core"; - -import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; - -import { - PendingAuthRequestsStateService, - PendingAuthUserMarker, -} from "./pending-auth-requests.state"; - -export class AuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction { - constructor( - private readonly accountService: AccountService, - private readonly actionService: ActionsService, - private readonly authService: AuthService, - private readonly i18nService: I18nService, - private readonly masterPasswordService: MasterPasswordServiceAbstraction, - private readonly messagingService: MessagingService, - private readonly pendingAuthRequestsState: PendingAuthRequestsStateService, - private readonly platformUtilsService: PlatformUtilsService, - private readonly systemNotificationsService: SystemNotificationsService, - ) {} - - async receivedPendingAuthRequest(userId: UserId, authRequestId: string): Promise { - const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); - const activeUserId: UserId | null = await firstValueFrom( - this.accountService.activeAccount$.pipe(getOptionalUserId), - ); - const forceSetPasswordReason = await firstValueFrom( - this.masterPasswordService.forceSetPasswordReason$(userId), - ); - const popupOpen = await this.platformUtilsService.isPopupOpen(); - - // Always persist the pending marker for this user to global state. - await this.pendingAuthRequestsState.add(userId); - - // These are the conditions we are looking for to know if the extension is in a state to show - // the approval dialog. - const userIsAvailableToReceiveAuthRequest = - popupOpen && - authStatus === AuthenticationStatus.Unlocked && - activeUserId === userId && - forceSetPasswordReason === ForceSetPasswordReason.None; - - if (!userIsAvailableToReceiveAuthRequest) { - // Get the user's email to include in the system notification - const accounts = await firstValueFrom(this.accountService.accounts$); - const emailForUser = accounts[userId].email; - - await this.systemNotificationsService.create({ - id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`, // the underscore is an important delimiter. - title: this.i18nService.t("accountAccessRequested"), - body: this.i18nService.t("confirmAccessAttempt", emailForUser), - buttons: [], - }); - return; - } - - // Popup is open and conditions are met; open dialog immediately for this request - this.messagingService.send("openLoginApproval"); - } - - async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise { - if (event.buttonIdentifier === ButtonLocation.NotificationButton) { - await this.systemNotificationsService.clear({ - id: `${event.id}`, - }); - await this.actionService.openPopup(); - } - } - - async processPendingAuthRequests(): Promise { - // 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"); - } - } - } -} diff --git a/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.ts b/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.ts new file mode 100644 index 00000000000..f98156b6b67 --- /dev/null +++ b/libs/common/src/auth/services/auth-request-answering/default-auth-request-answering.service.ts @@ -0,0 +1,78 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service"; +import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/user-core"; + +import { AuthRequestAnsweringService } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; + +import { + PendingAuthRequestsStateService, + PendingAuthUserMarker, +} from "./pending-auth-requests.state"; + +export class DefaultAuthRequestAnsweringService implements AuthRequestAnsweringService { + constructor( + protected readonly accountService: AccountService, + protected readonly authService: AuthService, + protected readonly masterPasswordService: MasterPasswordServiceAbstraction, + protected readonly messagingService: MessagingService, + protected readonly pendingAuthRequestsState: PendingAuthRequestsStateService, + ) {} + + async receivedPendingAuthRequest(userId: UserId): Promise { + // 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"); + } + } + + async userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise { + const authStatus = await firstValueFrom(this.authService.activeAccountStatus$); + const activeUserId: UserId | null = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + const forceSetPasswordReason = await firstValueFrom( + this.masterPasswordService.forceSetPasswordReason$(userId), + ); + + const meetsConditions = + authStatus === AuthenticationStatus.Unlocked && + activeUserId === userId && + forceSetPasswordReason === ForceSetPasswordReason.None; + + return meetsConditions; + } + + async processPendingAuthRequests(): Promise { + // 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"); + } + } + } +} diff --git a/libs/common/src/auth/services/auth-request-answering/noop-auth-request-answering.service.ts b/libs/common/src/auth/services/auth-request-answering/noop-auth-request-answering.service.ts index 730362adfed..295ff0270ec 100644 --- a/libs/common/src/auth/services/auth-request-answering/noop-auth-request-answering.service.ts +++ b/libs/common/src/auth/services/auth-request-answering/noop-auth-request-answering.service.ts @@ -1,14 +1,15 @@ -import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service"; import { UserId } from "@bitwarden/user-core"; -import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction"; -export class NoopAuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction { +export class NoopAuthRequestAnsweringService implements AuthRequestAnsweringService { constructor() {} async receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise {} - async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise {} + async userMeetsConditionsToShowApprovalDialog(userId: UserId): Promise { + return false; + } async processPendingAuthRequests(): Promise {} } diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts index 47af8f5e00c..7d4d56ff316 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.ts @@ -15,7 +15,7 @@ import { // This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop. // eslint-disable-next-line no-restricted-imports import { LogoutReason } from "@bitwarden/auth/common"; -import { AuthRequestAnsweringServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; +import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { trackedMerge } from "@bitwarden/common/platform/misc"; @@ -64,7 +64,7 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer private readonly signalRConnectionService: SignalRConnectionService, private readonly authService: AuthService, private readonly webPushConnectionService: WebPushConnectionService, - private readonly authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction, + private readonly authRequestAnsweringService: AuthRequestAnsweringService, private readonly configService: ConfigService, ) { this.notifications$ = this.configService @@ -293,9 +293,9 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer * pending auth request to process at a time, so this second call will not cause any * duplicate processing conflicts on Extension. */ - this.messagingService.send("openLoginApproval", { - notificationId: notification.payload.id, - }); + // this.messagingService.send("openLoginApproval", { + // notificationId: notification.payload.id, + // }); break; case NotificationType.SyncOrganizationStatusChanged: await this.syncService.fullSync(true);