mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
Auth/pm 14943/auth request extension dialog approve (#16132)
* feat(notification-processing): [PM-19877] System Notification Implementation - Implemented the full feature set for device approval from extension. * test(notification-processing): [PM-19877] System Notification Implementation - Updated tests. --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
493788c9d0
commit
fe692acc07
@@ -2,7 +2,17 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import "core-js/proposals/explicit-resource-management";
|
import "core-js/proposals/explicit-resource-management";
|
||||||
|
|
||||||
import { filter, firstValueFrom, map, merge, Subject, switchMap, timeout } from "rxjs";
|
import {
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
from,
|
||||||
|
map,
|
||||||
|
merge,
|
||||||
|
Observable,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
timeout,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
|
||||||
import {
|
import {
|
||||||
@@ -42,6 +52,7 @@ import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-se
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.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 { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
@@ -152,6 +163,7 @@ import { SyncService } from "@bitwarden/common/platform/sync";
|
|||||||
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
// eslint-disable-next-line no-restricted-imports -- Needed for service creation
|
||||||
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
|
||||||
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/";
|
import { SystemNotificationsService } from "@bitwarden/common/platform/system-notifications/";
|
||||||
|
import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notifications/system-notifications.service";
|
||||||
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
|
import { UnsupportedSystemNotificationsService } from "@bitwarden/common/platform/system-notifications/unsupported-system-notifications.service";
|
||||||
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { DefaultThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||||
@@ -407,6 +419,7 @@ export default class MainBackground {
|
|||||||
individualVaultExportService: IndividualVaultExportServiceAbstraction;
|
individualVaultExportService: IndividualVaultExportServiceAbstraction;
|
||||||
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
organizationVaultExportService: OrganizationVaultExportServiceAbstraction;
|
||||||
vaultSettingsService: VaultSettingsServiceAbstraction;
|
vaultSettingsService: VaultSettingsServiceAbstraction;
|
||||||
|
pendingAuthRequestStateService: PendingAuthRequestsStateService;
|
||||||
biometricStateService: BiometricStateService;
|
biometricStateService: BiometricStateService;
|
||||||
biometricsService: BiometricsService;
|
biometricsService: BiometricsService;
|
||||||
stateEventRunnerService: StateEventRunnerService;
|
stateEventRunnerService: StateEventRunnerService;
|
||||||
@@ -1135,12 +1148,16 @@ export default class MainBackground {
|
|||||||
this.systemNotificationService = new UnsupportedSystemNotificationsService();
|
this.systemNotificationService = new UnsupportedSystemNotificationsService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.pendingAuthRequestStateService = new PendingAuthRequestsStateService(this.stateProvider);
|
||||||
|
|
||||||
this.authRequestAnsweringService = new AuthRequestAnsweringService(
|
this.authRequestAnsweringService = new AuthRequestAnsweringService(
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.actionsService,
|
this.actionsService,
|
||||||
this.authService,
|
this.authService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
|
this.messagingService,
|
||||||
|
this.pendingAuthRequestStateService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.systemNotificationService,
|
this.systemNotificationService,
|
||||||
);
|
);
|
||||||
@@ -1421,7 +1438,7 @@ export default class MainBackground {
|
|||||||
// Only the "true" background should run migrations
|
// Only the "true" background should run migrations
|
||||||
await this.migrationRunner.run();
|
await this.migrationRunner.run();
|
||||||
|
|
||||||
// This is here instead of in in the InitService b/c we don't plan for
|
// This is here instead of in the InitService b/c we don't plan for
|
||||||
// side effects to run in the Browser InitService.
|
// side effects to run in the Browser InitService.
|
||||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||||
|
|
||||||
@@ -1800,18 +1817,41 @@ export default class MainBackground {
|
|||||||
/**
|
/**
|
||||||
* This function is for creating any subscriptions for the background service worker. We do this
|
* This function is for creating any subscriptions for the background service worker. We do this
|
||||||
* here because it's important to run this during the evaluation period of the browser extension
|
* here because it's important to run this during the evaluation period of the browser extension
|
||||||
* service worker.
|
* service worker. If it's not done this way we risk the service worker being closed before it's
|
||||||
|
* registered these system notification click events.
|
||||||
*/
|
*/
|
||||||
initNotificationSubscriptions() {
|
initNotificationSubscriptions() {
|
||||||
this.systemNotificationService.notificationClicked$
|
const handlers: Array<{
|
||||||
.pipe(
|
startsWith: string;
|
||||||
filter((n) => n.id.startsWith(AuthServerNotificationTags.AuthRequest + "_")),
|
handler: (event: SystemNotificationEvent) => Promise<void>;
|
||||||
map((n) => ({ event: n, authRequestId: n.id.split("_")[1] })),
|
}> = [];
|
||||||
switchMap(({ event }) =>
|
|
||||||
this.authRequestAnsweringService.handleAuthRequestNotificationClicked(event),
|
const register = (
|
||||||
|
startsWith: string,
|
||||||
|
handler: (event: SystemNotificationEvent) => Promise<void>,
|
||||||
|
) => {
|
||||||
|
handlers.push({ startsWith, handler });
|
||||||
|
};
|
||||||
|
|
||||||
|
// ======= Register All System Notification Handlers Here =======
|
||||||
|
register(AuthServerNotificationTags.AuthRequest, (event) =>
|
||||||
|
this.authRequestAnsweringService.handleAuthRequestNotificationClicked(event),
|
||||||
|
);
|
||||||
|
// ======= End Register All System Notification Handlers =======
|
||||||
|
|
||||||
|
const streams: Observable<void>[] = handlers.map(({ startsWith, handler }) =>
|
||||||
|
this.systemNotificationService.notificationClicked$.pipe(
|
||||||
|
filter((event: SystemNotificationEvent): boolean => event.id.startsWith(startsWith + "_")),
|
||||||
|
switchMap(
|
||||||
|
(event: SystemNotificationEvent): Observable<void> =>
|
||||||
|
from(Promise.resolve(handler(event))),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
.subscribe();
|
);
|
||||||
|
|
||||||
|
if (streams.length > 0) {
|
||||||
|
merge(...streams).subscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,33 +4,47 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
|
inject,
|
||||||
NgZone,
|
NgZone,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
inject,
|
|
||||||
} from "@angular/core";
|
} from "@angular/core";
|
||||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||||
import {
|
import {
|
||||||
Subject,
|
|
||||||
takeUntil,
|
|
||||||
firstValueFrom,
|
|
||||||
concatMap,
|
|
||||||
filter,
|
|
||||||
tap,
|
|
||||||
catchError,
|
catchError,
|
||||||
of,
|
concatMap,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
|
of,
|
||||||
|
pairwise,
|
||||||
|
startWith,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
takeUntil,
|
||||||
|
tap,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
|
import { LoginApprovalDialogComponent } from "@bitwarden/angular/auth/login-approval/login-approval-dialog.component";
|
||||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||||
import { LogoutReason, UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import {
|
||||||
|
AuthRequestServiceAbstraction,
|
||||||
|
LogoutReason,
|
||||||
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
|
} from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
|
||||||
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -67,6 +81,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private lastActivity: Date;
|
private lastActivity: Date;
|
||||||
private activeUserId: UserId;
|
private activeUserId: UserId;
|
||||||
private routerAnimations = false;
|
private routerAnimations = false;
|
||||||
|
private processingPendingAuth = false;
|
||||||
|
private extensionLoginApprovalFeatureFlag = false;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -99,6 +115,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private readonly documentLangSetter: DocumentLangSetter,
|
private readonly documentLangSetter: DocumentLangSetter,
|
||||||
private popupSizeService: PopupSizeService,
|
private popupSizeService: PopupSizeService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
|
private pendingAuthRequestsState: PendingAuthRequestsStateService,
|
||||||
|
private authRequestAnsweringService: AuthRequestAnsweringServiceAbstraction,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||||
|
|
||||||
@@ -107,6 +127,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
this.extensionLoginApprovalFeatureFlag = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.PM14938_BrowserExtensionLoginApproval),
|
||||||
|
);
|
||||||
|
|
||||||
initPopupClosedListener();
|
initPopupClosedListener();
|
||||||
|
|
||||||
this.compactModeService.init();
|
this.compactModeService.init();
|
||||||
@@ -116,6 +140,25 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
this.activeUserId = account?.id;
|
this.activeUserId = account?.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (this.extensionLoginApprovalFeatureFlag) {
|
||||||
|
// Trigger processing auth requests when the active user is in an unlocked state. Runs once when
|
||||||
|
// the popup is open.
|
||||||
|
this.accountService.activeAccount$
|
||||||
|
.pipe(
|
||||||
|
map((a) => a?.id), // Extract active userId
|
||||||
|
distinctUntilChanged(), // Only when userId actually changes
|
||||||
|
filter((userId) => userId != null), // Require a valid userId
|
||||||
|
switchMap((userId) => this.authService.authStatusFor$(userId).pipe(take(1))), // Get current auth status once for new user
|
||||||
|
filter((status) => status === AuthenticationStatus.Unlocked), // Only when the new user is Unlocked
|
||||||
|
tap(() => {
|
||||||
|
// Trigger processing when switching users while popup is open
|
||||||
|
void this.authRequestAnsweringService.processPendingAuthRequests();
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
this.authService.activeAccountStatus$
|
this.authService.activeAccountStatus$
|
||||||
.pipe(
|
.pipe(
|
||||||
filter((status) => status === AuthenticationStatus.Unlocked),
|
filter((status) => status === AuthenticationStatus.Unlocked),
|
||||||
@@ -126,6 +169,25 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
if (this.extensionLoginApprovalFeatureFlag) {
|
||||||
|
// When the popup is already open and the active account transitions to Unlocked,
|
||||||
|
// process any pending auth requests for the active user. The above subscription does not handle
|
||||||
|
// this case.
|
||||||
|
this.authService.activeAccountStatus$
|
||||||
|
.pipe(
|
||||||
|
startWith(null as unknown as AuthenticationStatus), // Seed previous value to handle initial emission
|
||||||
|
pairwise(), // Compare previous and current statuses
|
||||||
|
filter(
|
||||||
|
([prev, curr]) =>
|
||||||
|
prev !== AuthenticationStatus.Unlocked && curr === AuthenticationStatus.Unlocked, // Fire on transitions into Unlocked (incl. initial)
|
||||||
|
),
|
||||||
|
takeUntil(this.destroy$),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
void this.authRequestAnsweringService.processPendingAuthRequests();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.ngZone.runOutsideAngular(() => {
|
this.ngZone.runOutsideAngular(() => {
|
||||||
window.onmousedown = () => this.recordActivity();
|
window.onmousedown = () => this.recordActivity();
|
||||||
window.ontouchstart = () => this.recordActivity();
|
window.ontouchstart = () => this.recordActivity();
|
||||||
@@ -179,6 +241,42 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.router.navigate(["lock"]);
|
await this.router.navigate(["lock"]);
|
||||||
|
} else if (
|
||||||
|
msg.command === "openLoginApproval" &&
|
||||||
|
this.extensionLoginApprovalFeatureFlag
|
||||||
|
) {
|
||||||
|
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<string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
} else if (msg.command === "showDialog") {
|
} else if (msg.command === "showDialog") {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
|||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
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 { 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 {
|
import {
|
||||||
AutofillSettingsService,
|
AutofillSettingsService,
|
||||||
AutofillSettingsServiceAbstraction,
|
AutofillSettingsServiceAbstraction,
|
||||||
@@ -86,7 +87,10 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
|||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import {
|
||||||
|
MessagingService,
|
||||||
|
MessagingService as MessagingServiceAbstraction,
|
||||||
|
} from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||||
@@ -484,6 +488,8 @@ const safeProviders: SafeProvider[] = [
|
|||||||
AuthService,
|
AuthService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
MasterPasswordServiceAbstraction,
|
MasterPasswordServiceAbstraction,
|
||||||
|
MessagingService,
|
||||||
|
PendingAuthRequestsStateService,
|
||||||
PlatformUtilsService,
|
PlatformUtilsService,
|
||||||
SystemNotificationsService,
|
SystemNotificationsService,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import { firstValueFrom } from "rxjs";
|
|||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common";
|
import { AuthRequestApiServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction";
|
||||||
import {
|
import {
|
||||||
DevicePendingAuthRequest,
|
DevicePendingAuthRequest,
|
||||||
DeviceResponse,
|
DeviceResponse,
|
||||||
} from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
} from "@bitwarden/common/auth/abstractions/devices/responses/device.response";
|
||||||
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view";
|
||||||
|
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||||
|
import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state";
|
||||||
import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums";
|
import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
@@ -66,14 +69,16 @@ export class DeviceManagementComponent implements OnInit {
|
|||||||
protected showHeaderInfo = false;
|
protected showHeaderInfo = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authRequestApiService: AuthRequestApiServiceAbstraction,
|
private readonly accountService: AccountService,
|
||||||
private destroyRef: DestroyRef,
|
private readonly authRequestApiService: AuthRequestApiServiceAbstraction,
|
||||||
private deviceManagementComponentService: DeviceManagementComponentServiceAbstraction,
|
private readonly destroyRef: DestroyRef,
|
||||||
private devicesService: DevicesServiceAbstraction,
|
private readonly deviceManagementComponentService: DeviceManagementComponentServiceAbstraction,
|
||||||
private dialogService: DialogService,
|
private readonly devicesService: DevicesServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private readonly dialogService: DialogService,
|
||||||
private messageListener: MessageListener,
|
private readonly i18nService: I18nService,
|
||||||
private validationService: ValidationService,
|
private readonly messageListener: MessageListener,
|
||||||
|
private readonly pendingAuthRequestStateService: PendingAuthRequestsStateService,
|
||||||
|
private readonly validationService: ValidationService,
|
||||||
) {
|
) {
|
||||||
this.showHeaderInfo = this.deviceManagementComponentService.showHeaderInformation();
|
this.showHeaderInfo = this.deviceManagementComponentService.showHeaderInformation();
|
||||||
}
|
}
|
||||||
@@ -248,6 +253,12 @@ export class DeviceManagementComponent implements OnInit {
|
|||||||
// Auth request was approved or denied, so clear the
|
// Auth request was approved or denied, so clear the
|
||||||
// pending auth request and re-sort the device array
|
// pending auth request and re-sort the device array
|
||||||
this.devices = clearAuthRequestAndResortDevices(this.devices, pendingAuthRequest);
|
this.devices = clearAuthRequestAndResortDevices(this.devices, pendingAuthRequest);
|
||||||
|
|
||||||
|
// If a user ignores or doesn't see the auth request dialog, but comes to account settings
|
||||||
|
// to approve a device login attempt, clear out the state for that user.
|
||||||
|
await this.pendingAuthRequestStateService.clear(
|
||||||
|
await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services
|
|||||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.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 { NoopAuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/noop-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 { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
import { AvatarService } from "@bitwarden/common/auth/services/avatar.service";
|
||||||
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
import { DefaultActiveUserAccessor } from "@bitwarden/common/auth/services/default-active-user.accessor";
|
||||||
@@ -942,6 +943,11 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: UnsupportedSystemNotificationsService,
|
useClass: UnsupportedSystemNotificationsService,
|
||||||
deps: [],
|
deps: [],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: PendingAuthRequestsStateService,
|
||||||
|
useClass: PendingAuthRequestsStateService,
|
||||||
|
deps: [StateProvider],
|
||||||
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: AuthRequestAnsweringServiceAbstraction,
|
provide: AuthRequestAnsweringServiceAbstraction,
|
||||||
useClass: NoopAuthRequestAnsweringService,
|
useClass: NoopAuthRequestAnsweringService,
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ export class SelfHostedEnvConfigDialogComponent implements OnInit, OnDestroy {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
this.showErrorSummary = false;
|
this.showErrorSummary = false;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
See diagram for the high level picture of how this is wired up.
|
||||||
|
|
||||||
|
## Diagram
|
||||||
|
|
||||||
|

|
||||||
@@ -2,7 +2,29 @@ import { SystemNotificationEvent } from "@bitwarden/common/platform/system-notif
|
|||||||
import { UserId } from "@bitwarden/user-core";
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
export abstract class AuthRequestAnsweringServiceAbstraction {
|
export abstract class AuthRequestAnsweringServiceAbstraction {
|
||||||
abstract receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise<void>;
|
/**
|
||||||
|
* 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
|
||||||
|
* so that even if someone closes a window or a popup and comes back, it could be processed later.
|
||||||
|
* Only way to clear out the global state is to respond to the auth request.
|
||||||
|
*
|
||||||
|
* Currently, this is only implemented for browser extension.
|
||||||
|
*
|
||||||
|
* @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<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
abstract handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void>;
|
abstract handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process notifications that have been received but didn't meet the conditions to display the
|
||||||
|
* approval dialog.
|
||||||
|
*/
|
||||||
|
abstract processPendingAuthRequests(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 670 KiB |
@@ -7,6 +7,7 @@ import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-se
|
|||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ActionsService } from "@bitwarden/common/platform/actions";
|
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
import { UserId } from "@bitwarden/user-core";
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
|
import { AuthRequestAnsweringService } from "./auth-request-answering.service";
|
||||||
|
import { PendingAuthRequestsStateService } from "./pending-auth-requests.state";
|
||||||
|
|
||||||
describe("AuthRequestAnsweringService", () => {
|
describe("AuthRequestAnsweringService", () => {
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
@@ -24,6 +26,8 @@ describe("AuthRequestAnsweringService", () => {
|
|||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$
|
let masterPasswordService: any; // MasterPasswordServiceAbstraction has many members; we only use forceSetPasswordReason$
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let pendingAuthRequestsState: MockProxy<PendingAuthRequestsStateService>;
|
||||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
let systemNotificationsService: MockProxy<SystemNotificationsService>;
|
let systemNotificationsService: MockProxy<SystemNotificationsService>;
|
||||||
|
|
||||||
@@ -37,6 +41,8 @@ describe("AuthRequestAnsweringService", () => {
|
|||||||
authService = mock<AuthService>();
|
authService = mock<AuthService>();
|
||||||
i18nService = mock<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
masterPasswordService = { forceSetPasswordReason$: jest.fn() };
|
masterPasswordService = { forceSetPasswordReason$: jest.fn() };
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
pendingAuthRequestsState = mock<PendingAuthRequestsStateService>();
|
||||||
platformUtilsService = mock<PlatformUtilsService>();
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
systemNotificationsService = mock<SystemNotificationsService>();
|
systemNotificationsService = mock<SystemNotificationsService>();
|
||||||
|
|
||||||
@@ -66,6 +72,8 @@ describe("AuthRequestAnsweringService", () => {
|
|||||||
authService,
|
authService,
|
||||||
i18nService,
|
i18nService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
|
messagingService,
|
||||||
|
pendingAuthRequestsState,
|
||||||
platformUtilsService,
|
platformUtilsService,
|
||||||
systemNotificationsService,
|
systemNotificationsService,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||||||
import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags";
|
import { AuthServerNotificationTags } from "@bitwarden/common/auth/enums/auth-server-notification-tags";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
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 { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { ActionsService } from "@bitwarden/common/platform/actions";
|
import { ActionsService } from "@bitwarden/common/platform/actions";
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +20,11 @@ import { UserId } from "@bitwarden/user-core";
|
|||||||
|
|
||||||
import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
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 {
|
export class AuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly accountService: AccountService,
|
private readonly accountService: AccountService,
|
||||||
@@ -26,10 +32,51 @@ export class AuthRequestAnsweringService implements AuthRequestAnsweringServiceA
|
|||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly i18nService: I18nService,
|
private readonly i18nService: I18nService,
|
||||||
private readonly masterPasswordService: MasterPasswordServiceAbstraction,
|
private readonly masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private readonly messagingService: MessagingService,
|
||||||
|
private readonly pendingAuthRequestsState: PendingAuthRequestsStateService,
|
||||||
private readonly platformUtilsService: PlatformUtilsService,
|
private readonly platformUtilsService: PlatformUtilsService,
|
||||||
private readonly systemNotificationsService: SystemNotificationsService,
|
private readonly systemNotificationsService: SystemNotificationsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async receivedPendingAuthRequest(userId: UserId, authRequestId: string): Promise<void> {
|
||||||
|
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<void> {
|
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void> {
|
||||||
if (event.buttonIdentifier === ButtonLocation.NotificationButton) {
|
if (event.buttonIdentifier === ButtonLocation.NotificationButton) {
|
||||||
await this.systemNotificationsService.clear({
|
await this.systemNotificationsService.clear({
|
||||||
@@ -39,32 +86,26 @@ export class AuthRequestAnsweringService implements AuthRequestAnsweringServiceA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async receivedPendingAuthRequest(userId: UserId, authRequestId: string): Promise<void> {
|
async processPendingAuthRequests(): Promise<void> {
|
||||||
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
|
// Prune any stale pending requests (older than 15 minutes)
|
||||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
// This comes from GlobalSettings.cs
|
||||||
const forceSetPasswordReason = await firstValueFrom(
|
// public TimeSpan UserRequestExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||||
this.masterPasswordService.forceSetPasswordReason$(userId),
|
const fifteenMinutesMs = 15 * 60 * 1000;
|
||||||
);
|
|
||||||
|
|
||||||
// Is the popup already open?
|
await this.pendingAuthRequestsState.pruneOlderThan(fifteenMinutesMs);
|
||||||
if (
|
|
||||||
(await this.platformUtilsService.isPopupOpen()) &&
|
|
||||||
authStatus === AuthenticationStatus.Unlocked &&
|
|
||||||
activeUserId === userId &&
|
|
||||||
forceSetPasswordReason === ForceSetPasswordReason.None
|
|
||||||
) {
|
|
||||||
// TODO: Handled in 14934
|
|
||||||
} else {
|
|
||||||
// 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({
|
const pendingAuthRequestsInState: PendingAuthUserMarker[] =
|
||||||
id: `${AuthServerNotificationTags.AuthRequest}_${authRequestId}`,
|
(await firstValueFrom(this.pendingAuthRequestsState.getAll$())) ?? [];
|
||||||
title: this.i18nService.t("accountAccessRequested"),
|
|
||||||
body: this.i18nService.t("confirmAccessAttempt", emailForUser),
|
if (pendingAuthRequestsInState.length > 0) {
|
||||||
buttons: [],
|
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||||
});
|
const pendingAuthRequestsForActiveUser = pendingAuthRequestsInState.some(
|
||||||
|
(e) => e.userId === activeUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pendingAuthRequestsForActiveUser) {
|
||||||
|
this.messagingService.send("openLoginApproval");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { AuthRequestAnsweringServiceAbstraction } from "../../abstractions/auth-
|
|||||||
|
|
||||||
export class NoopAuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction {
|
export class NoopAuthRequestAnsweringService implements AuthRequestAnsweringServiceAbstraction {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void> {}
|
|
||||||
|
|
||||||
async receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise<void> {}
|
async receivedPendingAuthRequest(userId: UserId, notificationId: string): Promise<void> {}
|
||||||
|
|
||||||
|
async handleAuthRequestNotificationClicked(event: SystemNotificationEvent): Promise<void> {}
|
||||||
|
|
||||||
|
async processPendingAuthRequests(): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AUTH_REQUEST_DISK_LOCAL,
|
||||||
|
GlobalState,
|
||||||
|
KeyDefinition,
|
||||||
|
StateProvider,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { UserId } from "@bitwarden/user-core";
|
||||||
|
|
||||||
|
export type PendingAuthUserMarker = {
|
||||||
|
userId: UserId;
|
||||||
|
receivedAtMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PENDING_AUTH_REQUESTS = KeyDefinition.array<PendingAuthUserMarker>(
|
||||||
|
AUTH_REQUEST_DISK_LOCAL,
|
||||||
|
"pendingAuthRequests",
|
||||||
|
{
|
||||||
|
deserializer: (json) => json,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export class PendingAuthRequestsStateService {
|
||||||
|
private readonly state: GlobalState<PendingAuthUserMarker[]>;
|
||||||
|
|
||||||
|
constructor(private readonly stateProvider: StateProvider) {
|
||||||
|
this.state = this.stateProvider.getGlobal(PENDING_AUTH_REQUESTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll$(): Observable<PendingAuthUserMarker[] | null> {
|
||||||
|
return this.state.state$;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(userId: UserId): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
await this.stateProvider.getGlobal(PENDING_AUTH_REQUESTS).update((current) => {
|
||||||
|
const list = (current ?? []).filter((e) => e.userId !== userId);
|
||||||
|
return [...list, { userId, receivedAtMs: now }];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async pruneOlderThan(maxAgeMs: number): Promise<void> {
|
||||||
|
const cutoff = Date.now() - maxAgeMs;
|
||||||
|
await this.stateProvider.getGlobal(PENDING_AUTH_REQUESTS).update((current) => {
|
||||||
|
const list = current ?? [];
|
||||||
|
return list.filter((e) => e.receivedAtMs >= cutoff);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(userId: UserId): Promise<void> {
|
||||||
|
await this.stateProvider.getGlobal(PENDING_AUTH_REQUESTS).update((current) => {
|
||||||
|
const list = current ?? [];
|
||||||
|
return list.filter((e) => e.userId !== userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,6 +131,8 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
|||||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||||
[FeatureFlag.InactiveUserServerNotification]: true,
|
[FeatureFlag.InactiveUserServerNotification]: true,
|
||||||
|
[FeatureFlag.PushNotificationsWhenLocked]: true,
|
||||||
|
[FeatureFlag.PM14938_BrowserExtensionLoginApproval]: true,
|
||||||
};
|
};
|
||||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||||
});
|
});
|
||||||
@@ -253,8 +255,7 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
|||||||
.next({ type: "not-supported", reason: "test" } as any);
|
.next({ type: "not-supported", reason: "test" } as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: When PM-14943 goes in, uncomment
|
authRequestAnsweringService.receivedPendingAuthRequest.mockResolvedValue(undefined as any);
|
||||||
// authRequestAnsweringService.receivedPendingAuthRequest.mockResolvedValue(undefined as any);
|
|
||||||
|
|
||||||
const subscription = defaultServerNotificationsService.startListening();
|
const subscription = defaultServerNotificationsService.startListening();
|
||||||
|
|
||||||
@@ -273,12 +274,10 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
|||||||
expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval", {
|
expect(messagingService.send).toHaveBeenCalledWith("openLoginApproval", {
|
||||||
notificationId: "auth-id-2",
|
notificationId: "auth-id-2",
|
||||||
});
|
});
|
||||||
|
expect(authRequestAnsweringService.receivedPendingAuthRequest).toHaveBeenCalledWith(
|
||||||
// TODO: When PM-14943 goes in, uncomment
|
mockUserId2,
|
||||||
// expect(authRequestAnsweringService.receivedPendingAuthRequest).toHaveBeenCalledWith(
|
"auth-id-2",
|
||||||
// mockUserId2,
|
);
|
||||||
// "auth-id-2",
|
|
||||||
// );
|
|
||||||
|
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user