mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-26649] Prevent log-out when changing KDF settings (except old clients) (#16775)
* Prevent log-out when changing KDF settings (except old clients) * test coverage * logout reason enum
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const PushNotificationLogOutReasonType = Object.freeze({
|
||||
KdfChange: 0,
|
||||
} as const);
|
||||
|
||||
export type PushNotificationLogOutReasonType =
|
||||
(typeof PushNotificationLogOutReasonType)[keyof typeof PushNotificationLogOutReasonType];
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user