diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 2d2d3fa63f9..6a561d29a0f 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -37,6 +37,7 @@ export enum FeatureFlag { PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption", WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2", UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data", + NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change", /* Tools */ DesktopSendUIRefresh = "desktop-send-ui-refresh", @@ -120,6 +121,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.PM25174_DisableType0Decryption]: FALSE, [FeatureFlag.WindowsBiometricsV2]: FALSE, [FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE, + [FeatureFlag.NoLogoutOnKdfChange]: FALSE, /* Platform */ [FeatureFlag.IpcChannelFramework]: FALSE, diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index cf9dac80189..000f2a074d8 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -6,3 +6,4 @@ export * from "./http-status-code.enum"; export * from "./integration-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; +export * from "./push-notification-logout-reason.enum"; diff --git a/libs/common/src/enums/push-notification-logout-reason.enum.ts b/libs/common/src/enums/push-notification-logout-reason.enum.ts new file mode 100644 index 00000000000..ab269248850 --- /dev/null +++ b/libs/common/src/enums/push-notification-logout-reason.enum.ts @@ -0,0 +1,6 @@ +export const PushNotificationLogOutReasonType = Object.freeze({ + KdfChange: 0, +} as const); + +export type PushNotificationLogOutReasonType = + (typeof PushNotificationLogOutReasonType)[keyof typeof PushNotificationLogOutReasonType]; diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 56b22fd3117..167864208ee 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -1,6 +1,6 @@ import { NotificationViewResponse as EndUserNotificationResponse } from "@bitwarden/common/vault/notifications/models"; -import { NotificationType } from "../../enums"; +import { NotificationType, PushNotificationLogOutReasonType } from "../../enums"; import { BaseResponse } from "./base.response"; @@ -41,9 +41,11 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizations: case NotificationType.SyncOrgKeys: case NotificationType.SyncSettings: - case NotificationType.LogOut: this.payload = new UserNotification(payload); break; + case NotificationType.LogOut: + this.payload = new LogOutNotification(payload); + break; case NotificationType.SyncSendCreate: case NotificationType.SyncSendUpdate: case NotificationType.SyncSendDelete: @@ -184,3 +186,14 @@ export class ProviderBankAccountVerifiedPushNotification extends BaseResponse { this.adminId = this.getResponseProperty("AdminId"); } } + +export class LogOutNotification extends BaseResponse { + userId: string; + reason?: PushNotificationLogOutReasonType; + + constructor(response: any) { + super(response); + this.userId = this.getResponseProperty("UserId"); + this.reason = this.getResponseProperty("Reason"); + } +} diff --git a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts index a7b608f5b56..b2aa4fbd315 100644 --- a/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts +++ b/libs/common/src/platform/server-notifications/internal/default-server-notifications.service.spec.ts @@ -11,7 +11,7 @@ import { Matrix } from "../../../../spec/matrix"; import { AccountService } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { NotificationType } from "../../../enums"; +import { NotificationType, PushNotificationLogOutReasonType } from "../../../enums"; import { NotificationResponse } from "../../../models/response/notification.response"; import { UserId } from "../../../types/guid"; import { AppIdService } from "../../abstractions/app-id.service"; @@ -340,4 +340,56 @@ describe("NotificationsService", () => { expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1); subscription.unsubscribe(); }); + + describe("processNotification", () => { + beforeEach(async () => { + appIdService.getAppId.mockResolvedValue("test-app-id"); + activeAccount.next({ id: mockUser1, email: "email", name: "Test Name", emailVerified: true }); + }); + + describe("NotificationType.LogOut", () => { + it.each([ + { featureFlagEnabled: false, reason: undefined }, + { featureFlagEnabled: true, reason: undefined }, + { featureFlagEnabled: false, reason: PushNotificationLogOutReasonType.KdfChange }, + ])( + "should call logout callback when featureFlag=$featureFlagEnabled and reason=$reason", + async ({ featureFlagEnabled, reason }) => { + configService.getFeatureFlag$.mockReturnValue(of(featureFlagEnabled)); + + const payload: { UserId: UserId; Reason?: PushNotificationLogOutReasonType } = { + UserId: mockUser1, + Reason: undefined, + }; + if (reason != null) { + payload.Reason = reason; + } + + const notification = new NotificationResponse({ + type: NotificationType.LogOut, + payload, + contextId: "different-app-id", + }); + + await sut["processNotification"](notification, mockUser1); + + expect(logoutCallback).toHaveBeenCalledWith("logoutNotification", mockUser1); + }, + ); + + it("should skip logout when receiving KDF change reason with feature flag enabled", async () => { + configService.getFeatureFlag$.mockReturnValue(of(true)); + + const notification = new NotificationResponse({ + type: NotificationType.LogOut, + payload: { UserId: mockUser1, Reason: PushNotificationLogOutReasonType.KdfChange }, + contextId: "different-app-id", + }); + + await sut["processNotification"](notification, mockUser1); + + expect(logoutCallback).not.toHaveBeenCalled(); + }); + }); + }); }); 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..efe0a8ae408 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 @@ -22,8 +22,9 @@ import { trackedMerge } from "@bitwarden/common/platform/misc"; import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service"; import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; -import { NotificationType } from "../../../enums"; +import { NotificationType, PushNotificationLogOutReasonType } from "../../../enums"; import { + LogOutNotification, NotificationResponse, SyncCipherNotification, SyncFolderNotification, @@ -263,10 +264,25 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer this.activitySubject.next("inactive"); // Force a disconnect this.activitySubject.next("active"); // Allow a reconnect break; - case NotificationType.LogOut: + case NotificationType.LogOut: { this.logService.info("[Notifications Service] Received logout notification"); - await this.logoutCallback("logoutNotification", userId); + + const logOutNotification = notification.payload as LogOutNotification; + const noLogoutOnKdfChange = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.NoLogoutOnKdfChange), + ); + if ( + noLogoutOnKdfChange && + logOutNotification.reason === PushNotificationLogOutReasonType.KdfChange + ) { + this.logService.info( + "[Notifications Service] Skipping logout due to no logout KDF change", + ); + } else { + await this.logoutCallback("logoutNotification", userId); + } break; + } case NotificationType.SyncSendCreate: case NotificationType.SyncSendUpdate: await this.syncService.syncUpsertSend(