diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fa1add0602..15f21b2501 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -813,7 +813,6 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, - this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 437f807bc6..58329128b8 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -631,7 +631,6 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, - this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 27b182de5d..f31bcb1c51 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -628,7 +628,6 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, - TokenServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index e0833342ce..431f736e94 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -27,6 +27,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Account, AccountProfile, + AccountTokens, AccountKeys, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -212,6 +213,9 @@ describe("LoginStrategy", () => { kdfType: kdf, }, }, + tokens: { + ...new AccountTokens(), + }, keys: new AccountKeys(), }), ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a73c32e120..a6dc193183 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -27,7 +27,11 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { + Account, + AccountProfile, + AccountTokens, +} from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -188,6 +192,9 @@ export abstract class LoginStrategy { kdfType: tokenResponse.kdf, }, }, + tokens: { + ...new AccountTokens(), + }, }), ); diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index fc3bd317f4..75bb383882 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,10 +213,4 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; - - /** Gets the active or passed in user's security stamp */ - getSecurityStamp: (userId?: UserId) => Promise; - - /** Sets the security stamp for the active or passed in user */ - setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index 3e92053d2f..d32c4d8e1c 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,7 +23,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2192,84 +2191,6 @@ describe("TokenService", () => { }); }); - describe("Security Stamp methods", () => { - const mockSecurityStamp = "securityStamp"; - - describe("setSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.setSecurityStamp(mockSecurityStamp); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); - }); - - it("should set the security stamp in memory when there is an active user in global state", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - // Act - await tokenService.setSecurityStamp(mockSecurityStamp); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - - it("should set the security stamp in memory for the specified user id", async () => { - // Act - await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); - - // Assert - expect( - singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, - ).toHaveBeenCalledWith(mockSecurityStamp); - }); - }); - - describe("getSecurityStamp", () => { - it("should throw an error if no user id is provided and there is no active user in global state", async () => { - // Act - // note: don't await here because we want to test the error - const result = tokenService.getSecurityStamp(); - // Assert - await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); - }); - - it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { - // Arrange - globalStateProvider - .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) - .stateSubject.next(userIdFromAccessToken); - - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(); - - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - - it("should return the security stamp from memory for the specified user id", async () => { - // Arrange - singleUserStateProvider - .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) - .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); - - // Act - const result = await tokenService.getSecurityStamp(userIdFromAccessToken); - // Assert - expect(result).toEqual(mockSecurityStamp); - }); - }); - }); - // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index 40036a8453..c24a2c186b 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,7 +32,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -851,30 +850,6 @@ export class TokenService implements TokenServiceAbstraction { 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, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index bb82410fac..dc00fec383 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,7 +10,6 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, - SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -23,7 +22,6 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], - [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 57d85f2a55..458d6846c1 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,8 +69,3 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( clearOn: [], // Manually handled }, ); - -export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { - deserializer: (securityStamp) => securityStamp, - clearOn: ["logout"], -}); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index f1d4b3848e..051604f0ae 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -181,6 +181,8 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; + getSecurityStamp: (options?: StorageOptions) => Promise; + setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts new file mode 100644 index 0000000000..733b3908e9 --- /dev/null +++ b/libs/common/src/platform/models/domain/account-tokens.spec.ts @@ -0,0 +1,9 @@ +import { AccountTokens } from "./account"; + +describe("AccountTokens", () => { + describe("fromJSON", () => { + it("should deserialize to an instance of itself", () => { + expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); + }); + }); +}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 77c242b6ff..0c76c16cc2 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,12 +10,14 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); + const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); + expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index cd416ec1f9..5a9a764696 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -171,11 +171,24 @@ export class AccountSettings { } } +export class AccountTokens { + securityStamp?: string; + + static fromJSON(obj: Jsonify): AccountTokens { + if (obj == null) { + return null; + } + + return Object.assign(new AccountTokens(), obj); + } +} + export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); + tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial) { Object.assign(this, { @@ -195,6 +208,10 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, + tokens: { + ...new AccountTokens(), + ...init?.tokens, + }, }); } @@ -208,6 +225,7 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), + tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d0a55d7a47..f660cd7a34 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -839,6 +839,23 @@ export class StateService< ); } + async getSecurityStamp(options?: StorageOptions): Promise { + return ( + await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) + )?.tokens?.securityStamp; + } + + async setSecurityStamp(value: string, options?: StorageOptions): Promise { + const account = await this.getAccount( + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + account.tokens.securityStamp = value; + await this.saveAccount( + account, + this.reconcileOptions(options, await this.defaultInMemoryOptions()), + ); + } + async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 73869ff488..ff8e9f1f4f 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,7 +15,6 @@ import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; -import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -74,7 +73,6 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, - private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -311,7 +309,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.tokenService.getSecurityStamp(response.id as UserId); + const stamp = await this.stateService.getSecurityStamp(); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -325,7 +323,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.tokenService.setSecurityStamp(response.securityStamp, response.id as UserId); + await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); await this.billingAccountProfileStateService.setHasPremium(