1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

[AC-3043] Refactor AuthService to only use organization api key login (#622)

* Remove jslib authService and unused loginStrategies

* Delete KeyConnectorService

* Move OrganizationLoginStrategy into base LoginStrategy

* Remove unused code and services from loginStrategy

* Delete OrganizationService

* Move loginStrategy into authService
This commit is contained in:
Thomas Rittson
2024-09-23 08:46:38 +10:00
committed by GitHub
parent 9dc497dd13
commit 4652c6489f
33 changed files with 195 additions and 2026 deletions

View File

@@ -2,37 +2,29 @@ import { LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ApiService } from "@/jslib/common/src/services/api.service";
import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { AuthService } from "@/jslib/common/src/services/auth.service";
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
import {
SafeInjectionToken,
@@ -61,24 +53,6 @@ import { ValidationService } from "./validation.service";
useClass: AppIdService,
deps: [StorageServiceAbstraction],
}),
safeProvider({
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
}),
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
safeProvider({
provide: EnvironmentServiceAbstraction,
@@ -164,29 +138,6 @@ import { ValidationService } from "./validation.service";
),
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
safeProvider({
provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
LogService,
OrganizationServiceAbstraction,
CryptoFunctionServiceAbstraction,
],
}),
safeProvider({
provide: OrganizationServiceAbstraction,
useClass: OrganizationService,
deps: [StateServiceAbstraction],
}),
safeProvider({
provide: TwoFactorServiceAbstraction,
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
}),
] satisfies SafeProvider[],
})
export class JslibServicesModule {}

View File

@@ -1,114 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("ApiLogInStrategy", () => {
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 apiLogInStrategy: ApiLogInStrategy;
let credentials: ApiLogInCredentials;
const deviceId = Utils.newGuid();
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const apiClientId = "API_CLIENT_ID";
const apiClientSecret = "API_CLIENT_SECRET";
beforeEach(async () => {
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>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
apiLogInStrategy = new ApiLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
environmentService,
keyConnectorService,
);
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
});
it("sends api key credentials to the server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
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(identityTokenResponseFactory());
await apiLogInStrategy.logIn(credentials);
stateService.received(1).setApiKeyClientId(apiClientId);
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
stateService.received(1).addAccount(Arg.any());
});
it("gets and sets the Key Connector key from environmentUrl", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.apiUseKeyConnector = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
await apiLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
});
});

View File

@@ -1,288 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { Account, AccountProfile, AccountTokens } from "@/jslib/common/src/models/domain/account";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { EncString } from "@/jslib/common/src/models/domain/encString";
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { PasswordTokenRequest } from "@/jslib/common/src/models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { IdentityCaptchaResponse } from "@/jslib/common/src/models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
const email = "hello@world.com";
const masterPassword = "password";
const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN";
const encKey = "ENC_KEY";
const privateKey = "PRIVATE_KEY";
const captchaSiteKey = "CAPTCHA_SITE_KEY";
const kdf = 0;
const kdfIterations = 10000;
const userId = Utils.newGuid();
const masterPasswordHash = "MASTER_PASSWORD_HASH";
const decodedToken = {
sub: userId,
email: email,
premium: false,
};
const twoFactorProviderType = TwoFactorProviderType.Authenticator;
const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory() {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
KdfIterations: kdfIterations,
Key: encKey,
PrivateKey: privateKey,
ResetMasterPassword: false,
access_token: accessToken,
expires_in: 3600,
refresh_token: refreshToken,
scope: "api offline_access",
token_type: "Bearer",
});
}
describe("LogInStrategy", () => {
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>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
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>();
appIdService.getAppId().resolves(deviceId);
// The base class is abstract so we test it via PasswordLogInStrategy
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
authService,
);
credentials = new PasswordLogInCredentials(email, masterPassword);
});
describe("base class", () => {
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
tokenService.decodeToken(accessToken).resolves(decodedToken);
await passwordLogInStrategy.logIn(credentials);
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 () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
tokenResponse.resetMasterPassword = true;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
const expected = new AuthResult();
expected.forcePasswordReset = true;
expected.resetMasterPassword = true;
expected.twoFactorProviders = null;
expected.captchaSiteKey = "";
expect(result).toEqual(expected);
});
it("rejects login if CAPTCHA is required", async () => {
// Sample CAPTCHA response
const tokenResponse = new IdentityCaptchaResponse({
error: "invalid_grant",
error_description: "Captcha required.",
HCaptcha_SiteKey: captchaSiteKey,
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
const expected = new AuthResult();
expected.captchaSiteKey = captchaSiteKey;
expect(result).toEqual(expected);
});
it("makes a new public and private key for an old account", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.privateKey = null;
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postAccountKeys(Arg.any());
});
});
describe("Two-factor authentication", () => {
it("rejects login if 2FA is required", async () => {
// Sample response where TOTP 2FA required
const tokenResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
});
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
const result = await passwordLogInStrategy.logIn(credentials);
stateService.didNotReceive().addAccount(Arg.any());
messagingService.didNotReceive().send(Arg.any());
const expected = new AuthResult();
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
expected.twoFactorProviders.set(0, null);
expect(result).toEqual(expected);
});
it("sends stored 2FA token to server", async () => {
tokenService.getTwoFactorToken().resolves(twoFactorToken);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === false
);
}),
);
});
it("sends 2FA token provided by user to server (single step)", async () => {
// This occurs if the user enters the 2FA code as an argument in the CLI
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
credentials.twoFactor = new TokenRequestTwoFactor(
twoFactorProviderType,
twoFactorToken,
twoFactorRemember,
);
await passwordLogInStrategy.logIn(credentials);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
}),
);
});
it("sends 2FA token provided by user to server (two-step)", async () => {
// Simulate a partially completed login
passwordLogInStrategy.tokenRequest = new PasswordTokenRequest(
email,
masterPasswordHash,
null,
null,
);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await passwordLogInStrategy.logInTwoFactor(
new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
null,
);
apiService.received(1).postIdentityToken(
Arg.is((actual) => {
const passwordTokenRequest = actual as any;
return (
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
passwordTokenRequest.twoFactor.token === twoFactorToken &&
passwordTokenRequest.twoFactor.remember === twoFactorRemember
);
}),
);
});
});
});

