mirror of
https://github.com/bitwarden/jslib
synced 2026-01-04 17:43:14 +00:00
Refactor tests to test each delegate
This commit is contained in:
@@ -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<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
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<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
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<number, null>([[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;
|
||||
}
|
||||
});
|
||||
156
spec/common/services/logInDelegate/apiLogIn.delegate.spec.ts
Normal file
156
spec/common/services/logInDelegate/apiLogIn.delegate.spec.ts
Normal file
@@ -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<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
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<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
342
spec/common/services/logInDelegate/logIn.delegate.spec.ts
Normal file
342
spec/common/services/logInDelegate/logIn.delegate.spec.ts
Normal file
@@ -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<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
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<number, null>([[1, null]]);
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
@@ -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<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
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<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
167
spec/common/services/logInDelegate/ssoLogIn.delegate.spec.ts
Normal file
167
spec/common/services/logInDelegate/ssoLogIn.delegate.spec.ts
Normal file
@@ -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<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
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<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user