import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { Environment, EnvironmentService, } from "@bitwarden/common/platform/abstractions/environment.service"; 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 { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy"; describe("UserApiLoginStrategy", () => { let cache: UserApiLoginStrategyData; let cryptoService: MockProxy; let apiService: MockProxy; let tokenService: MockProxy; let appIdService: MockProxy; let platformUtilsService: MockProxy; let messagingService: MockProxy; let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; const deviceId = Utils.newGuid(); const keyConnectorUrl = "KEY_CONNECTOR_URL"; const apiClientId = "API_CLIENT_ID"; const apiClientSecret = "API_CLIENT_SECRET"; beforeEach(async () => { cryptoService = mock(); apiService = mock(); tokenService = mock(); appIdService = mock(); platformUtilsService = mock(); messagingService = mock(); logService = mock(); stateService = mock(); twoFactorService = mock(); userDecryptionOptionsService = mock(); keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); tokenService.decodeAccessToken.mockResolvedValue({}); apiLogInStrategy = new UserApiLoginStrategy( cache, cryptoService, apiService, tokenService, appIdService, platformUtilsService, messagingService, logService, stateService, twoFactorService, userDecryptionOptionsService, environmentService, keyConnectorService, billingAccountProfileStateService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); }); it("sends api key credentials to the server", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); await apiLogInStrategy.logIn(credentials); expect(apiService.postIdentityToken).toHaveBeenCalledWith( expect.objectContaining({ clientId: apiClientId, clientSecret: apiClientSecret, device: expect.objectContaining({ identifier: deviceId, }), twoFactor: expect.objectContaining({ provider: null, token: null, }), }), ); }); it("sets the local environment after a successful login", async () => { apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); const mockVaultTimeoutAction = VaultTimeoutAction.Lock; const mockVaultTimeout = 60; stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); await apiLogInStrategy.logIn(credentials); expect(tokenService.setClientId).toHaveBeenCalledWith( apiClientId, mockVaultTimeoutAction, mockVaultTimeout, ); expect(tokenService.setClientSecret).toHaveBeenCalledWith( apiClientSecret, mockVaultTimeoutAction, mockVaultTimeout, ); expect(stateService.addAccount).toHaveBeenCalled(); }); it("sets the encrypted user key and private key from the identity token response", async () => { const tokenResponse = identityTokenResponseFactory(); apiService.postIdentityToken.mockResolvedValue(tokenResponse); await apiLogInStrategy.logIn(credentials); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); }); it("gets and sets the master key if Key Connector is enabled", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.apiUseKeyConnector = true; const env = mock(); env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl); environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); await apiLogInStrategy.logIn(credentials); expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl); }); it("decrypts and sets the user key if Key Connector is enabled", async () => { const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; const masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; const tokenResponse = identityTokenResponseFactory(); tokenResponse.apiUseKeyConnector = true; const env = mock(); env.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl); environmentService.environment$ = new BehaviorSubject(env); apiService.postIdentityToken.mockResolvedValue(tokenResponse); cryptoService.getMasterKey.mockResolvedValue(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); await apiLogInStrategy.logIn(credentials); expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); }); });