View File

@@ -1,110 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { HashPurpose } from "@/jslib/common/src/enums/hashPurpose";
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
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();
describe("PasswordLogInStrategy", () => {
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>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
beforeEach(async () => {
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>();
appIdService.getAppId().resolves(deviceId);
tokenService.getTwoFactorToken().resolves(null);
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
cryptoService
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
.resolves(localHashedPassword);
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
authService,
);
credentials = new PasswordLogInCredentials(email, masterPassword);
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
});
it("sends master password credentials to the server", async () => {
await passwordLogInStrategy.logIn(credentials);
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 () => {
await passwordLogInStrategy.logIn(credentials);
cryptoService.received(1).setKey(preloginKey);
cryptoService.received(1).setKeyHash(localHashedPassword);
});
});

View File

@@ -1,127 +0,0 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { SsoLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/ssoLogin.strategy";
import { Utils } from "@/jslib/common/src/misc/utils";
import { SsoLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
describe("SsoLogInStrategy", () => {
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 keyConnectorService: SubstituteOf<KeyConnectorService>;
let stateService: SubstituteOf<StateService>;
let twoFactorService: SubstituteOf<TwoFactorService>;
let ssoLogInStrategy: SsoLogInStrategy;
let credentials: SsoLogInCredentials;
const deviceId = Utils.newGuid();
const encKey = "ENC_KEY";
const privateKey = "PRIVATE_KEY";
const keyConnectorUrl = "KEY_CONNECTOR_URL";
const ssoCode = "SSO_CODE";
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
const ssoRedirectUrl = "SSO_REDIRECT_URL";
const ssoOrgId = "SSO_ORG_ID";
beforeEach(async () => {
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>();
keyConnectorService = Substitute.for<KeyConnectorService>();
twoFactorService = Substitute.for<TwoFactorService>();
tokenService.getTwoFactorToken().resolves(null);
appIdService.getAppId().resolves(deviceId);
ssoLogInStrategy = new SsoLogInStrategy(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
keyConnectorService,
);
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
});
it("sends SSO information to server", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await ssoLogInStrategy.logIn(credentials);
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 = identityTokenResponseFactory();
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
cryptoService.didNotReceive().setEncKey(encKey);
});
it("gets and sets KeyConnector key for enrolled user", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
});
it("converts new SSO user to Key Connector on first login", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.keyConnectorUrl = keyConnectorUrl;
tokenResponse.key = null;
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
await ssoLogInStrategy.logIn(credentials);
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
});
});

View File

@@ -1,25 +0,0 @@
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
export abstract class AuthService {
masterPasswordHash: string;
email: string;
logIn: (
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
) => Promise<AuthResult>;
logInTwoFactor: (
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
) => Promise<AuthResult>;
logOut: (callback: () => void) => void;
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
authingWithApiKey: () => boolean;
authingWithSso: () => boolean;
authingWithPassword: () => boolean;
}

View File

@@ -1,19 +0,0 @@
import { Organization } from "../models/domain/organization";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
export abstract class KeyConnectorService {
getAndSetKey: (url?: string) => Promise<void>;
getManagingOrganization: () => Promise<Organization>;
getUsesKeyConnector: () => Promise<boolean>;
migrateUser: () => Promise<void>;
userNeedsMigration: () => Promise<boolean>;
convertNewSsoUserToKeyConnector: (
tokenResponse: IdentityTokenResponse,
orgId: string,
) => Promise<void>;
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
setConvertAccountRequired: (status: boolean) => Promise<void>;
getConvertAccountRequired: () => Promise<boolean>;
removeConvertAccountRequired: () => Promise<void>;
clear: () => Promise<void>;
}

View File

