mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 01:03:35 +00:00
* PM-5263 - Update token svc state provider migration to avoid persisting secrets that shouldn't exist in local storage to state provider local storage using new migration helper type. * PM-5263 - TokenSvc migration - tests TODO * write tests for migration * fix tests --------- Co-authored-by: Jake Fink <jfink@bitwarden.com>
244 lines
8.1 KiB
TypeScript
244 lines
8.1 KiB
TypeScript
import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper";
|
|
import { Migrator } from "../migrator";
|
|
|
|
// Types to represent data as it is stored in JSON
|
|
type ExpectedAccountType = {
|
|
tokens?: {
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
};
|
|
profile?: {
|
|
apiKeyClientId?: string;
|
|
email?: string;
|
|
};
|
|
keys?: {
|
|
apiKeyClientSecret?: string;
|
|
};
|
|
};
|
|
|
|
type ExpectedGlobalType = {
|
|
twoFactorToken?: string;
|
|
};
|
|
|
|
export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL: KeyDefinitionLike = {
|
|
key: "emailTwoFactorTokenRecord",
|
|
stateDefinition: {
|
|
name: "tokenDiskLocal",
|
|
},
|
|
};
|
|
|
|
const TOKEN_STATE_DEF_LIKE: StateDefinitionLike = {
|
|
name: "token",
|
|
};
|
|
|
|
export const ACCESS_TOKEN_DISK: KeyDefinitionLike = {
|
|
key: "accessToken", // matches KeyDefinition.key
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const REFRESH_TOKEN_DISK: KeyDefinitionLike = {
|
|
key: "refreshToken",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const API_KEY_CLIENT_ID_DISK: KeyDefinitionLike = {
|
|
key: "apiKeyClientId",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export const API_KEY_CLIENT_SECRET_DISK: KeyDefinitionLike = {
|
|
key: "apiKeyClientSecret",
|
|
stateDefinition: TOKEN_STATE_DEF_LIKE,
|
|
};
|
|
|
|
export class TokenServiceStateProviderMigrator extends Migrator<37, 38> {
|
|
async migrate(helper: MigrationHelper): Promise<void> {
|
|
// Move global data
|
|
const globalData = await helper.get<ExpectedGlobalType>("global");
|
|
|
|
// Create new global record for 2FA token that we can accumulate data in
|
|
const emailTwoFactorTokenRecord = {};
|
|
|
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
|
async function migrateAccount(
|
|
userId: string,
|
|
account: ExpectedAccountType | undefined,
|
|
globalTwoFactorToken: string | undefined,
|
|
emailTwoFactorTokenRecord: Record<string, string>,
|
|
): Promise<void> {
|
|
let updatedAccount = false;
|
|
|
|
// migrate 2FA token from global to user state
|
|
// Due to the existing implmentation, n users on the same device share the same global state value for 2FA token.
|
|
// So, we will just migrate it to all users to keep it valid for whichever was the user that set it previously.
|
|
// Note: don't bother migrating 2FA Token if user account or email is undefined
|
|
const email = account?.profile?.email;
|
|
if (globalTwoFactorToken != undefined && account != undefined && email != undefined) {
|
|
emailTwoFactorTokenRecord[email] = globalTwoFactorToken;
|
|
// Note: don't set updatedAccount to true here as we aren't updating
|
|
// the legacy user state, just migrating a global state to a new user state
|
|
}
|
|
|
|
// Migrate access token
|
|
const existingAccessToken = account?.tokens?.accessToken;
|
|
|
|
if (existingAccessToken != null) {
|
|
// Only migrate data that exists
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate access token to session storage - never local.
|
|
await helper.setToUser(userId, ACCESS_TOKEN_DISK, existingAccessToken);
|
|
}
|
|
delete account.tokens.accessToken;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate refresh token
|
|
const existingRefreshToken = account?.tokens?.refreshToken;
|
|
|
|
if (existingRefreshToken != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate refresh token to session storage - never local.
|
|
await helper.setToUser(userId, REFRESH_TOKEN_DISK, existingRefreshToken);
|
|
}
|
|
delete account.tokens.refreshToken;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate API key client id
|
|
const existingApiKeyClientId = account?.profile?.apiKeyClientId;
|
|
|
|
if (existingApiKeyClientId != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate client id to session storage - never local.
|
|
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, existingApiKeyClientId);
|
|
}
|
|
delete account.profile.apiKeyClientId;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
// Migrate API key client secret
|
|
const existingApiKeyClientSecret = account?.keys?.apiKeyClientSecret;
|
|
if (existingApiKeyClientSecret != null) {
|
|
if (helper.type !== "web-disk-local") {
|
|
// only migrate client secret to session storage - never local.
|
|
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, existingApiKeyClientSecret);
|
|
}
|
|
delete account.keys.apiKeyClientSecret;
|
|
updatedAccount = true;
|
|
}
|
|
|
|
if (updatedAccount) {
|
|
// Save the migrated account only if it was updated
|
|
await helper.set(userId, account);
|
|
}
|
|
}
|
|
|
|
await Promise.all([
|
|
...accounts.map(({ userId, account }) =>
|
|
migrateAccount(userId, account, globalData?.twoFactorToken, emailTwoFactorTokenRecord),
|
|
),
|
|
]);
|
|
|
|
// Save the global 2FA token record
|
|
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, emailTwoFactorTokenRecord);
|
|
|
|
// Delete global data
|
|
delete globalData?.twoFactorToken;
|
|
await helper.set("global", globalData);
|
|
}
|
|
|
|
async rollback(helper: MigrationHelper): Promise<void> {
|
|
const accounts = await helper.getAccounts<ExpectedAccountType>();
|
|
|
|
// Since we migrated the global 2FA token to all users, we need to rollback the 2FA token for all users
|
|
// but we only need to set it to the global state once
|
|
|
|
// Go through accounts and find the first user that has a non-null email and 2FA token
|
|
let migratedTwoFactorToken: string | null = null;
|
|
for (const { account } of accounts) {
|
|
const email = account?.profile?.email;
|
|
if (email == null) {
|
|
continue;
|
|
}
|
|
const emailTwoFactorTokenRecord: Record<string, string> = await helper.getFromGlobal(
|
|
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
|
|
);
|
|
|
|
migratedTwoFactorToken = emailTwoFactorTokenRecord[email];
|
|
|
|
if (migratedTwoFactorToken != null) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (migratedTwoFactorToken != null) {
|
|
let legacyGlobal = await helper.get<ExpectedGlobalType>("global");
|
|
if (!legacyGlobal) {
|
|
legacyGlobal = {};
|
|
}
|
|
legacyGlobal.twoFactorToken = migratedTwoFactorToken;
|
|
await helper.set("global", legacyGlobal);
|
|
}
|
|
|
|
// delete global 2FA token record
|
|
await helper.setToGlobal(EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, null);
|
|
|
|
async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise<void> {
|
|
let updatedLegacyAccount = false;
|
|
|
|
// Rollback access token
|
|
const migratedAccessToken = await helper.getFromUser<string>(userId, ACCESS_TOKEN_DISK);
|
|
|
|
if (account?.tokens && migratedAccessToken != null) {
|
|
account.tokens.accessToken = migratedAccessToken;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, ACCESS_TOKEN_DISK, null);
|
|
|
|
// Rollback refresh token
|
|
const migratedRefreshToken = await helper.getFromUser<string>(userId, REFRESH_TOKEN_DISK);
|
|
|
|
if (account?.tokens && migratedRefreshToken != null) {
|
|
account.tokens.refreshToken = migratedRefreshToken;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, REFRESH_TOKEN_DISK, null);
|
|
|
|
// Rollback API key client id
|
|
|
|
const migratedApiKeyClientId = await helper.getFromUser<string>(
|
|
userId,
|
|
API_KEY_CLIENT_ID_DISK,
|
|
);
|
|
|
|
if (account?.profile && migratedApiKeyClientId != null) {
|
|
account.profile.apiKeyClientId = migratedApiKeyClientId;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, API_KEY_CLIENT_ID_DISK, null);
|
|
|
|
// Rollback API key client secret
|
|
const migratedApiKeyClientSecret = await helper.getFromUser<string>(
|
|
userId,
|
|
API_KEY_CLIENT_SECRET_DISK,
|
|
);
|
|
|
|
if (account?.keys && migratedApiKeyClientSecret != null) {
|
|
account.keys.apiKeyClientSecret = migratedApiKeyClientSecret;
|
|
updatedLegacyAccount = true;
|
|
}
|
|
|
|
await helper.setToUser(userId, API_KEY_CLIENT_SECRET_DISK, null);
|
|
|
|
if (updatedLegacyAccount) {
|
|
await helper.set(userId, account);
|
|
}
|
|
}
|
|
|
|
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
|
}
|
|
}
|