1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 01:03:35 +00:00
Files
browser/libs/common/src/state-migrations/migrations/38-migrate-token-svc-to-state-provider.ts
Matt Gibson 9c1e2ebd67 Typescript-strict-plugin (#12235)
* Use typescript-strict-plugin to iteratively turn on strict

* Add strict testing to pipeline

Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files.

* turn on strict for scripts directory

* Use plugin for all tsconfigs in monorepo

vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes.

* remove plugin from configs that extend one that already has it

* Update workspace settings to honor strict plugin

* Apply strict-plugin to native message test runner

* Update vscode workspace to use root tsc version

* `./node_modules/.bin/update-strict-comments` 🤖

This is a one-time operation. All future files should adhere to strict type checking.

* Add fixme to `ts-strict-ignore` comments

* `update-strict-comments` 🤖

repeated for new merge files
2024-12-09 20:58:50 +01:00

246 lines
8.2 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
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))]);
}
}