@@ -1,11 +0,0 @@
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export abstract class OrganizationService {
get: (id: string) => Promise<Organization>;
getByIdentifier: (identifier: string) => Promise<Organization>;
getAll: (userId?: string) => Promise<Organization[]>;
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
canManageSponsorships: () => Promise<boolean>;
hasOrganizations: (userId?: string) => Promise<boolean>;
}

View File

@@ -1,23 +0,0 @@
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
export interface TwoFactorProviderDetails {
type: TwoFactorProviderType;
name: string;
description: string;
priority: number;
sort: number;
premium: boolean;
}
export abstract class TwoFactorService {
init: () => void;
getSupportedProviders: (win: Window) => TwoFactorProviderDetails[];
getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
setSelectedProvider: (type: TwoFactorProviderType) => void;
clearSelectedProvider: () => void;
setProviders: (response: IdentityTwoFactorResponse) => void;
clearProviders: () => void;
getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>;
}

View File

@@ -1,5 +0,0 @@
export enum AuthenticationType {
Password = 0,
Sso = 1,
Api = 2,
}

View File

@@ -1,70 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { EnvironmentService } from "../../abstractions/environment.service";
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { TokenService } from "../../abstractions/token.service";
import { TwoFactorService } from "../../abstractions/twoFactor.service";
import { ApiLogInCredentials } from "../../models/domain/logInCredentials";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { LogInStrategy } from "./logIn.strategy";
export class ApiLogInStrategy extends LogInStrategy {
tokenRequest: ApiTokenRequest;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private environmentService: EnvironmentService,
private keyConnectorService: KeyConnectorService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
if (tokenResponse.apiUseKeyConnector) {
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
}
}
async logIn(credentials: ApiLogInCredentials) {
this.tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
await this.buildTwoFactor(),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
await super.saveAccountInformation(tokenResponse);
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret);
}
}

View File

