mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 07:43:35 +00:00
[PM-5499] auth request service migrations (#8597)
* move auth request storage to service * create migrations for auth requests * fix tests * fix browser * fix login strategy * update migration * use correct test descriptions in migration
This commit is contained in:
@@ -1,11 +1,7 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
|
||||
// TODO: Tech Debt: potentially create a type Storage shape vs using a class here in the future
|
||||
// type StorageShape {
|
||||
// id: string;
|
||||
// privateKey: string;
|
||||
// }
|
||||
// so we can get rid of the any type passed into fromJSON and coming out of ToJSON
|
||||
export class AdminAuthRequestStorable {
|
||||
id: string;
|
||||
privateKey: Uint8Array;
|
||||
@@ -23,7 +19,7 @@ export class AdminAuthRequestStorable {
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(obj: any): AdminAuthRequestStorable {
|
||||
static fromJSON(obj: Jsonify<AdminAuthRequestStorable>): AdminAuthRequestStorable {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||
@@ -124,11 +123,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getAdminAuthRequest: (options?: StorageOptions) => Promise<AdminAuthRequestStorable | null>;
|
||||
setAdminAuthRequest: (
|
||||
adminAuthRequest: AdminAuthRequestStorable,
|
||||
options?: StorageOptions,
|
||||
) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
setEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmailVerified: (options?: StorageOptions) => Promise<boolean>;
|
||||
@@ -207,7 +201,5 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>;
|
||||
setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
nextUpActiveUser: () => Promise<UserId>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable";
|
||||
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
||||
import {
|
||||
@@ -169,7 +168,6 @@ export class AccountSettings {
|
||||
protectedPin?: string;
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string = "lock";
|
||||
approveLoginRequests?: boolean;
|
||||
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
@@ -206,7 +204,6 @@ export class Account {
|
||||
profile?: AccountProfile = new AccountProfile();
|
||||
settings?: AccountSettings = new AccountSettings();
|
||||
tokens?: AccountTokens = new AccountTokens();
|
||||
adminAuthRequest?: Jsonify<AdminAuthRequestStorable> = null;
|
||||
|
||||
constructor(init: Partial<Account>) {
|
||||
Object.assign(this, {
|
||||
@@ -230,7 +227,6 @@ export class Account {
|
||||
...new AccountTokens(),
|
||||
...init?.tokens,
|
||||
},
|
||||
adminAuthRequest: init?.adminAuthRequest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -245,7 +241,6 @@ export class Account {
|
||||
profile: AccountProfile.fromJSON(json?.profile),
|
||||
settings: AccountSettings.fromJSON(json?.settings),
|
||||
tokens: AccountTokens.fromJSON(json?.tokens),
|
||||
adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Jsonify, JsonValue } from "type-fest";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { TokenService } from "../../auth/abstractions/token.service";
|
||||
import { AdminAuthRequestStorable } from "../../auth/models/domain/admin-auth-req-storable";
|
||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||
@@ -548,37 +547,6 @@ export class StateService<
|
||||
: await this.secureStorageService.save(DDG_SHARED_KEY, value, options);
|
||||
}
|
||||
|
||||
async getAdminAuthRequest(options?: StorageOptions): Promise<AdminAuthRequestStorable | null> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
return account?.adminAuthRequest
|
||||
? AdminAuthRequestStorable.fromJSON(account.adminAuthRequest)
|
||||
: null;
|
||||
}
|
||||
|
||||
async setAdminAuthRequest(
|
||||
adminAuthRequest: AdminAuthRequestStorable,
|
||||
options?: StorageOptions,
|
||||
): Promise<void> {
|
||||
options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions());
|
||||
|
||||
if (options?.userId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.getAccount(options);
|
||||
|
||||
account.adminAuthRequest = adminAuthRequest?.toJSON();
|
||||
|
||||
await this.saveAccount(account, options);
|
||||
}
|
||||
|
||||
async getEmail(options?: StorageOptions): Promise<string> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
||||
@@ -1032,24 +1000,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getApproveLoginRequests(options?: StorageOptions): Promise<boolean> {
|
||||
const approveLoginRequests = (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.settings?.approveLoginRequests;
|
||||
return approveLoginRequests;
|
||||
}
|
||||
|
||||
async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
account.settings.approveLoginRequests = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||
let globals: TGlobalState;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
@@ -1392,7 +1342,6 @@ export class StateService<
|
||||
protected resetAccount(account: TAccount) {
|
||||
const persistentAccountInformation = {
|
||||
settings: account.settings,
|
||||
adminAuthRequest: account.adminAuthRequest,
|
||||
};
|
||||
return Object.assign(this.createAccount(), persistentAccountInformation);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
||||
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as signalR from "@microsoft/signalr";
|
||||
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AuthRequestServiceAbstraction } from "../../../auth/src/common/abstractions";
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||
import { AuthService } from "../auth/abstractions/auth.service";
|
||||
@@ -18,6 +19,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../platform/abstractions/messaging.service";
|
||||
import { StateService } from "../platform/abstractions/state.service";
|
||||
import { UserId } from "../types/guid";
|
||||
import { SyncService } from "../vault/abstractions/sync/sync.service.abstraction";
|
||||
|
||||
export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
@@ -37,6 +39,7 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||
private stateService: StateService,
|
||||
private authService: AuthService,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
) {
|
||||
this.environmentService.environment$.subscribe(() => {
|
||||
@@ -199,10 +202,13 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
||||
break;
|
||||
case NotificationType.AuthRequest:
|
||||
if (await this.stateService.getApproveLoginRequests()) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: notification.payload.id,
|
||||
});
|
||||
{
|
||||
const userId = await this.stateService.getUserId();
|
||||
if (await this.authRequestService.getAcceptAuthRequests(userId as UserId)) {
|
||||
this.messagingService.send("openLoginApproval", {
|
||||
notificationId: notification.payload.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -52,6 +52,7 @@ import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version
|
||||
import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers";
|
||||
import { SendMigrator } from "./migrations/54-move-encrypted-sends";
|
||||
import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider";
|
||||
import { AuthRequestMigrator } from "./migrations/56-move-auth-requests";
|
||||
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
|
||||
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
|
||||
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
|
||||
@@ -59,7 +60,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
|
||||
import { MinVersionMigrator } from "./migrations/min-version";
|
||||
|
||||
export const MIN_VERSION = 3;
|
||||
export const CURRENT_VERSION = 55;
|
||||
export const CURRENT_VERSION = 56;
|
||||
|
||||
export type MinVersion = typeof MIN_VERSION;
|
||||
|
||||
@@ -117,7 +118,8 @@ export function createMigrationBuilder() {
|
||||
.with(DeleteInstalledVersion, 51, 52)
|
||||
.with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53)
|
||||
.with(SendMigrator, 53, 54)
|
||||
.with(MoveMasterKeyStateToProviderMigrator, 54, CURRENT_VERSION);
|
||||
.with(MoveMasterKeyStateToProviderMigrator, 54, 55)
|
||||
.with(AuthRequestMigrator, 55, CURRENT_VERSION);
|
||||
}
|
||||
|
||||
export async function currentVersion(
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { AuthRequestMigrator } from "./56-move-auth-requests";
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
approveLoginRequests: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
},
|
||||
SecondAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
user_FirstAccount_authRequestLocal_adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
user_FirstAccount_authRequestLocal_acceptAuthRequests: true,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["FirstAccount", "SecondAccount"],
|
||||
FirstAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
SecondAccount: {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "adminAuthRequest",
|
||||
};
|
||||
|
||||
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "acceptAuthRequests",
|
||||
};
|
||||
|
||||
describe("AuthRequestMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: AuthRequestMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 55);
|
||||
sut = new AuthRequestMigrator(55, 56);
|
||||
});
|
||||
|
||||
it("removes the existing adminAuthRequest and approveLoginRequests", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).not.toHaveBeenCalledWith("SecondAccount");
|
||||
});
|
||||
|
||||
it("sets the adminAuthRequest and approveLoginRequests under the new key definitions", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
});
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, true);
|
||||
expect(helper.setToUser).not.toHaveBeenCalledWith("SecondAccount");
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 56);
|
||||
sut = new AuthRequestMigrator(55, 56);
|
||||
});
|
||||
|
||||
it("nulls the new adminAuthRequest and acceptAuthRequests values", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ADMIN_AUTH_REQUEST_KEY, null);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||
});
|
||||
|
||||
it("sets back the adminAuthRequest and approveLoginRequests under old account object", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledWith("FirstAccount", {
|
||||
adminAuthRequest: {
|
||||
id: "id1",
|
||||
privateKey: "privateKey1",
|
||||
},
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
approveLoginRequests: true,
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
type AdminAuthRequestStorable = {
|
||||
id: string;
|
||||
privateKey: string;
|
||||
};
|
||||
|
||||
type ExpectedAccountType = {
|
||||
adminAuthRequest?: AdminAuthRequestStorable;
|
||||
settings?: {
|
||||
approveLoginRequests?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const ADMIN_AUTH_REQUEST_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "adminAuthRequest",
|
||||
};
|
||||
|
||||
const ACCEPT_AUTH_REQUESTS_KEY: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "authRequestLocal",
|
||||
},
|
||||
key: "acceptAuthRequests",
|
||||
};
|
||||
|
||||
export class AuthRequestMigrator extends Migrator<55, 56> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
|
||||
// Migrate admin auth request
|
||||
const existingAdminAuthRequest = account?.adminAuthRequest;
|
||||
|
||||
if (existingAdminAuthRequest != null) {
|
||||
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, existingAdminAuthRequest);
|
||||
delete account.adminAuthRequest;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
// Migrate approve login requests
|
||||
const existingApproveLoginRequests = account?.settings?.approveLoginRequests;
|
||||
|
||||
if (existingApproveLoginRequests != null) {
|
||||
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, existingApproveLoginRequests);
|
||||
delete account.settings.approveLoginRequests;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
if (updatedAccount) {
|
||||
// Save the migrated account
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
||||
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
||||
let updatedAccount = false;
|
||||
// Rollback admin auth request
|
||||
const migratedAdminAuthRequest: AdminAuthRequestStorable = await helper.getFromUser(
|
||||
userId,
|
||||
ADMIN_AUTH_REQUEST_KEY,
|
||||
);
|
||||
|
||||
if (migratedAdminAuthRequest != null) {
|
||||
account.adminAuthRequest = migratedAdminAuthRequest;
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ADMIN_AUTH_REQUEST_KEY, null);
|
||||
|
||||
// Rollback approve login requests
|
||||
const migratedAcceptAuthRequest: boolean = await helper.getFromUser(
|
||||
userId,
|
||||
ACCEPT_AUTH_REQUESTS_KEY,
|
||||
);
|
||||
|
||||
if (migratedAcceptAuthRequest != null) {
|
||||
account.settings = Object.assign(account.settings ?? {}, {
|
||||
approveLoginRequests: migratedAcceptAuthRequest,
|
||||
});
|
||||
updatedAccount = true;
|
||||
}
|
||||
|
||||
await helper.setToUser(userId, ACCEPT_AUTH_REQUESTS_KEY, null);
|
||||
|
||||
if (updatedAccount) {
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user