1
0
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:
Maciej Zieniuk
2025-10-21 11:26:48 +02:00
committed by GitHub
parent a15d6867f9
commit 20ddf3b6fd
6 changed files with 96 additions and 6 deletions

View File

@@ -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,

View File

@@ -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";

View File

@@ -0,0 +1,6 @@
export const PushNotificationLogOutReasonType = Object.freeze({
KdfChange: 0,
} as const);
export type PushNotificationLogOutReasonType =
(typeof PushNotificationLogOutReasonType)[keyof typeof PushNotificationLogOutReasonType];

View File

@@ -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");
}
}

View File

@@ -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();
});
});
});
});

View File

@@ -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(