diff --git a/jslib/angular/src/services/jslib-services.module.ts b/jslib/angular/src/services/jslib-services.module.ts index b7bc1de2a..20331a658 100644 --- a/jslib/angular/src/services/jslib-services.module.ts +++ b/jslib/angular/src/services/jslib-services.module.ts @@ -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 {} diff --git a/jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts b/jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts deleted file mode 100644 index 78fa3d013..000000000 --- a/jslib/common/spec/misc/logInStrategies/apiLogIn.strategy.spec.ts +++ /dev/null @@ -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; - let apiService: SubstituteOf; - let tokenService: SubstituteOf; - let appIdService: SubstituteOf; - let platformUtilsService: SubstituteOf; - let messagingService: SubstituteOf; - let logService: SubstituteOf; - let environmentService: SubstituteOf; - let keyConnectorService: SubstituteOf; - let stateService: SubstituteOf; - let twoFactorService: SubstituteOf; - - let 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(); - apiService = Substitute.for(); - tokenService = Substitute.for(); - appIdService = Substitute.for(); - platformUtilsService = Substitute.for(); - messagingService = Substitute.for(); - logService = Substitute.for(); - environmentService = Substitute.for(); - stateService = Substitute.for(); - keyConnectorService = Substitute.for(); - twoFactorService = Substitute.for(); - - 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); - }); -}); diff --git a/jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts b/jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts deleted file mode 100644 index c8a90220d..000000000 --- a/jslib/common/spec/misc/logInStrategies/logIn.strategy.spec.ts +++ /dev/null @@ -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; - let apiService: SubstituteOf; - let tokenService: SubstituteOf; - let appIdService: SubstituteOf; - let platformUtilsService: SubstituteOf; - let messagingService: SubstituteOf; - let logService: SubstituteOf; - let stateService: SubstituteOf; - let twoFactorService: SubstituteOf; - let authService: SubstituteOf; - - let passwordLogInStrategy: PasswordLogInStrategy; - let credentials: PasswordLogInCredentials; - - beforeEach(async () => { - cryptoService = Substitute.for(); - apiService = Substitute.for(); - tokenService = Substitute.for(); - appIdService = Substitute.for(); - platformUtilsService = Substitute.for(); - messagingService = Substitute.for(); - logService = Substitute.for(); - stateService = Substitute.for(); - twoFactorService = Substitute.for(); - authService = Substitute.for(); - - 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(); - 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 - ); - }), - ); - }); - }); -}); diff --git a/jslib/common/spec/misc/logInStrategies/passwordLogIn.strategy.spec.ts b/jslib/common/spec/misc/logInStrategies/passwordLogIn.strategy.spec.ts deleted file mode 100644 index e36fde211..000000000 --- a/jslib/common/spec/misc/logInStrategies/passwordLogIn.strategy.spec.ts +++ /dev/null @@ -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; - let apiService: SubstituteOf; - let tokenService: SubstituteOf; - let appIdService: SubstituteOf; - let platformUtilsService: SubstituteOf; - let messagingService: SubstituteOf; - let logService: SubstituteOf; - let stateService: SubstituteOf; - let twoFactorService: SubstituteOf; - let authService: SubstituteOf; - - let passwordLogInStrategy: PasswordLogInStrategy; - let credentials: PasswordLogInCredentials; - - beforeEach(async () => { - cryptoService = Substitute.for(); - apiService = Substitute.for(); - tokenService = Substitute.for(); - appIdService = Substitute.for(); - platformUtilsService = Substitute.for(); - messagingService = Substitute.for(); - logService = Substitute.for(); - stateService = Substitute.for(); - twoFactorService = Substitute.for(); - authService = Substitute.for(); - - 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); - }); -}); diff --git a/jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts b/jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts deleted file mode 100644 index eb38fa393..000000000 --- a/jslib/common/spec/misc/logInStrategies/ssoLogIn.strategy.spec.ts +++ /dev/null @@ -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; - let apiService: SubstituteOf; - let tokenService: SubstituteOf; - let appIdService: SubstituteOf; - let platformUtilsService: SubstituteOf; - let messagingService: SubstituteOf; - let logService: SubstituteOf; - let keyConnectorService: SubstituteOf; - let stateService: SubstituteOf; - let twoFactorService: SubstituteOf; - - 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(); - apiService = Substitute.for(); - tokenService = Substitute.for(); - appIdService = Substitute.for(); - platformUtilsService = Substitute.for(); - messagingService = Substitute.for(); - logService = Substitute.for(); - stateService = Substitute.for(); - keyConnectorService = Substitute.for(); - twoFactorService = Substitute.for(); - - 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); - }); -}); diff --git a/jslib/common/src/abstractions/auth.service.ts b/jslib/common/src/abstractions/auth.service.ts deleted file mode 100644 index 1be0b2fef..000000000 --- a/jslib/common/src/abstractions/auth.service.ts +++ /dev/null @@ -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; - logInTwoFactor: ( - twoFactor: TokenRequestTwoFactor, - captchaResponse: string, - ) => Promise; - logOut: (callback: () => void) => void; - makePreloginKey: (masterPassword: string, email: string) => Promise; - authingWithApiKey: () => boolean; - authingWithSso: () => boolean; - authingWithPassword: () => boolean; -} diff --git a/jslib/common/src/abstractions/keyConnector.service.ts b/jslib/common/src/abstractions/keyConnector.service.ts deleted file mode 100644 index 1e4ee9282..000000000 --- a/jslib/common/src/abstractions/keyConnector.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Organization } from "../models/domain/organization"; -import { IdentityTokenResponse } from "../models/response/identityTokenResponse"; - -export abstract class KeyConnectorService { - getAndSetKey: (url?: string) => Promise; - getManagingOrganization: () => Promise; - getUsesKeyConnector: () => Promise; - migrateUser: () => Promise; - userNeedsMigration: () => Promise; - convertNewSsoUserToKeyConnector: ( - tokenResponse: IdentityTokenResponse, - orgId: string, - ) => Promise; - setUsesKeyConnector: (enabled: boolean) => Promise; - setConvertAccountRequired: (status: boolean) => Promise; - getConvertAccountRequired: () => Promise; - removeConvertAccountRequired: () => Promise; - clear: () => Promise; -} diff --git a/jslib/common/src/abstractions/organization.service.ts b/jslib/common/src/abstractions/organization.service.ts deleted file mode 100644 index e0b5fde43..000000000 --- a/jslib/common/src/abstractions/organization.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OrganizationData } from "../models/data/organizationData"; -import { Organization } from "../models/domain/organization"; - -export abstract class OrganizationService { - get: (id: string) => Promise; - getByIdentifier: (identifier: string) => Promise; - getAll: (userId?: string) => Promise; - save: (orgs: { [id: string]: OrganizationData }) => Promise; - canManageSponsorships: () => Promise; - hasOrganizations: (userId?: string) => Promise; -} diff --git a/jslib/common/src/abstractions/twoFactor.service.ts b/jslib/common/src/abstractions/twoFactor.service.ts deleted file mode 100644 index 74f091678..000000000 --- a/jslib/common/src/abstractions/twoFactor.service.ts +++ /dev/null @@ -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; -} diff --git a/jslib/common/src/enums/authenticationType.ts b/jslib/common/src/enums/authenticationType.ts deleted file mode 100644 index ed7375c80..000000000 --- a/jslib/common/src/enums/authenticationType.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum AuthenticationType { - Password = 0, - Sso = 1, - Api = 2, -} diff --git a/jslib/common/src/misc/logInStrategies/apiLogin.strategy.ts b/jslib/common/src/misc/logInStrategies/apiLogin.strategy.ts deleted file mode 100644 index 8333c5d95..000000000 --- a/jslib/common/src/misc/logInStrategies/apiLogin.strategy.ts +++ /dev/null @@ -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); - } -} diff --git a/jslib/common/src/misc/logInStrategies/logIn.strategy.ts b/jslib/common/src/misc/logInStrategies/logIn.strategy.ts deleted file mode 100644 index 6c846571e..000000000 --- a/jslib/common/src/misc/logInStrategies/logIn.strategy.ts +++ /dev/null @@ -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; - - async logInTwoFactor( - twoFactor: TokenRequestTwoFactor, - captchaResponse: string = null, - ): Promise { - this.tokenRequest.setTwoFactor(twoFactor); - return this.startLogIn(); - } - - protected async startLogIn(): Promise { - 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 { - // 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 { - 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 { - 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 { - 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); - } - } -} diff --git a/jslib/common/src/misc/logInStrategies/passwordLogin.strategy.ts b/jslib/common/src/misc/logInStrategies/passwordLogin.strategy.ts deleted file mode 100644 index 24a51608c..000000000 --- a/jslib/common/src/misc/logInStrategies/passwordLogin.strategy.ts +++ /dev/null @@ -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 { - 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(); - } -} diff --git a/jslib/common/src/misc/logInStrategies/ssoLogin.strategy.ts b/jslib/common/src/misc/logInStrategies/ssoLogin.strategy.ts deleted file mode 100644 index d7eb925de..000000000 --- a/jslib/common/src/misc/logInStrategies/ssoLogin.strategy.ts +++ /dev/null @@ -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(); - } -} diff --git a/jslib/common/src/models/domain/authResult.ts b/jslib/common/src/models/domain/authResult.ts deleted file mode 100644 index 0781f8721..000000000 --- a/jslib/common/src/models/domain/authResult.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { TwoFactorProviderType } from "../../enums/twoFactorProviderType"; -import { Utils } from "../../misc/utils"; - -export class AuthResult { - captchaSiteKey = ""; - resetMasterPassword = false; - forcePasswordReset = false; - twoFactorProviders: Map = null; - - get requiresCaptcha() { - return !Utils.isNullOrWhitespace(this.captchaSiteKey); - } - - get requiresTwoFactor() { - return this.twoFactorProviders != null; - } -} diff --git a/jslib/common/src/models/domain/logInCredentials.ts b/jslib/common/src/models/domain/logInCredentials.ts deleted file mode 100644 index e6dc2f88a..000000000 --- a/jslib/common/src/models/domain/logInCredentials.ts +++ /dev/null @@ -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, - ) {} -} diff --git a/jslib/common/src/services/auth.service.ts b/jslib/common/src/services/auth.service.ts deleted file mode 100644 index 9768a3c7d..000000000 --- a/jslib/common/src/services/auth.service.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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); - } - } -} diff --git a/jslib/common/src/services/keyConnector.service.ts b/jslib/common/src/services/keyConnector.service.ts deleted file mode 100644 index 803a3e52b..000000000 --- a/jslib/common/src/services/keyConnector.service.ts +++ /dev/null @@ -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 { - 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 { - return await this.stateService.getConvertAccountToKeyConnector(); - } - - async removeConvertAccountRequired() { - await this.stateService.setConvertAccountToKeyConnector(null); - } - - async clear() { - await this.removeConvertAccountRequired(); - } -} diff --git a/jslib/common/src/services/organization.service.ts b/jslib/common/src/services/organization.service.ts deleted file mode 100644 index 368f2209e..000000000 --- a/jslib/common/src/services/organization.service.ts +++ /dev/null @@ -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 { - 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 { - 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 { - 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 { - const orgs = await this.getAll(); - return orgs.some( - (o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null, - ); - } - - async hasOrganizations(userId?: string): Promise { - const organizations = await this.getAll(userId); - return organizations.length > 0; - } -} diff --git a/jslib/common/src/services/twoFactor.service.ts b/jslib/common/src/services/twoFactor.service.ts deleted file mode 100644 index d58c042d7..000000000 --- a/jslib/common/src/services/twoFactor.service.ts +++ /dev/null @@ -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> = - { - [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; - 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; - } -} diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts new file mode 100644 index 000000000..3176cff68 --- /dev/null +++ b/src/abstractions/auth.service.ts @@ -0,0 +1,4 @@ +export abstract class AuthService { + logIn: (credentials: { clientId: string; clientSecret: string }) => Promise; + logOut: (callback: () => void) => void; +} diff --git a/src/app/accounts/apiKey.component.ts b/src/app/accounts/apiKey.component.ts index 615f14079..4504b7d16 100644 --- a/src/app/accounts/apiKey.component.ts +++ b/src/app/accounts/apiKey.component.ts @@ -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); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 8d156062c..17d8e2272 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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"; diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index e45fecc8b..a3b219b22 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -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 {} diff --git a/src/bwdc.ts b/src/bwdc.ts index c0a526cbb..84abf65bb 100644 --- a/src/bwdc.ts +++ b/src/bwdc.ts @@ -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( diff --git a/src/commands/login.command.spec.ts b/src/commands/login.command.spec.ts index 8630eaa86..bf890f360 100644 --- a/src/commands/login.command.spec.ts +++ b/src/commands/login.command.spec.ts @@ -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!", diff --git a/src/commands/login.command.ts b/src/commands/login.command.ts index e68e0e33e..b74ab6077 100644 --- a/src/commands/login.command.ts +++ b/src/commands/login.command.ts @@ -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); diff --git a/jslib/node/src/cli/commands/logout.command.ts b/src/commands/logout.command.ts similarity index 54% rename from jslib/node/src/cli/commands/logout.command.ts rename to src/commands/logout.command.ts index 60688a50d..2c4e95019 100644 --- a/jslib/node/src/cli/commands/logout.command.ts +++ b/src/commands/logout.command.ts @@ -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, ) {} diff --git a/src/misc/logInStrategies/organizationLogIn.strategy.ts b/src/misc/logInStrategies/organizationLogIn.strategy.ts deleted file mode 100644 index 53a092366..000000000 --- a/src/misc/logInStrategies/organizationLogIn.strategy.ts +++ /dev/null @@ -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 { - 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(), - }), - ); - } -} diff --git a/src/program.ts b/src/program.ts index 2700e2ea2..84eba6604 100644 --- a/src/program.ts +++ b/src/program.ts @@ -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(); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 6e1b38ebf..645c63118 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -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 { - 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"); + } + + private async buildDeviceRequest() { + const appId = await this.appIdService.getAppId(); + return new DeviceRequest(appId, this.platformUtilsService); + } - return strategy.logIn(credentials); + 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(), + }), + ); } } diff --git a/src/services/authService.spec.ts b/src/services/authService.spec.ts new file mode 100644 index 000000000..8a3a33456 --- /dev/null +++ b/src/services/authService.spec.ts @@ -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; + let appIdService: SubstituteOf; + let platformUtilsService: SubstituteOf; + let messagingService: SubstituteOf; + let stateService: SubstituteOf; + + 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(), + }), + ); + }); +}); diff --git a/src/services/noop/noopTwoFactor.service.ts b/src/services/noop/noopTwoFactor.service.ts deleted file mode 100644 index 5e9910b95..000000000 --- a/src/services/noop/noopTwoFactor.service.ts +++ /dev/null @@ -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 { - return null; - } -}