@@ -1,170 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { TokenService } from "../../abstractions/token.service";
import { TwoFactorService } from "../../abstractions/twoFactor.service";
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
import { AuthResult } from "../../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../../models/domain/logInCredentials";
import { DeviceRequest } from "../../models/request/deviceRequest";
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { KeysRequest } from "../../models/request/keysRequest";
import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFactorResponse";
export abstract class LogInStrategy {
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
protected captchaBypassToken: string = null;
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,
protected appIdService: AppIdService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected logService: LogService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
) {}
abstract logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
): Promise<AuthResult>;
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string = null,
): Promise<AuthResult> {
this.tokenRequest.setTwoFactor(twoFactor);
return this.startLogIn();
}
protected async startLogIn(): Promise<AuthResult> {
this.twoFactorService.clearSelectedProvider();
const response = await this.apiService.postIdentityToken(this.tokenRequest);
if (response instanceof IdentityTwoFactorResponse) {
return this.processTwoFactorResponse(response);
} else if (response instanceof IdentityCaptchaResponse) {
return this.processCaptchaResponse(response);
} else if (response instanceof IdentityTokenResponse) {
return this.processTokenResponse(response);
}
throw new Error("Invalid response object.");
}
protected onSuccessfulLogin(response: IdentityTokenResponse): Promise<void> {
// Implemented in subclass if required
return null;
}
protected async buildDeviceRequest() {
const appId = await this.appIdService.getAppId();
return new DeviceRequest(appId, this.platformUtilsService);
}
protected async buildTwoFactor(userProvidedTwoFactor?: TokenRequestTwoFactor) {
if (userProvidedTwoFactor != null) {
return userProvidedTwoFactor;
}
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken();
if (storedTwoFactorToken != null) {
return new TokenRequestTwoFactor(TwoFactorProviderType.Remember, storedTwoFactorToken, false);
}
return new TokenRequestTwoFactor();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: accountInformation.sub,
email: accountInformation.email,
hasPremiumPersonally: accountInformation.premium,
kdfIterations: tokenResponse.kdfIterations,
kdfType: tokenResponse.kdf,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
}),
);
}
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
const result = new AuthResult();
result.resetMasterPassword = response.resetMasterPassword;
result.forcePasswordReset = response.forcePasswordReset;
await this.saveAccountInformation(response);
if (response.twoFactorToken != null) {
await this.tokenService.setTwoFactorToken(response);
}
const newSsoUser = response.key == null;
if (!newSsoUser) {
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(
response.privateKey ?? (await this.createKeyPairForOldAccount()),
);
}
await this.onSuccessfulLogin(response);
await this.stateService.setBiometricLocked(false);
this.messagingService.send("loggedIn");
return result;
}
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response);
this.captchaBypassToken = response.captchaToken ?? null;
return result;
}
private async processCaptchaResponse(response: IdentityCaptchaResponse): Promise<AuthResult> {
const result = new AuthResult();
result.captchaSiteKey = response.siteKey;
return result;
}
private async createKeyPairForOldAccount() {
try {
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
return privateKey.encryptedString;
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -1,95 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { AuthService } from "../../abstractions/auth.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { TokenService } from "../../abstractions/token.service";
import { TwoFactorService } from "../../abstractions/twoFactor.service";
import { HashPurpose } from "../../enums/hashPurpose";
import { AuthResult } from "../../models/domain/authResult";
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
import { LogInStrategy } from "./logIn.strategy";
export class PasswordLogInStrategy extends LogInStrategy {
get email() {
return this.tokenRequest.email;
}
get masterPasswordHash() {
return this.tokenRequest.masterPasswordHash;
}
tokenRequest: PasswordTokenRequest;
private localHashedPassword: string;
private key: SymmetricCryptoKey;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private authService: AuthService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin() {
await this.cryptoService.setKey(this.key);
await this.cryptoService.setKeyHash(this.localHashedPassword);
}
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
}
async logIn(credentials: PasswordLogInCredentials) {
const { email, masterPassword, captchaToken, twoFactor } = credentials;
this.key = await this.authService.makePreloginKey(masterPassword, email);
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
this.localHashedPassword = await this.cryptoService.hashPassword(
masterPassword,
this.key,
HashPurpose.LocalAuthorization,
);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
this.tokenRequest = new PasswordTokenRequest(
email,
hashedPassword,
captchaToken,
await this.buildTwoFactor(twoFactor),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
}

View File

@@ -1,70 +0,0 @@
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { KeyConnectorService } from "../../abstractions/keyConnector.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { TokenService } from "../../abstractions/token.service";
import { TwoFactorService } from "../../abstractions/twoFactor.service";
import { SsoLogInCredentials } from "../../models/domain/logInCredentials";
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
import { LogInStrategy } from "./logIn.strategy";
export class SsoLogInStrategy extends LogInStrategy {
tokenRequest: SsoTokenRequest;
orgId: string;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
twoFactorService: TwoFactorService,
private keyConnectorService: KeyConnectorService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
stateService,
twoFactorService,
);
}
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
const newSsoUser = tokenResponse.key == null;
if (tokenResponse.keyConnectorUrl != null) {
if (!newSsoUser) {
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
} else {
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
}
}
}
async logIn(credentials: SsoLogInCredentials) {
this.orgId = credentials.orgId;
this.tokenRequest = new SsoTokenRequest(
credentials.code,
credentials.codeVerifier,
credentials.redirectUrl,
await this.buildTwoFactor(credentials.twoFactor),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
}

View File

@@ -1,17 +0,0 @@
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
import { Utils } from "../../misc/utils";
export class AuthResult {
captchaSiteKey = "";
resetMasterPassword = false;
forcePasswordReset = false;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
get requiresCaptcha() {
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
}
get requiresTwoFactor() {
return this.twoFactorProviders != null;
}
}

View File

@@ -1,34 +0,0 @@
import { AuthenticationType } from "../../enums/authenticationType";
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
export class PasswordLogInCredentials {
readonly type = AuthenticationType.Password;
constructor(
public email: string,
public masterPassword: string,
public captchaToken?: string,
public twoFactor?: TokenRequestTwoFactor,
) {}
}
export class SsoLogInCredentials {
readonly type = AuthenticationType.Sso;
constructor(
public code: string,
public codeVerifier: string,
public redirectUrl: string,
public orgId: string,
public twoFactor?: TokenRequestTwoFactor,
) {}
}
export class ApiLogInCredentials {
readonly type = AuthenticationType.Api;
constructor(
public clientId: string,
public clientSecret: string,
) {}
}

View File

@@ -1,198 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { CryptoService } from "../abstractions/crypto.service";
import { EnvironmentService } from "../abstractions/environment.service";
import { I18nService } from "../abstractions/i18n.service";
import { KeyConnectorService } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/twoFactor.service";
import { AuthenticationType } from "../enums/authenticationType";
import { KdfType } from "../enums/kdfType";
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
import { AuthResult } from "../models/domain/authResult";
import {
ApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
} from "../models/domain/logInCredentials";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
import { PreloginRequest } from "../models/request/preloginRequest";
import { ErrorResponse } from "../models/response/errorResponse";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
export class AuthService implements AuthServiceAbstraction {
get email(): string {
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
}
get masterPasswordHash(): string {
return this.logInStrategy instanceof PasswordLogInStrategy
? this.logInStrategy.masterPasswordHash
: null;
}
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
private sessionTimeout: any;
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,
protected appIdService: AppIdService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected logService: LogService,
protected keyConnectorService: KeyConnectorService,
protected environmentService: EnvironmentService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
) {}
async logIn(
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
): Promise<AuthResult> {
this.clearState();
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
if (credentials.type === AuthenticationType.Password) {
strategy = new PasswordLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this,
);
} else if (credentials.type === AuthenticationType.Sso) {
strategy = new SsoLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService,
);
} else if (credentials.type === AuthenticationType.Api) {
strategy = new ApiLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService,
);
}
const result = await strategy.logIn(credentials as any);
if (result?.requiresTwoFactor) {
this.saveState(strategy);
}
return result;
}
async logInTwoFactor(
twoFactor: TokenRequestTwoFactor,
captchaResponse: string,
): Promise<AuthResult> {
if (this.logInStrategy == null) {
throw new Error(this.i18nService.t("sessionTimeout"));
}
try {
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
this.clearState();
}
return result;
} catch (e) {
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
if (!(e instanceof ErrorResponse)) {
this.clearState();
}
throw e;
}
}
logOut(callback: () => void) {
callback();
this.messagingService.send("loggedOut");
}
authingWithApiKey(): boolean {
return this.logInStrategy instanceof ApiLogInStrategy;
}
authingWithSso(): boolean {
return this.logInStrategy instanceof SsoLogInStrategy;
}
authingWithPassword(): boolean {
return this.logInStrategy instanceof PasswordLogInStrategy;
}
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
email = email.trim().toLowerCase();
let kdf: KdfType = null;
let kdfIterations: number = null;
try {
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
if (preloginResponse != null) {
kdf = preloginResponse.kdf;
kdfIterations = preloginResponse.kdfIterations;
}
} catch (e) {
if (e == null || e.statusCode !== 404) {
throw e;
}
}
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
}
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}
private clearState() {
this.logInStrategy = null;
this.clearSessionTimeout();
}
private startSessionTimeout() {
this.clearSessionTimeout();
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
}
private clearSessionTimeout() {
if (this.sessionTimeout != null) {
clearTimeout(this.sessionTimeout);
}
}
}

