diff --git a/spec/common/services/auth.service.spec.ts b/spec/common/services/auth.service.spec.ts deleted file mode 100644 index 71c04ed3..00000000 --- a/spec/common/services/auth.service.spec.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; - -import { ApiService } from "jslib-common/abstractions/api.service"; -import { AppIdService } from "jslib-common/abstractions/appId.service"; -import { AuthService } from "jslib-common/abstractions/auth.service"; -import { CryptoService } from "jslib-common/abstractions/crypto.service"; -import { EnvironmentService } from "jslib-common/abstractions/environment.service"; -import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; -import { LogService } from "jslib-common/abstractions/log.service"; -import { MessagingService } from "jslib-common/abstractions/messaging.service"; -import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; -import { StateService } from "jslib-common/abstractions/state.service"; -import { TokenService } from "jslib-common/abstractions/token.service"; - -import { PasswordLogInDelegate } from 'jslib-common/services/logInDelegate/passwordLogin.delegate'; -import { ApiLogInDelegate } from 'jslib-common/services/logInDelegate/apiLogin.delegate'; -import { SsoLogInDelegate } from 'jslib-common/services/logInDelegate/ssoLogin.delegate'; - -import { Utils } from "jslib-common/misc/utils"; - -import { Account, AccountProfile, AccountTokens } from "jslib-common/models/domain/account"; -import { AuthResult } from "jslib-common/models/domain/authResult"; -import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; - -import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; - -import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; -import { HashPurpose } from "jslib-common/enums/hashPurpose"; -import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; - -describe("LogInDelegates", () => { - let cryptoService: SubstituteOf; - let apiService: SubstituteOf; - let tokenService: SubstituteOf; - let appIdService: SubstituteOf; - let platformUtilsService: SubstituteOf; - let messagingService: SubstituteOf; - let logService: SubstituteOf; - let environmentService: SubstituteOf; - let keyConnectorService: SubstituteOf; - let stateService: SubstituteOf; - let twoFactorService: SubstituteOf; - let authService: SubstituteOf; - const setCryptoKeys = true; - - let passwordLogInDelegate: PasswordLogInDelegate; - let apiLogInDelegate: ApiLogInDelegate; - let ssoLogInDelegate: SsoLogInDelegate; - - const email = "hello@world.com"; - const masterPassword = "password"; - const hashedPassword = "HASHED_PASSWORD"; - const localHashedPassword = "LOCAL_HASHED_PASSWORD"; - const preloginKey = new SymmetricCryptoKey( - Utils.fromB64ToArray( - "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==" - ) - ); - const deviceId = Utils.newGuid(); - const accessToken = "ACCESS_TOKEN"; - const refreshToken = "REFRESH_TOKEN"; - const encKey = "ENC_KEY"; - const privateKey = "PRIVATE_KEY"; - const keyConnectorUrl = "KEY_CONNECTOR_URL"; - const kdf = 0; - const kdfIterations = 10000; - const userId = Utils.newGuid(); - - const decodedToken = { - sub: userId, - email: email, - premium: false, - }; - - const ssoCode = "SSO_CODE"; - const ssoCodeVerifier = "SSO_CODE_VERIFIER"; - const ssoRedirectUrl = "SSO_REDIRECT_URL"; - const ssoOrgId = "SSO_ORG_ID"; - - const twoFactorProviderType = TwoFactorProviderType.Authenticator; - const twoFactorToken = "TWO_FACTOR_TOKEN"; - const twoFactorRemember = true; - - const apiClientId = "API_CLIENT_ID"; - const apiClientSecret = "API_CLIENT_SECRET"; - - beforeEach(() => { - cryptoService = Substitute.for(); - apiService = Substitute.for(); - tokenService = Substitute.for(); - appIdService = Substitute.for(); - platformUtilsService = Substitute.for(); - messagingService = Substitute.for(); - logService = Substitute.for(); - environmentService = Substitute.for(); - stateService = Substitute.for(); - keyConnectorService = Substitute.for(); - twoFactorService = Substitute.for(); - authService = Substitute.for(); - - passwordLogInDelegate = new PasswordLogInDelegate(cryptoService, apiService, tokenService, appIdService, platformUtilsService, messagingService, logService, stateService, setCryptoKeys, twoFactorService, authService); - apiLogInDelegate = new ApiLogInDelegate(cryptoService, apiService, tokenService, appIdService, platformUtilsService, messagingService, logService, stateService, setCryptoKeys, twoFactorService, environmentService, keyConnectorService); - ssoLogInDelegate = new SsoLogInDelegate(cryptoService, apiService, tokenService, appIdService, platformUtilsService, messagingService, logService, stateService, setCryptoKeys, twoFactorService, keyConnectorService); - - appIdService.getAppId().resolves(deviceId); - tokenService.decodeToken(accessToken).resolves(decodedToken); - }); - - describe("Master Password authentication", () => { - beforeEach(() => { - passwordLogInSetup(); - }); - - it("works in simple cases (e.g. no 2FA, no captcha)", async () => { - // Arrange - apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); - tokenService.getTwoFactorToken().resolves(null); - - // Act - await passwordLogInDelegate.init(email, masterPassword); - const result = await passwordLogInDelegate.logIn(); - - // Assert - // Api call: - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const passwordTokenRequest = actual as any; // Need to access private fields - return ( - passwordTokenRequest.email === email && - passwordTokenRequest.masterPasswordHash === hashedPassword && - passwordTokenRequest.device.identifier === deviceId && - passwordTokenRequest.twoFactor.provider == null && - passwordTokenRequest.twoFactor.token == null && - passwordTokenRequest.captchaResponse == null - ); - }) - ); - - // Sets local environment: - commonSuccessAssertions(); - cryptoService.received(1).setKey(preloginKey); - cryptoService.received(1).setKeyHash(localHashedPassword); - cryptoService.received(1).setEncKey(encKey); - cryptoService.received(1).setEncPrivateKey(privateKey); - - // Negative tests - apiService.didNotReceive().postAccountKeys(Arg.any()); // Did not generate new private key pair - keyConnectorService.didNotReceive().getAndSetKey(Arg.any()); // Did not fetch Key Connector key - keyConnectorService.didNotReceive().convertNewSsoUserToKeyConnector(Arg.all()); // Did not send key to Key Connector - tokenService.didNotReceive().setTwoFactorToken(Arg.any()); // Did not save 2FA token - - // Return result: - const expected = buildAuthResponse(); - expect(result).toEqual(expected); - }); - - it("rejects login if CAPTCHA is required", async () => { - // Arrange - const siteKey = "CAPTCHA_SITE_KEY"; - const tokenResponse = newTokenResponse(); - (tokenResponse as any).siteKey = siteKey; - - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - // Act - await passwordLogInDelegate.init(email, masterPassword); - const result = await passwordLogInDelegate.logIn(); - - // Assert - stateService.didNotReceive().addAccount(Arg.any()); - messagingService.didNotReceive().send(Arg.any()); - - const expected = new AuthResult(); - expected.captchaSiteKey = siteKey; - expect(result).toEqual(expected); - }); - - it("does not set crypto keys if setCryptoKeys is false", async () => { - // Arrange - apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); - - // Re-init setCryptoKeys = false - passwordLogInDelegate = new PasswordLogInDelegate(cryptoService, apiService, tokenService, appIdService, platformUtilsService, messagingService, logService, stateService, false, twoFactorService, authService); - - // Act - await passwordLogInDelegate.init(email, masterPassword); - await passwordLogInDelegate.logIn(); - - // Assertions - commonSuccessAssertions(); - cryptoService.didNotReceive().setKey(Arg.any()); - cryptoService.didNotReceive().setKeyHash(Arg.any()); - cryptoService.didNotReceive().setEncKey(Arg.any()); - cryptoService.didNotReceive().setEncPrivateKey(Arg.any()); - }); - - it("makes a new public and private key for an old account", async () => { - const tokenResponse = newTokenResponse(); - tokenResponse.privateKey = null; - - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - await passwordLogInDelegate.init(email, masterPassword); - await passwordLogInDelegate.logIn(); - - commonSuccessAssertions(); - apiService.received(1).postAccountKeys(Arg.any()); - }); - }); - - // SSO tests - describe("Single sign-on authentication", () => { - it("handles authentication with SSO in simple cases", async () => { - // Arrange - apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); - tokenService.getTwoFactorToken().resolves(null); - - // Act - await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); - const result = await ssoLogInDelegate.logIn(); - - // Assert - // Api call: - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const ssoTokenRequest = actual as any; - return ( - ssoTokenRequest.code === ssoCode && - ssoTokenRequest.codeVerifier === ssoCodeVerifier && - ssoTokenRequest.redirectUri === ssoRedirectUrl && - ssoTokenRequest.device.identifier === deviceId && - ssoTokenRequest.twoFactor.provider == null && - ssoTokenRequest.twoFactor.token == null - ); - }) - ); - - // Sets local environment: - commonSuccessAssertions(); - cryptoService.received(1).setEncPrivateKey(privateKey); - cryptoService.received(1).setEncKey(encKey); - - // Negative tests - cryptoService.didNotReceive().setKey(preloginKey); // Not set by SSO - cryptoService.didNotReceive().setKeyHash(localHashedPassword); // Not set by SSO - apiService.didNotReceive().postAccountKeys(Arg.any()); // Did not generate new private key pair - keyConnectorService.didNotReceive().getAndSetKey(Arg.any()); // Did not fetch Key Connector key - keyConnectorService.didNotReceive().convertNewSsoUserToKeyConnector(Arg.all()); // Did not send key to Key Connector - tokenService.didNotReceive().setTwoFactorToken(Arg.any()); // Did not save 2FA token - - // Return result: - const expected = buildAuthResponse(); - expect(result).toEqual(expected); - }); - - it("does not set keys for new SSO user flow", async () => { - const tokenResponse = newTokenResponse(); - tokenResponse.key = null; - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); - await ssoLogInDelegate.logIn(); - - cryptoService.didNotReceive().setEncPrivateKey(privateKey); - cryptoService.didNotReceive().setEncKey(encKey); - }); - - it("gets and sets KeyConnector key for enrolled user", async () => { - const tokenResponse = newTokenResponse(); - tokenResponse.keyConnectorUrl = keyConnectorUrl; - - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); - await ssoLogInDelegate.logIn(); - - commonSuccessAssertions(); - keyConnectorService.received(1).getAndSetKey(keyConnectorUrl); - }); - - it("converts new SSO user to Key Connector on first login", async () => { - const tokenResponse = newTokenResponse(); - tokenResponse.keyConnectorUrl = keyConnectorUrl; - tokenResponse.key = null; - - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); - await ssoLogInDelegate.logIn(); - - commonSuccessAssertions(); - keyConnectorService - .received(1) - .convertNewSsoUserToKeyConnector(kdf, kdfIterations, keyConnectorUrl, ssoOrgId); - }); - }); - - describe("Api Key authentication", () => { - it("works in simple cases", async () => { - apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); - tokenService.getTwoFactorToken().resolves(null); - - await apiLogInDelegate.init(apiClientId, apiClientSecret); - const result = await apiLogInDelegate.logIn(); - - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const apiTokenRequest = actual as any; - return ( - apiTokenRequest.clientId === apiClientId && - apiTokenRequest.clientSecret === apiClientSecret && - apiTokenRequest.device.identifier === deviceId && - apiTokenRequest.twoFactor.provider == null && - apiTokenRequest.twoFactor.token == null && - apiTokenRequest.captchaResponse == null - ); - }) - ); - - // Sets local environment: - stateService.received(1).setApiKeyClientId(apiClientId); - stateService.received(1).setApiKeyClientSecret(apiClientSecret); - commonSuccessAssertions(); - - cryptoService.received(1).setEncKey(encKey); - cryptoService.received(1).setEncPrivateKey(privateKey); - - // Negative tests - apiService.didNotReceive().postAccountKeys(Arg.any()); // Did not generate new private key pair - keyConnectorService.didNotReceive().getAndSetKey(Arg.any()); // Did not fetch Key Connector key - keyConnectorService.didNotReceive().convertNewSsoUserToKeyConnector(Arg.all()); // Did not send key to Key Connector - tokenService.didNotReceive().setTwoFactorToken(Arg.any()); // Did not save 2FA token - - // Return result: - const expected = buildAuthResponse(); - expect(result).toEqual(expected); - }); - }); - - describe("Two-factor authentication", () => { - beforeEach(() => { - passwordLogInSetup(); - }); - - it("rejects login if 2FA is required", async () => { - const twoFactorProviders = new Map([[1, null]]); - const tokenResponse = newTokenResponse(); - (tokenResponse as any).twoFactorProviders2 = twoFactorProviders; - - apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); - - await passwordLogInDelegate.init(email, masterPassword); - const result = await passwordLogInDelegate.logIn(); - - stateService.didNotReceive().addAccount(Arg.any()); - messagingService.didNotReceive().send(Arg.any()); - - const expected = new AuthResult(); - expected.twoFactorProviders = twoFactorProviders; - expected.captchaSiteKey = undefined; - expect(result).toEqual(expected); - }); - - it("uses stored 2FA token", async () => { - tokenService.getTwoFactorToken().resolves(twoFactorToken); - - await passwordLogInDelegate.init(email, masterPassword); - await passwordLogInDelegate.logIn(); - - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const passwordTokenRequest = actual as any; - return ( - passwordTokenRequest.email === email && - passwordTokenRequest.masterPasswordHash === hashedPassword && - passwordTokenRequest.device.identifier === deviceId && - passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember && - passwordTokenRequest.twoFactor.token === twoFactorToken && - passwordTokenRequest.twoFactor.remember === false && - passwordTokenRequest.captchaResponse == null - ); - }) - ); - }); - - it("uses 2FA token entered by user at the same time as the Master Password", async () => { - passwordLogInSetup(); - - await passwordLogInDelegate.init(email, masterPassword, null, { provider: twoFactorProviderType, token: twoFactorToken, remember: twoFactorRemember, }); - await passwordLogInDelegate.logIn(); - - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const passwordTokenRequest = actual as any; - return ( - passwordTokenRequest.email === email && - passwordTokenRequest.masterPasswordHash === hashedPassword && - passwordTokenRequest.device.identifier === deviceId && - passwordTokenRequest.twoFactor.provider === twoFactorProviderType && - passwordTokenRequest.twoFactor.token === twoFactorToken && - passwordTokenRequest.twoFactor.remember === twoFactorRemember && - passwordTokenRequest.captchaResponse == null - ); - }) - ); - }); - - it("logInTwoFactor: uses 2FA token entered by user from the 2FA page", async () => { - await passwordLogInDelegate.init(email, masterPassword); - await passwordLogInDelegate.logInTwoFactor({ - provider: twoFactorProviderType, - token: twoFactorToken, - remember: twoFactorRemember, - }); - - apiService.received(1).postIdentityToken( - Arg.is((actual) => { - const passwordTokenRequest = actual as any; - return ( - passwordTokenRequest.email === email && - passwordTokenRequest.masterPasswordHash === hashedPassword && - passwordTokenRequest.device.identifier === deviceId && - passwordTokenRequest.twoFactor.provider === twoFactorProviderType && - passwordTokenRequest.twoFactor.token === twoFactorToken && - passwordTokenRequest.twoFactor.remember === twoFactorRemember && - passwordTokenRequest.captchaResponse == null - ); - }) - ); - }); - }); - - // Helper functions - - function passwordLogInSetup() { - authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey); - cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword); - cryptoService - .hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization) - .resolves(localHashedPassword); - } - - function commonSuccessAssertions() { - stateService.received(1).addAccount( - new Account({ - profile: { - ...new AccountProfile(), - ...{ - userId: userId, - email: email, - hasPremiumPersonally: false, - kdfIterations: kdfIterations, - kdfType: kdf, - }, - }, - tokens: { - ...new AccountTokens(), - ...{ - accessToken: accessToken, - refreshToken: refreshToken, - }, - }, - }) - ); - stateService.received(1).setBiometricLocked(false); - messagingService.received(1).send("loggedIn"); - } - - function newTokenResponse() { - const tokenResponse = new IdentityTokenResponse({}); - (tokenResponse as any).twoFactorProviders2 = null; - (tokenResponse as any).siteKey = undefined; - tokenResponse.resetMasterPassword = false; - tokenResponse.forcePasswordReset = false; - tokenResponse.accessToken = accessToken; - tokenResponse.refreshToken = refreshToken; - tokenResponse.kdf = kdf; - tokenResponse.kdfIterations = kdfIterations; - tokenResponse.key = encKey; - tokenResponse.privateKey = privateKey; - return tokenResponse; - } - - function buildAuthResponse() { - const expected = new AuthResult(); - expected.forcePasswordReset = false; - expected.resetMasterPassword = false; - expected.twoFactorProviders = null; - expected.captchaSiteKey = undefined; - return expected; - } -}); diff --git a/spec/common/services/logInDelegate/apiLogIn.delegate.spec.ts b/spec/common/services/logInDelegate/apiLogIn.delegate.spec.ts new file mode 100644 index 00000000..1eb7aebb --- /dev/null +++ b/spec/common/services/logInDelegate/apiLogIn.delegate.spec.ts @@ -0,0 +1,156 @@ +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; + +import { ApiLogInDelegate } from "jslib-common/services/logInDelegate/apiLogin.delegate"; + +import { Utils } from "jslib-common/misc/utils"; + +import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; + +import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; + +describe("ApiLogInDelegate", () => { + let cryptoService: SubstituteOf; + let apiService: SubstituteOf; + let tokenService: SubstituteOf; + let appIdService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let messagingService: SubstituteOf; + let logService: SubstituteOf; + let environmentService: SubstituteOf; + let keyConnectorService: SubstituteOf; + let stateService: SubstituteOf; + let twoFactorService: SubstituteOf; + let authService: SubstituteOf; + const setCryptoKeys = true; + + let apiLogInDelegate: ApiLogInDelegate; + + const email = "hello@world.com"; + + const deviceId = Utils.newGuid(); + const accessToken = "ACCESS_TOKEN"; + const refreshToken = "REFRESH_TOKEN"; + const encKey = "ENC_KEY"; + const privateKey = "PRIVATE_KEY"; + const keyConnectorUrl = "KEY_CONNECTOR_URL"; + const kdf = 0; + const kdfIterations = 10000; + const userId = Utils.newGuid(); + + const decodedToken = { + sub: userId, + email: email, + premium: false, + }; + + const apiClientId = "API_CLIENT_ID"; + const apiClientSecret = "API_CLIENT_SECRET"; + + beforeEach(() => { + cryptoService = Substitute.for(); + apiService = Substitute.for(); + tokenService = Substitute.for(); + appIdService = Substitute.for(); + platformUtilsService = Substitute.for(); + messagingService = Substitute.for(); + logService = Substitute.for(); + environmentService = Substitute.for(); + stateService = Substitute.for(); + keyConnectorService = Substitute.for(); + twoFactorService = Substitute.for(); + authService = Substitute.for(); + + apiLogInDelegate = new ApiLogInDelegate( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + setCryptoKeys, + twoFactorService, + environmentService, + keyConnectorService + ); + + appIdService.getAppId().resolves(deviceId); + }); + + it("sends api key credentials to the server", async () => { + tokenService.getTwoFactorToken().resolves(null); + + await apiLogInDelegate.init(apiClientId, apiClientSecret); + await apiLogInDelegate.logIn(); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const apiTokenRequest = actual as any; + return ( + apiTokenRequest.clientId === apiClientId && + apiTokenRequest.clientSecret === apiClientSecret && + apiTokenRequest.device.identifier === deviceId && + apiTokenRequest.twoFactor.provider == null && + apiTokenRequest.twoFactor.token == null && + apiTokenRequest.captchaResponse == null + ); + }) + ); + }); + + it("sets the local environment after a successful login", async () => { + apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); + tokenService.getTwoFactorToken().resolves(null); + + await apiLogInDelegate.init(apiClientId, apiClientSecret); + await apiLogInDelegate.logIn(); + + stateService.received(1).setApiKeyClientId(apiClientId); + stateService.received(1).setApiKeyClientSecret(apiClientSecret); + stateService.received(1).addAccount(Arg.any()); + }); + + it("gets and sets the Key Connector key if required", async () => { + const tokenResponse = newTokenResponse(); + tokenResponse.apiUseKeyConnector = true; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + tokenService.getTwoFactorToken().resolves(null); + environmentService.getKeyConnectorUrl().returns(keyConnectorUrl); + + await apiLogInDelegate.init(apiClientId, apiClientSecret); + await apiLogInDelegate.logIn(); + + keyConnectorService.received(1).getAndSetKey(keyConnectorUrl); + }); + + // Helper functions + + function newTokenResponse() { + const tokenResponse = new IdentityTokenResponse({}); + (tokenResponse as any).twoFactorProviders2 = null; + (tokenResponse as any).siteKey = undefined; + tokenResponse.resetMasterPassword = false; + tokenResponse.forcePasswordReset = false; + tokenResponse.accessToken = accessToken; + tokenResponse.refreshToken = refreshToken; + tokenResponse.kdf = kdf; + tokenResponse.kdfIterations = kdfIterations; + tokenResponse.key = encKey; + tokenResponse.privateKey = privateKey; + return tokenResponse; + } +}); diff --git a/spec/common/services/logInDelegate/logIn.delegate.spec.ts b/spec/common/services/logInDelegate/logIn.delegate.spec.ts new file mode 100644 index 00000000..1532b18a --- /dev/null +++ b/spec/common/services/logInDelegate/logIn.delegate.spec.ts @@ -0,0 +1,342 @@ +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; + +import { PasswordLogInDelegate } from "jslib-common/services/logInDelegate/passwordLogin.delegate"; +import { ApiLogInDelegate } from "jslib-common/services/logInDelegate/apiLogin.delegate"; +import { SsoLogInDelegate } from "jslib-common/services/logInDelegate/ssoLogin.delegate"; + +import { Utils } from "jslib-common/misc/utils"; + +import { Account, AccountProfile, AccountTokens } from "jslib-common/models/domain/account"; +import { AuthResult } from "jslib-common/models/domain/authResult"; +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; + +import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; + +import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; +import { HashPurpose } from "jslib-common/enums/hashPurpose"; +import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; +import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse"; +import { IdentityCaptchaResponse } from "jslib-common/models/response/identityCaptchaResponse"; + +describe("LogInDelegates", () => { + let cryptoService: SubstituteOf; + let apiService: SubstituteOf; + let tokenService: SubstituteOf; + let appIdService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let messagingService: SubstituteOf; + let logService: SubstituteOf; + let stateService: SubstituteOf; + let twoFactorService: SubstituteOf; + let authService: SubstituteOf; + const setCryptoKeys = true; + + let passwordLogInDelegate: PasswordLogInDelegate; + + const email = "hello@world.com"; + const masterPassword = "password"; + const hashedPassword = "HASHED_PASSWORD"; + const localHashedPassword = "LOCAL_HASHED_PASSWORD"; + const preloginKey = new SymmetricCryptoKey( + Utils.fromB64ToArray( + "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==" + ) + ); + const deviceId = Utils.newGuid(); + const accessToken = "ACCESS_TOKEN"; + const refreshToken = "REFRESH_TOKEN"; + const encKey = "ENC_KEY"; + const privateKey = "PRIVATE_KEY"; + const keyConnectorUrl = "KEY_CONNECTOR_URL"; + const kdf = 0; + const kdfIterations = 10000; + const userId = Utils.newGuid(); + + const decodedToken = { + sub: userId, + email: email, + premium: false, + }; + + const twoFactorProviderType = TwoFactorProviderType.Authenticator; + const twoFactorToken = "TWO_FACTOR_TOKEN"; + const twoFactorRemember = true; + + const captchaSiteKey = "CAPTCHA_SITE_KEY"; + + const twoFactorProviders = new Map([[1, null]]); + + beforeEach(() => { + cryptoService = Substitute.for(); + apiService = Substitute.for(); + tokenService = Substitute.for(); + appIdService = Substitute.for(); + platformUtilsService = Substitute.for(); + messagingService = Substitute.for(); + logService = Substitute.for(); + stateService = Substitute.for(); + twoFactorService = Substitute.for(); + authService = Substitute.for(); + + passwordLogInDelegate = new PasswordLogInDelegate( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + setCryptoKeys, + twoFactorService, + authService + ); + + appIdService.getAppId().resolves(deviceId); + }); + + it("sets the local environment after a successful login", async () => { + apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); + tokenService.getTwoFactorToken().resolves(null); + tokenService.decodeToken(accessToken).resolves(decodedToken); + + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logIn(); + + stateService.received(1).addAccount( + new Account({ + profile: { + ...new AccountProfile(), + ...{ + userId: userId, + email: email, + hasPremiumPersonally: false, + kdfIterations: kdfIterations, + kdfType: kdf, + }, + }, + tokens: { + ...new AccountTokens(), + ...{ + accessToken: accessToken, + refreshToken: refreshToken, + }, + }, + }) + ); + cryptoService.received(1).setEncKey(encKey); + cryptoService.received(1).setEncPrivateKey(privateKey); + + stateService.received(1).setBiometricLocked(false); + messagingService.received(1).send("loggedIn"); + }); + + it("builds AuthResult", async () => { + // Arrange + const tokenResponse = newTokenResponse(); + tokenResponse.forcePasswordReset = true; + tokenResponse.resetMasterPassword = true; + (tokenResponse as any as IdentityTwoFactorResponse).twoFactorProviders2 = null; + (tokenResponse as any as IdentityCaptchaResponse).siteKey = null; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + tokenService.getTwoFactorToken().resolves(null); + + // Act + await passwordLogInDelegate.init(email, masterPassword); + const result = await passwordLogInDelegate.logIn(); + + // Assert + const expected = new AuthResult(); + expected.forcePasswordReset = true; + expected.resetMasterPassword = true; + expected.twoFactorProviders = null; + expected.captchaSiteKey = null; + expect(result).toEqual(expected); + }); + + it("rejects login if CAPTCHA is required", async () => { + const tokenResponse = newTokenResponse(); + (tokenResponse as any).siteKey = captchaSiteKey; + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await passwordLogInDelegate.init(email, masterPassword); + const result = await passwordLogInDelegate.logIn(); + + stateService.didNotReceive().addAccount(Arg.any()); + messagingService.didNotReceive().send(Arg.any()); + + const expected = new AuthResult(); + expected.captchaSiteKey = captchaSiteKey; + expect(result).toEqual(expected); + }); + + it("does not set crypto keys if setCryptoKeys is false", async () => { + apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); + + passwordLogInDelegate = new PasswordLogInDelegate( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + false, + twoFactorService, + authService + ); + + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logIn(); + + cryptoService.didNotReceive().setKey(Arg.any()); + cryptoService.didNotReceive().setKeyHash(Arg.any()); + cryptoService.didNotReceive().setEncKey(Arg.any()); + cryptoService.didNotReceive().setEncPrivateKey(Arg.any()); + }); + + it("makes a new public and private key for an old account", async () => { + const tokenResponse = newTokenResponse(); + tokenResponse.privateKey = null; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logIn(); + + apiService.received(1).postAccountKeys(Arg.any()); + }); + + describe("Two-factor authentication", () => { + beforeEach(() => { + passwordLogInSetup(); + }); + + it("rejects login if 2FA is required", async () => { + const tokenResponse = newTokenResponse(); + (tokenResponse as any).twoFactorProviders2 = twoFactorProviders; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await passwordLogInDelegate.init(email, masterPassword); + const result = await passwordLogInDelegate.logIn(); + + stateService.didNotReceive().addAccount(Arg.any()); + messagingService.didNotReceive().send(Arg.any()); + + const expected = new AuthResult(); + expected.twoFactorProviders = twoFactorProviders; + expected.captchaSiteKey = undefined; + expect(result).toEqual(expected); + }); + + it("sends stored 2FA token to server", async () => { + tokenService.getTwoFactorToken().resolves(twoFactorToken); + + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logIn(); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const passwordTokenRequest = actual as any; + return ( + passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + passwordTokenRequest.device.identifier === deviceId && + passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember && + passwordTokenRequest.twoFactor.token === twoFactorToken && + passwordTokenRequest.twoFactor.remember === false && + passwordTokenRequest.captchaResponse == null + ); + }) + ); + }); + + it("sends 2FA token provided by user to server (single step)", async () => { + await passwordLogInDelegate.init(email, masterPassword, null, { + provider: twoFactorProviderType, + token: twoFactorToken, + remember: twoFactorRemember, + }); + await passwordLogInDelegate.logIn(); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const passwordTokenRequest = actual as any; + return ( + passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + passwordTokenRequest.device.identifier === deviceId && + passwordTokenRequest.twoFactor.provider === twoFactorProviderType && + passwordTokenRequest.twoFactor.token === twoFactorToken && + passwordTokenRequest.twoFactor.remember === twoFactorRemember && + passwordTokenRequest.captchaResponse == null + ); + }) + ); + }); + + it("sends 2FA token provided by user to server (two-step)", async () => { + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logInTwoFactor({ + provider: twoFactorProviderType, + token: twoFactorToken, + remember: twoFactorRemember, + }); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const passwordTokenRequest = actual as any; + return ( + passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + passwordTokenRequest.device.identifier === deviceId && + passwordTokenRequest.twoFactor.provider === twoFactorProviderType && + passwordTokenRequest.twoFactor.token === twoFactorToken && + passwordTokenRequest.twoFactor.remember === twoFactorRemember && + passwordTokenRequest.captchaResponse == null + ); + }) + ); + }); + }); + + // Helper functions + + function passwordLogInSetup() { + authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey); + cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword); + cryptoService + .hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization) + .resolves(localHashedPassword); + } + + function newTokenResponse() { + const tokenResponse = new IdentityTokenResponse({}); + (tokenResponse as any).twoFactorProviders2 = null; + (tokenResponse as any).siteKey = undefined; + tokenResponse.resetMasterPassword = false; + tokenResponse.forcePasswordReset = false; + tokenResponse.accessToken = accessToken; + tokenResponse.refreshToken = refreshToken; + tokenResponse.kdf = kdf; + tokenResponse.kdfIterations = kdfIterations; + tokenResponse.key = encKey; + tokenResponse.privateKey = privateKey; + return tokenResponse; + } +}); diff --git a/spec/common/services/logInDelegate/passwordLogIn.delegate.spec.ts b/spec/common/services/logInDelegate/passwordLogIn.delegate.spec.ts new file mode 100644 index 00000000..62f31043 --- /dev/null +++ b/spec/common/services/logInDelegate/passwordLogIn.delegate.spec.ts @@ -0,0 +1,146 @@ +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; + +import { PasswordLogInDelegate } from "jslib-common/services/logInDelegate/passwordLogin.delegate"; + +import { Utils } from "jslib-common/misc/utils"; + +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; + +import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; + +import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; +import { HashPurpose } from "jslib-common/enums/hashPurpose"; + +describe("PasswordLogInDelegate", () => { + let cryptoService: SubstituteOf; + let apiService: SubstituteOf; + let tokenService: SubstituteOf; + let appIdService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let messagingService: SubstituteOf; + let logService: SubstituteOf; + let environmentService: SubstituteOf; + let keyConnectorService: SubstituteOf; + let stateService: SubstituteOf; + let twoFactorService: SubstituteOf; + let authService: SubstituteOf; + const setCryptoKeys = true; + + let passwordLogInDelegate: PasswordLogInDelegate; + + const email = "hello@world.com"; + const masterPassword = "password"; + const hashedPassword = "HASHED_PASSWORD"; + const localHashedPassword = "LOCAL_HASHED_PASSWORD"; + const preloginKey = new SymmetricCryptoKey( + Utils.fromB64ToArray( + "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg==" + ) + ); + const deviceId = Utils.newGuid(); + const accessToken = "ACCESS_TOKEN"; + const refreshToken = "REFRESH_TOKEN"; + const encKey = "ENC_KEY"; + const privateKey = "PRIVATE_KEY"; + const kdf = 0; + const kdfIterations = 10000; + const userId = Utils.newGuid(); + + beforeEach(() => { + cryptoService = Substitute.for(); + apiService = Substitute.for(); + tokenService = Substitute.for(); + appIdService = Substitute.for(); + platformUtilsService = Substitute.for(); + messagingService = Substitute.for(); + logService = Substitute.for(); + environmentService = Substitute.for(); + stateService = Substitute.for(); + keyConnectorService = Substitute.for(); + twoFactorService = Substitute.for(); + authService = Substitute.for(); + + passwordLogInDelegate = new PasswordLogInDelegate( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + setCryptoKeys, + twoFactorService, + authService + ); + + appIdService.getAppId().resolves(deviceId); + }); + + beforeEach(() => { + authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey); + cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword); + cryptoService + .hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization) + .resolves(localHashedPassword); + }); + + it("sends master password credentials to the server", async () => { + tokenService.getTwoFactorToken().resolves(null); + + await passwordLogInDelegate.init(email, masterPassword); + const result = await passwordLogInDelegate.logIn(); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const passwordTokenRequest = actual as any; // Need to access private fields + return ( + passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + passwordTokenRequest.device.identifier === deviceId && + passwordTokenRequest.twoFactor.provider == null && + passwordTokenRequest.twoFactor.token == null && + passwordTokenRequest.captchaResponse == null + ); + }) + ); + }); + + it("sets the local environment after a successful login", async () => { + apiService.postIdentityToken(Arg.any()).resolves(newTokenResponse()); + tokenService.getTwoFactorToken().resolves(null); + + await passwordLogInDelegate.init(email, masterPassword); + await passwordLogInDelegate.logIn(); + + cryptoService.received(1).setKey(preloginKey); + cryptoService.received(1).setKeyHash(localHashedPassword); + }); + + function newTokenResponse() { + const tokenResponse = new IdentityTokenResponse({}); + (tokenResponse as any).twoFactorProviders2 = null; + (tokenResponse as any).siteKey = undefined; + tokenResponse.resetMasterPassword = false; + tokenResponse.forcePasswordReset = false; + tokenResponse.accessToken = accessToken; + tokenResponse.refreshToken = refreshToken; + tokenResponse.kdf = kdf; + tokenResponse.kdfIterations = kdfIterations; + tokenResponse.key = encKey; + tokenResponse.privateKey = privateKey; + return tokenResponse; + } +}); diff --git a/spec/common/services/logInDelegate/ssoLogIn.delegate.spec.ts b/spec/common/services/logInDelegate/ssoLogIn.delegate.spec.ts new file mode 100644 index 00000000..3a25f667 --- /dev/null +++ b/spec/common/services/logInDelegate/ssoLogIn.delegate.spec.ts @@ -0,0 +1,167 @@ +import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; + +import { ApiService } from "jslib-common/abstractions/api.service"; +import { AppIdService } from "jslib-common/abstractions/appId.service"; +import { AuthService } from "jslib-common/abstractions/auth.service"; +import { CryptoService } from "jslib-common/abstractions/crypto.service"; +import { EnvironmentService } from "jslib-common/abstractions/environment.service"; +import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; +import { LogService } from "jslib-common/abstractions/log.service"; +import { MessagingService } from "jslib-common/abstractions/messaging.service"; +import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { TokenService } from "jslib-common/abstractions/token.service"; + +import { PasswordLogInDelegate } from "jslib-common/services/logInDelegate/passwordLogin.delegate"; +import { ApiLogInDelegate } from "jslib-common/services/logInDelegate/apiLogin.delegate"; +import { SsoLogInDelegate } from "jslib-common/services/logInDelegate/ssoLogin.delegate"; + +import { Utils } from "jslib-common/misc/utils"; + +import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey"; + +import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse"; + +import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; +import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; + +describe("SsoLogInDelegate", () => { + let cryptoService: SubstituteOf; + let apiService: SubstituteOf; + let tokenService: SubstituteOf; + let appIdService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let messagingService: SubstituteOf; + let logService: SubstituteOf; + let environmentService: SubstituteOf; + let keyConnectorService: SubstituteOf; + let stateService: SubstituteOf; + let twoFactorService: SubstituteOf; + let authService: SubstituteOf; + const setCryptoKeys = true; + + let ssoLogInDelegate: SsoLogInDelegate; + + const email = "hello@world.com"; + const deviceId = Utils.newGuid(); + const accessToken = "ACCESS_TOKEN"; + const refreshToken = "REFRESH_TOKEN"; + const encKey = "ENC_KEY"; + const privateKey = "PRIVATE_KEY"; + const keyConnectorUrl = "KEY_CONNECTOR_URL"; + const kdf = 0; + const kdfIterations = 10000; + const userId = Utils.newGuid(); + + const ssoCode = "SSO_CODE"; + const ssoCodeVerifier = "SSO_CODE_VERIFIER"; + const ssoRedirectUrl = "SSO_REDIRECT_URL"; + const ssoOrgId = "SSO_ORG_ID"; + + beforeEach(() => { + cryptoService = Substitute.for(); + apiService = Substitute.for(); + tokenService = Substitute.for(); + appIdService = Substitute.for(); + platformUtilsService = Substitute.for(); + messagingService = Substitute.for(); + logService = Substitute.for(); + environmentService = Substitute.for(); + stateService = Substitute.for(); + keyConnectorService = Substitute.for(); + twoFactorService = Substitute.for(); + authService = Substitute.for(); + + ssoLogInDelegate = new SsoLogInDelegate( + cryptoService, + apiService, + tokenService, + appIdService, + platformUtilsService, + messagingService, + logService, + stateService, + setCryptoKeys, + twoFactorService, + keyConnectorService + ); + + appIdService.getAppId().resolves(deviceId); + }); + + it("sends SSO information to server", async () => { + tokenService.getTwoFactorToken().resolves(null); + + await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); + await ssoLogInDelegate.logIn(); + + apiService.received(1).postIdentityToken( + Arg.is((actual) => { + const ssoTokenRequest = actual as any; + return ( + ssoTokenRequest.code === ssoCode && + ssoTokenRequest.codeVerifier === ssoCodeVerifier && + ssoTokenRequest.redirectUri === ssoRedirectUrl && + ssoTokenRequest.device.identifier === deviceId && + ssoTokenRequest.twoFactor.provider == null && + ssoTokenRequest.twoFactor.token == null + ); + }) + ); + }); + + it("does not set keys for new SSO user flow", async () => { + const tokenResponse = newTokenResponse(); + tokenResponse.key = null; + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); + await ssoLogInDelegate.logIn(); + + cryptoService.didNotReceive().setEncPrivateKey(privateKey); + cryptoService.didNotReceive().setEncKey(encKey); + }); + + it("gets and sets KeyConnector key for enrolled user", async () => { + const tokenResponse = newTokenResponse(); + tokenResponse.keyConnectorUrl = keyConnectorUrl; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); + await ssoLogInDelegate.logIn(); + + keyConnectorService.received(1).getAndSetKey(keyConnectorUrl); + }); + + it("converts new SSO user to Key Connector on first login", async () => { + const tokenResponse = newTokenResponse(); + tokenResponse.keyConnectorUrl = keyConnectorUrl; + tokenResponse.key = null; + + apiService.postIdentityToken(Arg.any()).resolves(tokenResponse); + + await ssoLogInDelegate.init(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); + await ssoLogInDelegate.logIn(); + + keyConnectorService + .received(1) + .convertNewSsoUserToKeyConnector(kdf, kdfIterations, keyConnectorUrl, ssoOrgId); + }); + + // Helper functions + function newTokenResponse() { + const tokenResponse = new IdentityTokenResponse({}); + (tokenResponse as any).twoFactorProviders2 = null; + (tokenResponse as any).siteKey = undefined; + tokenResponse.resetMasterPassword = false; + tokenResponse.forcePasswordReset = false; + tokenResponse.accessToken = accessToken; + tokenResponse.refreshToken = refreshToken; + tokenResponse.kdf = kdf; + tokenResponse.kdfIterations = kdfIterations; + tokenResponse.key = encKey; + tokenResponse.privateKey = privateKey; + return tokenResponse; + } +});