mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +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",
|
PM25174_DisableType0Decryption = "pm-25174-disable-type-0-decryption",
|
||||||
WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2",
|
WindowsBiometricsV2 = "pm-25373-windows-biometrics-v2",
|
||||||
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
|
UnlockWithMasterPasswordUnlockData = "pm-23246-unlock-with-master-password-unlock-data",
|
||||||
|
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
|
||||||
|
|
||||||
/* Tools */
|
/* Tools */
|
||||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||||
@@ -120,6 +121,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.PM25174_DisableType0Decryption]: FALSE,
|
[FeatureFlag.PM25174_DisableType0Decryption]: FALSE,
|
||||||
[FeatureFlag.WindowsBiometricsV2]: FALSE,
|
[FeatureFlag.WindowsBiometricsV2]: FALSE,
|
||||||
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
|
[FeatureFlag.UnlockWithMasterPasswordUnlockData]: FALSE,
|
||||||
|
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
|
||||||
|
|
||||||
/* Platform */
|
/* Platform */
|
||||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from "./http-status-code.enum";
|
|||||||
export * from "./integration-type.enum";
|
export * from "./integration-type.enum";
|
||||||
export * from "./native-messaging-version.enum";
|
export * from "./native-messaging-version.enum";
|
||||||
export * from "./notification-type.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 { NotificationViewResponse as EndUserNotificationResponse } from "@bitwarden/common/vault/notifications/models";
|
||||||
|
|
||||||
import { NotificationType } from "../../enums";
|
import { NotificationType, PushNotificationLogOutReasonType } from "../../enums";
|
||||||
|
|
||||||
import { BaseResponse } from "./base.response";
|
import { BaseResponse } from "./base.response";
|
||||||
|
|
||||||
@@ -41,9 +41,11 @@ export class NotificationResponse extends BaseResponse {
|
|||||||
case NotificationType.SyncOrganizations:
|
case NotificationType.SyncOrganizations:
|
||||||
case NotificationType.SyncOrgKeys:
|
case NotificationType.SyncOrgKeys:
|
||||||
case NotificationType.SyncSettings:
|
case NotificationType.SyncSettings:
|
||||||
case NotificationType.LogOut:
|
|
||||||
this.payload = new UserNotification(payload);
|
this.payload = new UserNotification(payload);
|
||||||
break;
|
break;
|
||||||
|
case NotificationType.LogOut:
|
||||||
|
this.payload = new LogOutNotification(payload);
|
||||||
|
break;
|
||||||
case NotificationType.SyncSendCreate:
|
case NotificationType.SyncSendCreate:
|
||||||
case NotificationType.SyncSendUpdate:
|
case NotificationType.SyncSendUpdate:
|
||||||
case NotificationType.SyncSendDelete:
|
case NotificationType.SyncSendDelete:
|
||||||
@@ -184,3 +186,14 @@ export class ProviderBankAccountVerifiedPushNotification extends BaseResponse {
|
|||||||
this.adminId = this.getResponseProperty("AdminId");
|
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 { AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { NotificationType } from "../../../enums";
|
import { NotificationType, PushNotificationLogOutReasonType } from "../../../enums";
|
||||||
import { NotificationResponse } from "../../../models/response/notification.response";
|
import { NotificationResponse } from "../../../models/response/notification.response";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { AppIdService } from "../../abstractions/app-id.service";
|
import { AppIdService } from "../../abstractions/app-id.service";
|
||||||
@@ -340,4 +340,56 @@ describe("NotificationsService", () => {
|
|||||||
expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1);
|
expect(webPushNotificationConnectionService.supportStatus$).toHaveBeenCalledTimes(1);
|
||||||
subscription.unsubscribe();
|
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 { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { AuthService } from "../../../auth/abstractions/auth.service";
|
import { AuthService } from "../../../auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { NotificationType } from "../../../enums";
|
import { NotificationType, PushNotificationLogOutReasonType } from "../../../enums";
|
||||||
import {
|
import {
|
||||||
|
LogOutNotification,
|
||||||
NotificationResponse,
|
NotificationResponse,
|
||||||
SyncCipherNotification,
|
SyncCipherNotification,
|
||||||
SyncFolderNotification,
|
SyncFolderNotification,
|
||||||
@@ -263,10 +264,25 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
|||||||
this.activitySubject.next("inactive"); // Force a disconnect
|
this.activitySubject.next("inactive"); // Force a disconnect
|
||||||
this.activitySubject.next("active"); // Allow a reconnect
|
this.activitySubject.next("active"); // Allow a reconnect
|
||||||
break;
|
break;
|
||||||
case NotificationType.LogOut:
|
case NotificationType.LogOut: {
|
||||||
this.logService.info("[Notifications Service] Received logout notification");
|
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;
|
break;
|
||||||
|
}
|
||||||
case NotificationType.SyncSendCreate:
|
case NotificationType.SyncSendCreate:
|
||||||
case NotificationType.SyncSendUpdate:
|
case NotificationType.SyncSendUpdate:
|
||||||
await this.syncService.syncUpsertSend(
|
await this.syncService.syncUpsertSend(
|
||||||
|
|||||||
Reference in New Issue
Block a user