View File

@@ -1,134 +0,0 @@
import { ApiService } from "../abstractions/api.service";
import { CryptoService } from "../abstractions/crypto.service";
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service";
import { OrganizationUserType } from "../enums/organizationUserType";
import { Utils } from "../misc/utils";
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
import { KeysRequest } from "../models/request/keysRequest";
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService,
private cryptoFunctionService: CryptoFunctionService,
) {}
setUsesKeyConnector(usesKeyConnector: boolean) {
return this.stateService.setUsesKeyConnector(usesKeyConnector);
}
async getUsesKeyConnector(): Promise<boolean> {
return await this.stateService.getUsesKeyConnector();
}
async userNeedsMigration() {
const loggedInUsingSso = await this.tokenService.getIsExternal();
const requiredByOrganization = (await this.getManagingOrganization()) != null;
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
try {
await this.apiService.postUserKeyToKeyConnector(
organization.keyConnectorUrl,
keyConnectorRequest,
);
} catch (e) {
throw new Error("Unable to reach key connector");
}
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
} catch (e) {
this.logService.error(e);
throw new Error("Unable to reach key connector");
}
}
async getManagingOrganization() {
const orgs = await this.organizationService.getAll();
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser,
);
}
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
const { kdf, kdfIterations, keyConnectorUrl } = tokenResponse;
const password = await this.cryptoFunctionService.randomBytes(64);
const k = await this.cryptoService.makeKey(
Utils.fromBufferToB64(password),
await this.tokenService.getEmail(),
kdf,
kdfIterations,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
await this.cryptoService.setKey(k);
const encKey = await this.cryptoService.makeEncKey(k);
await this.cryptoService.setEncKey(encKey[1].encryptedString);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
try {
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
} catch (e) {
throw new Error("Unable to reach key connector");
}
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey[1].encryptedString,
kdf,
kdfIterations,
orgId,
keys,
);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
async setConvertAccountRequired(status: boolean) {
await this.stateService.setConvertAccountToKeyConnector(status);
}
async getConvertAccountRequired(): Promise<boolean> {
return await this.stateService.getConvertAccountToKeyConnector();
}
async removeConvertAccountRequired() {
await this.stateService.setConvertAccountToKeyConnector(null);
}
async clear() {
await this.removeConvertAccountRequired();
}
}

View File

@@ -1,55 +0,0 @@
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { OrganizationData } from "../models/data/organizationData";
import { Organization } from "../models/domain/organization";
export class OrganizationService implements OrganizationServiceAbstraction {
constructor(private stateService: StateService) {}
async get(id: string): Promise<Organization> {
const organizations = await this.stateService.getOrganizations();
// eslint-disable-next-line
if (organizations == null || !organizations.hasOwnProperty(id)) {
return null;
}
return new Organization(organizations[id]);
}
async getByIdentifier(identifier: string): Promise<Organization> {
const organizations = await this.getAll();
if (organizations == null || organizations.length === 0) {
return null;
}
return organizations.find((o) => o.identifier === identifier);
}
async getAll(userId?: string): Promise<Organization[]> {
const organizations = await this.stateService.getOrganizations({ userId: userId });
const response: Organization[] = [];
for (const id in organizations) {
// eslint-disable-next-line
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
response.push(new Organization(organizations[id]));
}
}
return response;
}
async save(organizations: { [id: string]: OrganizationData }) {
return await this.stateService.setOrganizations(organizations);
}
async canManageSponsorships(): Promise<boolean> {
const orgs = await this.getAll();
return orgs.some(
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null,
);
}
async hasOrganizations(userId?: string): Promise<boolean> {
const organizations = await this.getAll(userId);
return organizations.length > 0;
}
}

View File

@@ -1,186 +0,0 @@
import { I18nService } from "../abstractions/i18n.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import {
TwoFactorProviderDetails,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/twoFactor.service";
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
{
[TwoFactorProviderType.Authenticator]: {
type: TwoFactorProviderType.Authenticator,
name: null as string,
description: null as string,
priority: 1,
sort: 1,
premium: false,
},
[TwoFactorProviderType.Yubikey]: {
type: TwoFactorProviderType.Yubikey,
name: null as string,
description: null as string,
priority: 3,
sort: 2,
premium: true,
},
[TwoFactorProviderType.Duo]: {
type: TwoFactorProviderType.Duo,
name: "Duo",
description: null as string,
priority: 2,
sort: 3,
premium: true,
},
[TwoFactorProviderType.OrganizationDuo]: {
type: TwoFactorProviderType.OrganizationDuo,
name: "Duo (Organization)",
description: null as string,
priority: 10,
sort: 4,
premium: false,
},
[TwoFactorProviderType.Email]: {
type: TwoFactorProviderType.Email,
name: null as string,
description: null as string,
priority: 0,
sort: 6,
premium: false,
},
[TwoFactorProviderType.WebAuthn]: {
type: TwoFactorProviderType.WebAuthn,
name: null as string,
description: null as string,
priority: 4,
sort: 5,
premium: true,
},
};
export class TwoFactorService implements TwoFactorServiceAbstraction {
private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
private selectedTwoFactorProviderType: TwoFactorProviderType = null;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
) {}
init() {
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDesc");
TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
this.i18nService.t("authenticatorAppTitle");
TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
this.i18nService.t("authenticatorAppDesc");
TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDesc");
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
"Duo (" + this.i18nService.t("organization") + ")";
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
this.i18nService.t("duoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
this.i18nService.t("webAuthnDesc");
TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitle");
TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
this.i18nService.t("yubiKeyDesc");
}
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
const providers: any[] = [];
if (this.twoFactorProvidersData == null) {
return providers;
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
this.platformUtilsService.supportsWebAuthn(win)
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
}
return providers;
}
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
if (this.twoFactorProvidersData == null) {
return null;
}
if (
this.selectedTwoFactorProviderType != null &&
this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)
) {
return this.selectedTwoFactorProviderType;
}
let providerType: TwoFactorProviderType = null;
let providerPriority = -1;
this.twoFactorProvidersData.forEach((_value, type) => {
const provider = (TwoFactorProviders as any)[type];
if (provider != null && provider.priority > providerPriority) {
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
return;
}
providerType = type;
providerPriority = provider.priority;
}
});
return providerType;
}
setSelectedProvider(type: TwoFactorProviderType) {
this.selectedTwoFactorProviderType = type;
}
clearSelectedProvider() {
this.selectedTwoFactorProviderType = null;
}
setProviders(response: IdentityTwoFactorResponse) {
this.twoFactorProvidersData = response.twoFactorProviders2;
}
clearProviders() {
this.twoFactorProvidersData = null;
}
getProviders() {
return this.twoFactorProvidersData;
}
}

View File

@@ -0,0 +1,4 @@
export abstract class AuthService {
logIn: (credentials: { clientId: string; clientSecret: string }) => Promise<void>;
logOut: (callback: () => void) => void;
}

View File

@@ -3,13 +3,12 @@ import { Router } from "@angular/router";
import { takeUntil } from "rxjs";
import { ModalService } from "@/jslib/angular/src/services/modal.service";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { AuthService } from "../../abstractions/auth.service";
import { StateService } from "../../abstractions/state.service";
import { EnvironmentComponent } from "./environment.component";
@@ -81,9 +80,10 @@ export class ApiKeyComponent {
}
try {
this.formPromise = this.authService.logIn(
new ApiLogInCredentials(this.clientId, this.clientSecret),
);
this.formPromise = this.authService.logIn({
clientId: this.clientId,
clientSecret: this.clientSecret,
});
await this.formPromise;
const organizationId = await this.stateService.getEntityId();
await this.stateService.setOrganizationId(organizationId);

View File

@@ -10,7 +10,6 @@ import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
import { IndividualConfig, ToastrService } from "ngx-toastr";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService } from "@/jslib/common/src/abstractions/broadcaster.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
@@ -18,6 +17,7 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { AuthService } from "../abstractions/auth.service";
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../services/sync.service";

View File

@@ -3,20 +3,17 @@ import { APP_INITIALIZER, NgModule } from "@angular/core";
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ContainerService } from "@/jslib/common/src/services/container.service";
@@ -28,11 +25,11 @@ import { ElectronRendererStorageService } from "@/jslib/electron/src/services/el
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
import { Account } from "../../models/account";
import { AuthService } from "../../services/auth.service";
import { I18nService } from "../../services/i18n.service";
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
import { SyncService } from "../../services/sync.service";
@@ -160,18 +157,11 @@ export function initFactory(
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogServiceAbstraction,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
}),
safeProvider({
@@ -225,11 +215,6 @@ export function initFactory(
StateMigrationServiceAbstraction,
],
}),
safeProvider({
provide: TwoFactorServiceAbstraction,
useClass: NoopTwoFactorService,
deps: [],
}),
] satisfies SafeProvider[],
})
export class ServicesModule {}

View File

@@ -2,7 +2,6 @@ import * as fs from "fs";
import * as path from "path";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
@@ -11,9 +10,7 @@ import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { ContainerService } from "@/jslib/common/src/services/container.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
@@ -26,7 +23,6 @@ import { AuthService } from "./services/auth.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NoopTwoFactorService } from "./services/noop/noopTwoFactor.service";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
import { SyncService } from "./services/sync.service";
@@ -53,11 +49,8 @@ export class Main {
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
syncService: SyncService;
keyConnectorService: KeyConnectorService;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
twoFactorService: TwoFactorServiceAbstraction;
constructor() {
const applicationName = "Bitwarden Directory Connector";
@@ -145,33 +138,12 @@ export class Main {
);
this.containerService = new ContainerService(this.cryptoService);
this.organizationService = new OrganizationService(this.stateService);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
this.apiService,
this.tokenService,
this.logService,
this.organizationService,
this.cryptoFunctionService,
);
this.twoFactorService = new NoopTwoFactorService();
this.authService = new AuthService(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService,
);
this.syncService = new SyncService(

View File

@@ -1,8 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { AuthService } from "../abstractions/auth.service";
import { LoginCommand } from "./login.command";
@@ -37,11 +35,9 @@ describe("LoginCommand", () => {
process.env.BW_CLIENTID = clientId;
process.env.BW_CLIENTSECRET = clientSecret;
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
expect(result).toMatchObject({
data: {
title: "You are logged in!",
@@ -51,11 +47,9 @@ describe("LoginCommand", () => {
});
it("uses client id and secret prompted from the user", async () => {
authService.logIn.mockResolvedValue(new AuthResult()); // logging in with api key does not set any flag on the authResult
const result = await loginCommand.run();
expect(authService.logIn).toHaveBeenCalledWith(new ApiLogInCredentials(clientId, clientSecret));
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
expect(result).toMatchObject({
data: {
title: "You are logged in!",

View File

@@ -1,11 +1,10 @@
import * as inquirer from "inquirer";
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { Utils } from "../../jslib/common/src/misc/utils";
import { AuthService } from "../abstractions/auth.service";
export class LoginCommand {
private canInteract: boolean;
@@ -26,7 +25,7 @@ export class LoginCommand {
}
try {
await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
await this.authService.logIn({ clientId, clientSecret });
const res = new MessageResponse("You are logged in!", null);
return Response.success(res);

View File

@@ -1,13 +1,12 @@
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { AuthService } from "../abstractions/auth.service";
export class LogoutCommand {
constructor(
private authService: AuthService,
private i18nService: I18nService,
private logoutCallback: () => Promise<void>,
) {}

View File

@@ -1,66 +0,0 @@
import { LogInStrategy } from "@/jslib/common/src/misc/logInStrategies/logIn.strategy";
import {
AccountKeys,
AccountProfile,
AccountTokens,
} from "@/jslib/common/src/models/domain/account";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { Account, DirectoryConfigurations, DirectorySettings } from "@/src/models/account";
export class OrganizationLogInStrategy extends LogInStrategy {
tokenRequest: ApiTokenRequest;
async logIn(credentials: ApiLogInCredentials) {
this.tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
await this.buildTwoFactor(),
await this.buildDeviceRequest(),
);
return this.startLogIn();
}
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
await this.saveAccountInformation(response);
return new AuthResult();
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
const clientId = this.tokenRequest.clientId;
const entityId = clientId.split("organization.")[1];
const clientSecret = this.tokenRequest.clientSecret;
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: entityId,
apiKeyClientId: clientId,
entityId: entityId,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
keys: {
...new AccountKeys(),
...{
apiKeyClientSecret: clientSecret,
},
},
directorySettings: new DirectorySettings(),
directoryConfigurations: new DirectoryConfigurations(),
}),
);
}
}

View File

@@ -5,7 +5,6 @@ import { Command, OptionValues } from "commander";
import { Utils } from "@/jslib/common/src/misc/utils";
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
import { LogoutCommand } from "@/jslib/node/src/cli/commands/logout.command";
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
import { Response } from "@/jslib/node/src/cli/models/response";
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
@@ -15,6 +14,7 @@ import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from "./commands/config.command";
import { LastSyncCommand } from "./commands/lastSync.command";
import { LoginCommand } from "./commands/login.command";
import { LogoutCommand } from "./commands/logout.command";
import { SyncCommand } from "./commands/sync.command";
import { TestCommand } from "./commands/test.command";
@@ -118,7 +118,6 @@ export class Program extends BaseProgram {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout(),
);
const response = await command.run();

View File

@@ -1,65 +1,91 @@
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { AuthService as AuthServiceBase } from "@/jslib/common/src/services/auth.service";
import {
AccountKeys,
AccountProfile,
AccountTokens,
} from "@/jslib/common/src/models/domain/account";
import { DeviceRequest } from "@/jslib/common/src/models/request/deviceRequest";
import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest";
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { StateService } from "../abstractions/state.service";
import { OrganizationLogInStrategy } from "../misc/logInStrategies/organizationLogIn.strategy";
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
export class AuthService extends AuthServiceBase {
export class AuthService {
constructor(
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
keyConnectorService: KeyConnectorService,
environmentService: EnvironmentService,
stateService: StateService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
) {
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
keyConnectorService,
environmentService,
stateService,
twoFactorService,
i18nService,
private apiService: ApiService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private stateService: StateService,
) {}
async logIn(credentials: { clientId: string; clientSecret: string }) {
const tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
new TokenRequestTwoFactor(), // unused
await this.buildDeviceRequest(),
);
const response = await this.apiService.postIdentityToken(tokenRequest);
if (response instanceof IdentityTokenResponse) {
await this.saveAccountInformation(tokenRequest, response);
return;
}
throw new Error("Invalid response object.");
}
async logIn(credentials: ApiLogInCredentials): Promise<AuthResult> {
const strategy = new OrganizationLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
);
logOut(callback: () => void) {
callback();
this.messagingService.send("loggedOut");
}
return strategy.logIn(credentials);
private async buildDeviceRequest() {
const appId = await this.appIdService.getAppId();
return new DeviceRequest(appId, this.platformUtilsService);
}
private async saveAccountInformation(
tokenRequest: ApiTokenRequest,
tokenResponse: IdentityTokenResponse,
) {
const clientId = tokenRequest.clientId;
const entityId = clientId.split("organization.")[1];
const clientSecret = tokenRequest.clientSecret;
await this.stateService.addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: entityId,
apiKeyClientId: clientId,
entityId: entityId,
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
},
},
keys: {
...new AccountKeys(),
...{
apiKeyClientSecret: clientSecret,
},
},
directorySettings: new DirectorySettings(),
directoryConfigurations: new DirectoryConfigurations(),
}),
);
}
}

View File

@@ -0,0 +1,97 @@
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { Utils } from "@/jslib/common/src/misc/utils";
import {
AccountKeys,
AccountProfile,
AccountTokens,
} from "@/jslib/common/src/models/domain/account";
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service";
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
import { AuthService } from "./auth.service";
import { StateService } from "./state.service";
const clientId = "organization.CLIENT_ID";
const clientSecret = "CLIENT_SECRET";
const deviceId = Utils.newGuid();
const accessToken = "ACCESS_TOKEN";
const refreshToken = "REFRESH_TOKEN";
export function identityTokenResponseFactory() {
return new IdentityTokenResponse({
access_token: accessToken,
refresh_token: refreshToken, // not actually sure this is sent but including it out of caution
expires_in: 3600,
token_type: "Bearer",
scope: "api.organization",
});
}
describe("AuthService", () => {
let apiService: SubstituteOf<ApiService>;
let appIdService: SubstituteOf<AppIdService>;
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
let messagingService: SubstituteOf<MessagingService>;
let stateService: SubstituteOf<StateService>;
let authService: AuthService;
beforeEach(async () => {
apiService = Substitute.for();
appIdService = Substitute.for();
platformUtilsService = Substitute.for();
stateService = Substitute.for();
messagingService = Substitute.for();
appIdService.getAppId().resolves(deviceId);
authService = new AuthService(
apiService,
appIdService,
platformUtilsService,
messagingService,
stateService,
);
});
it("sets the local environment after a successful login", async () => {
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
await authService.logIn({ clientId, clientSecret });
stateService.received(1).addAccount(
new Account({
profile: {
...new AccountProfile(),
...{
userId: "CLIENT_ID",
apiKeyClientId: clientId, // with the "organization." prefix
entityId: "CLIENT_ID",
},
},
tokens: {
...new AccountTokens(),
...{
accessToken: accessToken,
refreshToken: refreshToken,
},
},
keys: {
...new AccountKeys(),
...{
apiKeyClientSecret: clientSecret,
},
},
directorySettings: new DirectorySettings(),
directoryConfigurations: new DirectoryConfigurations(),
}),
);
});
});

View File

@@ -1,40 +0,0 @@
import {
TwoFactorProviderDetails,
TwoFactorService,
} from "@/jslib/common/src/abstractions/twoFactor.service";
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
export class NoopTwoFactorService implements TwoFactorService {
init() {
// Noop
}
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
return null;
}
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
return null;
}
setSelectedProvider(type: TwoFactorProviderType) {
// Noop
}
clearSelectedProvider() {
// Noop
}
setProviders(response: IdentityTwoFactorResponse) {
// Noop
}
clearProviders() {
// Noop
}
getProviders(): Map<TwoFactorProviderType, { [key: string]: string }> {
return null;
}
}