import { Observable, combineLatest, firstValueFrom, map } from "rxjs"; import { Opaque } from "type-fest"; import { decodeJwtTokenToJson } from "@bitwarden/auth/common"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; import { EncString, EncryptedString } from "../../platform/models/domain/enc-string"; import { StorageOptions } from "../../platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { GlobalState, GlobalStateProvider, SingleUserStateProvider, UserKeyDefinition, } from "../../platform/state"; import { UserId } from "../../types/guid"; import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service"; import { ACCOUNT_ACTIVE_ACCOUNT_ID } from "./account.service"; import { ACCESS_TOKEN_DISK, ACCESS_TOKEN_MEMORY, API_KEY_CLIENT_ID_DISK, API_KEY_CLIENT_ID_MEMORY, API_KEY_CLIENT_SECRET_DISK, API_KEY_CLIENT_SECRET_MEMORY, EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { Disk = "disk", SecureStorage = "secureStorage", Memory = "memory", } /** * Type representing the structure of a standard Bitwarden decoded access token. * src: https://datatracker.ietf.org/doc/html/rfc7519#section-4.1 * Note: all claims are technically optional so we must verify their existence before using them. * Note 2: NumericDate is a number representing a date in seconds since the Unix epoch. */ export type DecodedAccessToken = { /** Issuer - the issuer of the token, typically the URL of the authentication server */ iss?: string; /** Not Before - a timestamp defining when the token starts being valid */ nbf?: number; /** Issued At - a timestamp of when the token was issued */ iat?: number; /** Expiration Time - a NumericDate timestamp of when the token will expire */ exp?: number; /** Scope - the scope of the access request, such as the permissions the token grants */ scope?: string[]; /** Authentication Method Reference - the methods used in the authentication */ amr?: string[]; /** Client ID - the identifier for the client that requested the token */ client_id?: string; /** Subject - the unique identifier for the user */ sub?: string; /** Authentication Time - a timestamp of when the user authentication occurred */ auth_time?: number; /** Identity Provider - the system or service that authenticated the user */ idp?: string; /** Premium - a boolean flag indicating whether the account is premium */ premium?: boolean; /** Email - the user's email address */ email?: string; /** Email Verified - a boolean flag indicating whether the user's email address has been verified */ email_verified?: boolean; /** * Security Stamp - a unique identifier which invalidates the access token if it changes in the db * (typically after critical account changes like a password change) */ sstamp?: string; /** Name - the name of the user */ name?: string; /** Organization Owners - a list of organization owner identifiers */ orgowner?: string[]; /** Device - the identifier of the device used */ device?: string; /** JWT ID - a unique identifier for the JWT */ jti?: string; }; /** * A symmetric key for encrypting the access token before the token is stored on disk. * This key should be stored in secure storage. * */ type AccessTokenKey = Opaque; export class TokenService implements TokenServiceAbstraction { private readonly accessTokenKeySecureStorageKey: string = "_accessTokenKey"; private readonly refreshTokenSecureStorageKey: string = "_refreshToken"; private emailTwoFactorTokenRecordGlobalState: GlobalState>; private activeUserIdGlobalState: GlobalState; constructor( // Note: we cannot use ActiveStateProvider because if we ever want to inject // this service into the AccountService, we will make a circular dependency private singleUserStateProvider: SingleUserStateProvider, private globalStateProvider: GlobalStateProvider, private readonly platformSupportsSecureStorage: boolean, private secureStorageService: AbstractStorageService, private keyGenerationService: KeyGenerationService, private encryptService: EncryptService, private logService: LogService, ) { this.initializeState(); } hasAccessToken$(userId: UserId): Observable { // FIXME Once once vault timeout action is observable, we can use it to determine storage location // and avoid the need to check both disk and memory. return combineLatest([ this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).state$, this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).state$, ]).pipe(map(([disk, memory]) => Boolean(disk || memory))); } // pivoting to an approach where we create a symmetric key we store in secure storage // which is used to protect the data before persisting to disk. // We will also use the same symmetric key to decrypt the data when reading from disk. private initializeState(): void { this.emailTwoFactorTokenRecordGlobalState = this.globalStateProvider.get( EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, ); this.activeUserIdGlobalState = this.globalStateProvider.get(ACCOUNT_ACTIVE_ACCOUNT_ID); } async setTokens( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, refreshToken?: string, clientIdClientSecret?: [string, string], ): Promise { if (!accessToken) { throw new Error("Access token is required."); } // get user id the access token const userId: UserId = await this.getUserIdFromAccessToken(accessToken); if (!userId) { throw new Error("User id not found. Cannot set tokens."); } await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); if (refreshToken) { await this.setRefreshToken(refreshToken, vaultTimeoutAction, vaultTimeout, userId); } if (clientIdClientSecret != null) { await this.setClientId(clientIdClientSecret[0], vaultTimeoutAction, vaultTimeout, userId); await this.setClientSecret(clientIdClientSecret[1], vaultTimeoutAction, vaultTimeout, userId); } } private async getAccessTokenKey(userId: UserId): Promise { const accessTokenKeyB64 = await this.secureStorageService.get< ReturnType >(`${userId}${this.accessTokenKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); if (!accessTokenKeyB64) { return null; } const accessTokenKey = SymmetricCryptoKey.fromJSON(accessTokenKeyB64) as AccessTokenKey; return accessTokenKey; } private async createAndSaveAccessTokenKey(userId: UserId): Promise { const newAccessTokenKey = (await this.keyGenerationService.createKey(512)) as AccessTokenKey; await this.secureStorageService.save( `${userId}${this.accessTokenKeySecureStorageKey}`, newAccessTokenKey, this.getSecureStorageOptions(userId), ); return newAccessTokenKey; } private async clearAccessTokenKey(userId: UserId): Promise { await this.secureStorageService.remove( `${userId}${this.accessTokenKeySecureStorageKey}`, this.getSecureStorageOptions(userId), ); } private async getOrCreateAccessTokenKey(userId: UserId): Promise { if (!this.platformSupportsSecureStorage) { throw new Error("Platform does not support secure storage. Cannot obtain access token key."); } if (!userId) { throw new Error("User id not found. Cannot obtain access token key."); } // First see if we have an accessTokenKey in secure storage and return it if we do let accessTokenKey: AccessTokenKey = await this.getAccessTokenKey(userId); if (!accessTokenKey) { // Otherwise, create a new one and save it to secure storage, then return it accessTokenKey = await this.createAndSaveAccessTokenKey(userId); } return accessTokenKey; } private async encryptAccessToken(accessToken: string, userId: UserId): Promise { const accessTokenKey = await this.getOrCreateAccessTokenKey(userId); return await this.encryptService.encrypt(accessToken, accessTokenKey); } private async decryptAccessToken( encryptedAccessToken: EncString, userId: UserId, ): Promise { const accessTokenKey = await this.getAccessTokenKey(userId); if (!accessTokenKey) { // If we don't have an accessTokenKey, then that means we don't have an access token as it hasn't been set yet // and we have to return null here to properly indicate the the user isn't logged in. return null; } const decryptedAccessToken = await this.encryptService.decryptToUtf8( encryptedAccessToken, accessTokenKey, ); return decryptedAccessToken; } /** * Internal helper for set access token which always requires user id. * This is useful because setTokens always will have a user id from the access token whereas * the public setAccessToken method does not. */ private async _setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, userId: UserId, ): Promise { const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, true, ); switch (storageLocation) { case TokenStorageLocation.SecureStorage: { // Secure storage implementations have variable length limitations (Windows), so we cannot // store the access token directly. Instead, we encrypt with accessTokenKey and store that // in secure storage. const encryptedAccessToken: EncString = await this.encryptAccessToken(accessToken, userId); // Save the encrypted access token to disk await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_DISK) .update((_) => encryptedAccessToken.encryptedString); // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 // 2024-02-20: Remove access token from memory so that we migrate to encrypt the access token over time. // Remove this call to remove the access token from memory after 3 releases. await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); return; } case TokenStorageLocation.Disk: // Access token stored on disk unencrypted as platform does not support secure storage await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_DISK) .update((_) => accessToken); return; case TokenStorageLocation.Memory: // Access token stored in memory due to vault timeout settings await this.singleUserStateProvider .get(userId, ACCESS_TOKEN_MEMORY) .update((_) => accessToken); return; } } async setAccessToken( accessToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, ): Promise { if (!accessToken) { throw new Error("Access token is required."); } const userId: UserId = await this.getUserIdFromAccessToken(accessToken); // If we don't have a user id, we can't save the value if (!userId) { throw new Error("User id not found. Cannot save access token."); } await this._setAccessToken(accessToken, vaultTimeoutAction, vaultTimeout, userId); } async clearAccessToken(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); // If we don't have a user id, we can't clear the value if (!userId) { throw new Error("User id not found. Cannot clear access token."); } // TODO: re-eval this implementation once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout // but we can simply clear all locations to avoid the need to require those parameters. if (this.platformSupportsSecureStorage) { // Always clear the access token key when clearing the access token // The next set of the access token will create a new access token key await this.clearAccessTokenKey(userId); } // Platform doesn't support secure storage, so use state provider implementation await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).update((_) => null); } async getAccessToken(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { return undefined; } // Try to get the access token from memory const accessTokenMemory = await this.getStateValueByUserIdAndKeyDef( userId, ACCESS_TOKEN_MEMORY, ); if (accessTokenMemory != null) { return accessTokenMemory; } // If memory is null, read from disk const accessTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, ACCESS_TOKEN_DISK); if (!accessTokenDisk) { return null; } if (this.platformSupportsSecureStorage) { const accessTokenKey = await this.getAccessTokenKey(userId); if (!accessTokenKey) { // We know this is an unencrypted access token because we don't have an access token key return accessTokenDisk; } try { const encryptedAccessTokenEncString = new EncString(accessTokenDisk as EncryptedString); const decryptedAccessToken = await this.decryptAccessToken( encryptedAccessTokenEncString, userId, ); return decryptedAccessToken; } catch (error) { // If an error occurs during decryption, return null for logout. // We don't try to recover here since we'd like to know // if access token and key are getting out of sync. this.logService.error( `Failed to decrypt access token: ${error?.message ?? "Unknown error."}`, ); return null; } } return accessTokenDisk; } // Private because we only ever set the refresh token when also setting the access token // and we need the user id from the access token to save to secure storage private async setRefreshToken( refreshToken: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, userId: UserId, ): Promise { // If we don't have a user id, we can't save the value if (!userId) { throw new Error("User id not found. Cannot save refresh token."); } const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, true, ); switch (storageLocation) { case TokenStorageLocation.SecureStorage: await this.saveStringToSecureStorage( userId, this.refreshTokenSecureStorageKey, refreshToken, ); // TODO: PM-6408 - https://bitwarden.atlassian.net/browse/PM-6408 // 2024-02-20: Remove refresh token from memory and disk so that we migrate to secure storage over time. // Remove these 2 calls to remove the refresh token from memory and disk after 3 releases. await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); return; case TokenStorageLocation.Disk: await this.singleUserStateProvider .get(userId, REFRESH_TOKEN_DISK) .update((_) => refreshToken); return; case TokenStorageLocation.Memory: await this.singleUserStateProvider .get(userId, REFRESH_TOKEN_MEMORY) .update((_) => refreshToken); return; } } async getRefreshToken(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { return undefined; } // pre-secure storage migration: // Always read memory first b/c faster const refreshTokenMemory = await this.getStateValueByUserIdAndKeyDef( userId, REFRESH_TOKEN_MEMORY, ); if (refreshTokenMemory != null) { return refreshTokenMemory; } // if memory is null, read from disk and then secure storage const refreshTokenDisk = await this.getStateValueByUserIdAndKeyDef(userId, REFRESH_TOKEN_DISK); if (refreshTokenDisk != null) { return refreshTokenDisk; } if (this.platformSupportsSecureStorage) { const refreshTokenSecureStorage = await this.getStringFromSecureStorage( userId, this.refreshTokenSecureStorageKey, ); if (refreshTokenSecureStorage != null) { return refreshTokenSecureStorage; } } return null; } private async clearRefreshToken(userId: UserId): Promise { // If we don't have a user id, we can't clear the value if (!userId) { throw new Error("User id not found. Cannot clear refresh token."); } // TODO: re-eval this once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout // but we can simply clear all locations to avoid the need to require those parameters if (this.platformSupportsSecureStorage) { await this.secureStorageService.remove( `${userId}${this.refreshTokenSecureStorageKey}`, this.getSecureStorageOptions(userId), ); } // Platform doesn't support secure storage, so use state provider implementation await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_MEMORY).update((_) => null); await this.singleUserStateProvider.get(userId, REFRESH_TOKEN_DISK).update((_) => null); } async setClientId( clientId: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); // If we don't have a user id, we can't save the value if (!userId) { throw new Error("User id not found. Cannot save client id."); } const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, false, // don't use secure storage for client id ); if (storageLocation === TokenStorageLocation.Disk) { await this.singleUserStateProvider .get(userId, API_KEY_CLIENT_ID_DISK) .update((_) => clientId); } else if (storageLocation === TokenStorageLocation.Memory) { await this.singleUserStateProvider .get(userId, API_KEY_CLIENT_ID_MEMORY) .update((_) => clientId); } } async getClientId(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { return undefined; } // Always read memory first b/c faster const apiKeyClientIdMemory = await this.getStateValueByUserIdAndKeyDef( userId, API_KEY_CLIENT_ID_MEMORY, ); if (apiKeyClientIdMemory != null) { return apiKeyClientIdMemory; } // if memory is null, read from disk return await this.getStateValueByUserIdAndKeyDef(userId, API_KEY_CLIENT_ID_DISK); } private async clearClientId(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); // If we don't have a user id, we can't clear the value if (!userId) { throw new Error("User id not found. Cannot clear client id."); } // TODO: re-eval this once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout // but we can simply clear both locations to avoid the need to require those parameters // Platform doesn't support secure storage, so use state provider implementation await this.singleUserStateProvider.get(userId, API_KEY_CLIENT_ID_MEMORY).update((_) => null); await this.singleUserStateProvider.get(userId, API_KEY_CLIENT_ID_DISK).update((_) => null); } async setClientSecret( clientSecret: string, vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, userId?: UserId, ): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { throw new Error("User id not found. Cannot save client secret."); } const storageLocation = await this.determineStorageLocation( vaultTimeoutAction, vaultTimeout, false, // don't use secure storage for client secret ); if (storageLocation === TokenStorageLocation.Disk) { await this.singleUserStateProvider .get(userId, API_KEY_CLIENT_SECRET_DISK) .update((_) => clientSecret); } else if (storageLocation === TokenStorageLocation.Memory) { await this.singleUserStateProvider .get(userId, API_KEY_CLIENT_SECRET_MEMORY) .update((_) => clientSecret); } } async getClientSecret(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { return undefined; } // Always read memory first b/c faster const apiKeyClientSecretMemory = await this.getStateValueByUserIdAndKeyDef( userId, API_KEY_CLIENT_SECRET_MEMORY, ); if (apiKeyClientSecretMemory != null) { return apiKeyClientSecretMemory; } // if memory is null, read from disk return await this.getStateValueByUserIdAndKeyDef(userId, API_KEY_CLIENT_SECRET_DISK); } private async clearClientSecret(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); // If we don't have a user id, we can't clear the value if (!userId) { throw new Error("User id not found. Cannot clear client secret."); } // TODO: re-eval this once we get shared key definitions for vault timeout and vault timeout action data. // we can't determine storage location w/out vaultTimeoutAction and vaultTimeout // but we can simply clear both locations to avoid the need to require those parameters // Platform doesn't support secure storage, so use state provider implementation await this.singleUserStateProvider .get(userId, API_KEY_CLIENT_SECRET_MEMORY) .update((_) => null); await this.singleUserStateProvider.get(userId, API_KEY_CLIENT_SECRET_DISK).update((_) => null); } async setTwoFactorToken(email: string, twoFactorToken: string): Promise { await this.emailTwoFactorTokenRecordGlobalState.update((emailTwoFactorTokenRecord) => { emailTwoFactorTokenRecord ??= {}; emailTwoFactorTokenRecord[email] = twoFactorToken; return emailTwoFactorTokenRecord; }); } async getTwoFactorToken(email: string): Promise { const emailTwoFactorTokenRecord: Record = await firstValueFrom( this.emailTwoFactorTokenRecordGlobalState.state$, ); if (!emailTwoFactorTokenRecord) { return null; } return emailTwoFactorTokenRecord[email]; } async clearTwoFactorToken(email: string): Promise { await this.emailTwoFactorTokenRecordGlobalState.update((emailTwoFactorTokenRecord) => { emailTwoFactorTokenRecord ??= {}; delete emailTwoFactorTokenRecord[email]; return emailTwoFactorTokenRecord; }); } // TODO: stop accepting optional userIds async clearTokens(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { throw new Error("User id not found. Cannot clear tokens."); } await Promise.all([ this.clearAccessToken(userId), this.clearRefreshToken(userId), this.clearClientId(userId), this.clearClientSecret(userId), ]); } // jwthelper methods // ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js async decodeAccessToken(token?: string): Promise { token = token ?? (await this.getAccessToken()); if (token == null) { throw new Error("Access token not found."); } return decodeJwtTokenToJson(token) as DecodedAccessToken; } // TODO: PM-6678- tech debt - consider consolidating the return types of all these access // token data retrieval methods to return null if something goes wrong instead of throwing an error. async getTokenExpirationDate(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } // per RFC, exp claim is optional but if it exists, it should be a number if (!decoded || typeof decoded.exp !== "number") { return null; } // The 0 in Date(0) is the key; it sets the date to the epoch const expirationDate = new Date(0); expirationDate.setUTCSeconds(decoded.exp); return expirationDate; } async tokenSecondsRemaining(offsetSeconds = 0): Promise { const date = await this.getTokenExpirationDate(); if (date == null) { return 0; } const msRemaining = date.valueOf() - (new Date().valueOf() + offsetSeconds * 1000); return Math.round(msRemaining / 1000); } async tokenNeedsRefresh(minutes = 5): Promise { const sRemaining = await this.tokenSecondsRemaining(); return sRemaining < 60 * minutes; } async getUserId(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.sub !== "string") { throw new Error("No user id found"); } return decoded.sub as UserId; } private async getUserIdFromAccessToken(accessToken: string): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(accessToken); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.sub !== "string") { throw new Error("No user id found"); } return decoded.sub as UserId; } async getEmail(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.email !== "string") { throw new Error("No email found"); } return decoded.email; } async getEmailVerified(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.email_verified !== "boolean") { throw new Error("No email verification found"); } return decoded.email_verified; } async getName(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.name !== "string") { return null; } return decoded.name; } async getIssuer(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } if (!decoded || typeof decoded.iss !== "string") { throw new Error("No issuer found"); } return decoded.iss; } async getIsExternal(): Promise { let decoded: DecodedAccessToken; try { decoded = await this.decodeAccessToken(); } catch (error) { throw new Error("Failed to decode access token: " + error.message); } return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } async getSecurityStamp(userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { throw new Error("User id not found. Cannot get security stamp."); } const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); return securityStamp; } async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); if (!userId) { throw new Error("User id not found. Cannot set security stamp."); } await this.singleUserStateProvider .get(userId, SECURITY_STAMP_MEMORY) .update((_) => securityStamp); } private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition, ): Promise { // read from single user state provider return await firstValueFrom(this.singleUserStateProvider.get(userId, storageLocation).state$); } private async determineStorageLocation( vaultTimeoutAction: VaultTimeoutAction, vaultTimeout: number | null, useSecureStorage: boolean, ): Promise { if (vaultTimeoutAction === VaultTimeoutAction.LogOut && vaultTimeout != null) { return TokenStorageLocation.Memory; } else { if (useSecureStorage && this.platformSupportsSecureStorage) { return TokenStorageLocation.SecureStorage; } return TokenStorageLocation.Disk; } } private async saveStringToSecureStorage( userId: UserId, storageKey: string, value: string, ): Promise { await this.secureStorageService.save( `${userId}${storageKey}`, value, this.getSecureStorageOptions(userId), ); } private async getStringFromSecureStorage( userId: UserId, storageKey: string, ): Promise { // If we have a user ID, read from secure storage. return await this.secureStorageService.get( `${userId}${storageKey}`, this.getSecureStorageOptions(userId), ); } private getSecureStorageOptions(userId: UserId): StorageOptions { return { storageLocation: StorageLocation.Disk, useSecureStorage: true, userId: userId, }; } }