Auth `UserKeyDefinition` Migration (#8587)
* Migrate DeviceTrustCryptoService * Migrate SsoLoginService * Migrate TokenService * Update libs/common/src/auth/services/token.state.ts Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> * Fix Test * Actually Fix Tests --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
parent
2bce6c538c
commit
84cd01165c
|
@ -14,7 +14,7 @@ import { StorageLocation } from "../../platform/enums";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
import { StorageOptions } from "../../platform/models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
import { DEVICE_TRUST_DISK_LOCAL, KeyDefinition, StateProvider } from "../../platform/state";
|
import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { UserKey, DeviceKey } from "../../types/key";
|
import { UserKey, DeviceKey } from "../../types/key";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction";
|
||||||
|
@ -27,16 +27,18 @@ import {
|
||||||
} from "../models/request/update-devices-trust.request";
|
} from "../models/request/update-devices-trust.request";
|
||||||
|
|
||||||
/** Uses disk storage so that the device key can persist after log out and tab removal. */
|
/** Uses disk storage so that the device key can persist after log out and tab removal. */
|
||||||
export const DEVICE_KEY = new KeyDefinition<DeviceKey>(DEVICE_TRUST_DISK_LOCAL, "deviceKey", {
|
export const DEVICE_KEY = new UserKeyDefinition<DeviceKey>(DEVICE_TRUST_DISK_LOCAL, "deviceKey", {
|
||||||
deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey,
|
deserializer: (deviceKey) => SymmetricCryptoKey.fromJSON(deviceKey) as DeviceKey,
|
||||||
|
clearOn: [], // Device key is needed to log back into device, so we can't clear it automatically during lock or logout
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Uses disk storage so that the shouldTrustDevice bool can persist across login. */
|
/** Uses disk storage so that the shouldTrustDevice bool can persist across login. */
|
||||||
export const SHOULD_TRUST_DEVICE = new KeyDefinition<boolean>(
|
export const SHOULD_TRUST_DEVICE = new UserKeyDefinition<boolean>(
|
||||||
DEVICE_TRUST_DISK_LOCAL,
|
DEVICE_TRUST_DISK_LOCAL,
|
||||||
"shouldTrustDevice",
|
"shouldTrustDevice",
|
||||||
{
|
{
|
||||||
deserializer: (shouldTrustDevice) => shouldTrustDevice,
|
deserializer: (shouldTrustDevice) => shouldTrustDevice,
|
||||||
|
clearOn: [], // Need to preserve the user setting, so we can't clear it automatically during lock or logout
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
KeyDefinition,
|
KeyDefinition,
|
||||||
SSO_DISK,
|
SSO_DISK,
|
||||||
StateProvider,
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "../abstractions/sso-login.service.abstraction";
|
||||||
|
|
||||||
|
@ -26,7 +27,19 @@ const SSO_STATE = new KeyDefinition<string>(SSO_DISK, "ssoState", {
|
||||||
/**
|
/**
|
||||||
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
|
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
|
||||||
*/
|
*/
|
||||||
const ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
|
const USER_ORGANIZATION_SSO_IDENTIFIER = new UserKeyDefinition<string>(
|
||||||
|
SSO_DISK,
|
||||||
|
"organizationSsoIdentifier",
|
||||||
|
{
|
||||||
|
deserializer: (organizationIdentifier) => organizationIdentifier,
|
||||||
|
clearOn: ["logout"], // Used for login, so not needed past logout
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses disk storage so that the organization sso identifier can be persisted across sso redirects.
|
||||||
|
*/
|
||||||
|
const GLOBAL_ORGANIZATION_SSO_IDENTIFIER = new KeyDefinition<string>(
|
||||||
SSO_DISK,
|
SSO_DISK,
|
||||||
"organizationSsoIdentifier",
|
"organizationSsoIdentifier",
|
||||||
{
|
{
|
||||||
|
@ -51,10 +64,10 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
|
||||||
constructor(private stateProvider: StateProvider) {
|
constructor(private stateProvider: StateProvider) {
|
||||||
this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER);
|
this.codeVerifierState = this.stateProvider.getGlobal(CODE_VERIFIER);
|
||||||
this.ssoState = this.stateProvider.getGlobal(SSO_STATE);
|
this.ssoState = this.stateProvider.getGlobal(SSO_STATE);
|
||||||
this.orgSsoIdentifierState = this.stateProvider.getGlobal(ORGANIZATION_SSO_IDENTIFIER);
|
this.orgSsoIdentifierState = this.stateProvider.getGlobal(GLOBAL_ORGANIZATION_SSO_IDENTIFIER);
|
||||||
this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL);
|
this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL);
|
||||||
this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive(
|
this.activeUserOrgSsoIdentifierState = this.stateProvider.getActive(
|
||||||
ORGANIZATION_SSO_IDENTIFIER,
|
USER_ORGANIZATION_SSO_IDENTIFIER,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,8 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
|
||||||
import {
|
import {
|
||||||
GlobalState,
|
GlobalState,
|
||||||
GlobalStateProvider,
|
GlobalStateProvider,
|
||||||
KeyDefinition,
|
|
||||||
SingleUserStateProvider,
|
SingleUserStateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
} from "../../platform/state";
|
} from "../../platform/state";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
|
||||||
|
@ -863,7 +863,7 @@ export class TokenService implements TokenServiceAbstraction {
|
||||||
|
|
||||||
private async getStateValueByUserIdAndKeyDef(
|
private async getStateValueByUserIdAndKeyDef(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
storageLocation: KeyDefinition<string>,
|
storageLocation: UserKeyDefinition<string>,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
// read from single user state provider
|
// read from single user state provider
|
||||||
return await firstValueFrom(this.singleUserStateProvider.get(userId, storageLocation).state$);
|
return await firstValueFrom(this.singleUserStateProvider.get(userId, storageLocation).state$);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { KeyDefinition } from "../../platform/state";
|
import { KeyDefinition, UserKeyDefinition } from "../../platform/state";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCESS_TOKEN_DISK,
|
ACCESS_TOKEN_DISK,
|
||||||
|
@ -28,8 +28,8 @@ describe.each([
|
||||||
"deserializes state key definitions",
|
"deserializes state key definitions",
|
||||||
(
|
(
|
||||||
keyDefinition:
|
keyDefinition:
|
||||||
| KeyDefinition<string>
|
| UserKeyDefinition<string>
|
||||||
| KeyDefinition<boolean>
|
| UserKeyDefinition<boolean>
|
||||||
| KeyDefinition<Record<string, string>>,
|
| KeyDefinition<Record<string, string>>,
|
||||||
state: string | boolean | Record<string, string>,
|
state: string | boolean | Record<string, string>,
|
||||||
) => {
|
) => {
|
||||||
|
@ -50,7 +50,10 @@ describe.each([
|
||||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function testDeserialization<T>(keyDefinition: KeyDefinition<T>, state: T) {
|
function testDeserialization<T>(
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
state: T,
|
||||||
|
) {
|
||||||
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
const deserialized = keyDefinition.deserializer(JSON.parse(JSON.stringify(state)));
|
||||||
expect(deserialized).toEqual(state);
|
expect(deserialized).toEqual(state);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,41 @@
|
||||||
import { KeyDefinition, TOKEN_DISK, TOKEN_DISK_LOCAL, TOKEN_MEMORY } from "../../platform/state";
|
import {
|
||||||
|
KeyDefinition,
|
||||||
|
TOKEN_DISK,
|
||||||
|
TOKEN_DISK_LOCAL,
|
||||||
|
TOKEN_MEMORY,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "../../platform/state";
|
||||||
|
|
||||||
// Note: all tokens / API key information must be cleared on logout.
|
// Note: all tokens / API key information must be cleared on logout.
|
||||||
// because we are using secure storage, we must manually call to clean up our tokens.
|
// because we are using secure storage, we must manually call to clean up our tokens.
|
||||||
// See stateService.deAuthenticateAccount for where we call clearTokens(...)
|
// See stateService.deAuthenticateAccount for where we call clearTokens(...)
|
||||||
|
|
||||||
export const ACCESS_TOKEN_DISK = new KeyDefinition<string>(TOKEN_DISK, "accessToken", {
|
export const ACCESS_TOKEN_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "accessToken", {
|
||||||
deserializer: (accessToken) => accessToken,
|
deserializer: (accessToken) => accessToken,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ACCESS_TOKEN_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "accessToken", {
|
export const ACCESS_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "accessToken", {
|
||||||
deserializer: (accessToken) => accessToken,
|
deserializer: (accessToken) => accessToken,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const REFRESH_TOKEN_DISK = new KeyDefinition<string>(TOKEN_DISK, "refreshToken", {
|
export const REFRESH_TOKEN_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "refreshToken", {
|
||||||
deserializer: (refreshToken) => refreshToken,
|
deserializer: (refreshToken) => refreshToken,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const REFRESH_TOKEN_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "refreshToken", {
|
export const REFRESH_TOKEN_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "refreshToken", {
|
||||||
deserializer: (refreshToken) => refreshToken,
|
deserializer: (refreshToken) => refreshToken,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new KeyDefinition<boolean>(
|
export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE = new UserKeyDefinition<boolean>(
|
||||||
TOKEN_DISK,
|
TOKEN_DISK,
|
||||||
"refreshTokenMigratedToSecureStorage",
|
"refreshTokenMigratedToSecureStorage",
|
||||||
{
|
{
|
||||||
deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage,
|
deserializer: (refreshTokenMigratedToSecureStorage) => refreshTokenMigratedToSecureStorage,
|
||||||
|
clearOn: [], // Don't clear on lock/logout so that we always check the correct place (secure storage) for the refresh token if it's been migrated
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -36,26 +47,34 @@ export const EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL = KeyDefinition.record<str
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const API_KEY_CLIENT_ID_DISK = new KeyDefinition<string>(TOKEN_DISK, "apiKeyClientId", {
|
export const API_KEY_CLIENT_ID_DISK = new UserKeyDefinition<string>(TOKEN_DISK, "apiKeyClientId", {
|
||||||
deserializer: (apiKeyClientId) => apiKeyClientId,
|
deserializer: (apiKeyClientId) => apiKeyClientId,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
});
|
});
|
||||||
|
|
||||||
export const API_KEY_CLIENT_ID_MEMORY = new KeyDefinition<string>(TOKEN_MEMORY, "apiKeyClientId", {
|
export const API_KEY_CLIENT_ID_MEMORY = new UserKeyDefinition<string>(
|
||||||
deserializer: (apiKeyClientId) => apiKeyClientId,
|
TOKEN_MEMORY,
|
||||||
});
|
"apiKeyClientId",
|
||||||
|
{
|
||||||
|
deserializer: (apiKeyClientId) => apiKeyClientId,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const API_KEY_CLIENT_SECRET_DISK = new KeyDefinition<string>(
|
export const API_KEY_CLIENT_SECRET_DISK = new UserKeyDefinition<string>(
|
||||||
TOKEN_DISK,
|
TOKEN_DISK,
|
||||||
"apiKeyClientSecret",
|
"apiKeyClientSecret",
|
||||||
{
|
{
|
||||||
deserializer: (apiKeyClientSecret) => apiKeyClientSecret,
|
deserializer: (apiKeyClientSecret) => apiKeyClientSecret,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const API_KEY_CLIENT_SECRET_MEMORY = new KeyDefinition<string>(
|
export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>(
|
||||||
TOKEN_MEMORY,
|
TOKEN_MEMORY,
|
||||||
"apiKeyClientSecret",
|
"apiKeyClientSecret",
|
||||||
{
|
{
|
||||||
deserializer: (apiKeyClientSecret) => apiKeyClientSecret,
|
deserializer: (apiKeyClientSecret) => apiKeyClientSecret,
|
||||||
|
clearOn: [], // Manually handled
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue