From aa2bdd00be0bd0a79dba5e9f2d8f09c9f4085fc9 Mon Sep 17 00:00:00 2001
From: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Date: Tue, 1 Feb 2022 09:51:32 +1000
Subject: [PATCH] [Tech debt] Refactor authService and remove LogInHelper
 (#588)

* Use different strategy classes for different types of login
* General refactor and cleanup of auth logic
* Create subclasses for different types of login credentials
* Create subclasses for different types of tokenRequests
* Create TwoFactorService, move code out of authService
* refactor base CLI commands to use new interface
---
 angular/src/components/login.component.ts     |  11 +-
 angular/src/components/sso.component.ts       |   6 +-
 .../two-factor-options.component.ts           |   5 +-
 .../src/components/two-factor.component.ts    |  28 +-
 angular/src/services/jslib-services.module.ts |  12 +-
 common/src/abstractions/api.service.ts        |   7 +-
 common/src/abstractions/auth.service.ts       |  61 +-
 .../src/abstractions/keyConnector.service.ts  |   6 +
 common/src/abstractions/token.service.ts      |   8 +-
 common/src/abstractions/twoFactor.service.ts  |  24 +
 .../misc/logInStrategies/apiLogin.strategy.ts |  73 ++
 .../misc/logInStrategies/logIn.strategy.ts    | 177 +++++
 .../logInStrategies/passwordLogin.strategy.ts |  88 +++
 .../misc/logInStrategies/ssoLogin.strategy.ts |  73 ++
 common/src/models/domain/authResult.ts        |  11 +-
 common/src/models/domain/logInCredentials.ts  |  24 +
 .../request/identityToken/apiTokenRequest.ts  |  24 +
 .../identityToken/passwordTokenRequest.ts     |  36 +
 .../request/identityToken/ssoTokenRequest.ts  |  26 +
 .../request/identityToken/tokenRequest.ts     |  48 ++
 common/src/models/request/tokenRequest.ts     |  91 ---
 common/src/services/api.service.ts            |  18 +-
 common/src/services/auth.service.ts           | 700 +++---------------
 common/src/services/keyConnector.service.ts   |  43 +-
 common/src/services/token.service.ts          |   6 +-
 common/src/services/twoFactor.service.ts      | 188 +++++
 node/src/cli/commands/login.command.ts        | 272 ++++---
 .../logInStrategies/apiLogIn.strategy.spec.ts | 116 +++
 .../logInStrategies/logIn.strategy.spec.ts    | 293 ++++++++
 .../passwordLogIn.strategy.spec.ts            | 113 +++
 .../logInStrategies/ssoLogIn.strategy.spec.ts | 130 ++++
 31 files changed, 1798 insertions(+), 920 deletions(-)
 create mode 100644 common/src/abstractions/twoFactor.service.ts
 create mode 100644 common/src/misc/logInStrategies/apiLogin.strategy.ts
 create mode 100644 common/src/misc/logInStrategies/logIn.strategy.ts
 create mode 100644 common/src/misc/logInStrategies/passwordLogin.strategy.ts
 create mode 100644 common/src/misc/logInStrategies/ssoLogin.strategy.ts
 create mode 100644 common/src/models/domain/logInCredentials.ts
 create mode 100644 common/src/models/request/identityToken/apiTokenRequest.ts
 create mode 100644 common/src/models/request/identityToken/passwordTokenRequest.ts
 create mode 100644 common/src/models/request/identityToken/ssoTokenRequest.ts
 create mode 100644 common/src/models/request/identityToken/tokenRequest.ts
 delete mode 100644 common/src/models/request/tokenRequest.ts
 create mode 100644 common/src/services/twoFactor.service.ts
 create mode 100644 spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts
 create mode 100644 spec/common/misc/logInStrategies/logIn.strategy.spec.ts
 create mode 100644 spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts
 create mode 100644 spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts

diff --git a/angular/src/components/login.component.ts b/angular/src/components/login.component.ts
index e860ca9c15..f719267191 100644
--- a/angular/src/components/login.component.ts
+++ b/angular/src/components/login.component.ts
@@ -5,6 +5,7 @@ import { Router } from "@angular/router";
 import { take } from "rxjs/operators";
 
 import { AuthResult } from "jslib-common/models/domain/authResult";
+import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
 
 import { AuthService } from "jslib-common/abstractions/auth.service";
 import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
@@ -96,7 +97,13 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
     }
 
     try {
-      this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken);
+      const credentials = new PasswordLogInCredentials(
+        this.email,
+        this.masterPassword,
+        this.captchaToken,
+        null
+      );
+      this.formPromise = this.authService.logIn(credentials);
       const response = await this.formPromise;
       if (this.rememberEmail || this.alwaysRememberEmail) {
         await this.stateService.setRememberedEmail(this.email);
@@ -105,7 +112,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
       }
       if (this.handleCaptchaRequired(response)) {
         return;
-      } else if (response.twoFactor) {
+      } else if (response.requiresTwoFactor) {
         if (this.onSuccessfulLoginTwoFactorNavigate != null) {
           this.onSuccessfulLoginTwoFactorNavigate();
         } else {
diff --git a/angular/src/components/sso.component.ts b/angular/src/components/sso.component.ts
index 649f298cab..06d0d0377c 100644
--- a/angular/src/components/sso.component.ts
+++ b/angular/src/components/sso.component.ts
@@ -16,6 +16,7 @@ import { StateService } from "jslib-common/abstractions/state.service";
 import { Utils } from "jslib-common/misc/utils";
 
 import { AuthResult } from "jslib-common/models/domain/authResult";
+import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
 
 @Directive()
 export class SsoComponent {
@@ -171,14 +172,15 @@ export class SsoComponent {
   private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
     this.loggingIn = true;
     try {
-      this.formPromise = this.authService.logInSso(
+      const credentials = new SsoLogInCredentials(
         code,
         codeVerifier,
         this.redirectUri,
         orgIdFromState
       );
+      this.formPromise = this.authService.logIn(credentials);
       const response = await this.formPromise;
-      if (response.twoFactor) {
+      if (response.requiresTwoFactor) {
         if (this.onSuccessfulLoginTwoFactorNavigate != null) {
           this.onSuccessfulLoginTwoFactorNavigate();
         } else {
diff --git a/angular/src/components/two-factor-options.component.ts b/angular/src/components/two-factor-options.component.ts
index 4909757f43..ddf3d2f3de 100644
--- a/angular/src/components/two-factor-options.component.ts
+++ b/angular/src/components/two-factor-options.component.ts
@@ -6,6 +6,7 @@ import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"
 import { AuthService } from "jslib-common/abstractions/auth.service";
 import { I18nService } from "jslib-common/abstractions/i18n.service";
 import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
 
 @Directive()
 export class TwoFactorOptionsComponent implements OnInit {
@@ -15,7 +16,7 @@ export class TwoFactorOptionsComponent implements OnInit {
   providers: any[] = [];
 
   constructor(
-    protected authService: AuthService,
+    protected twoFactorService: TwoFactorService,
     protected router: Router,
     protected i18nService: I18nService,
     protected platformUtilsService: PlatformUtilsService,
@@ -23,7 +24,7 @@ export class TwoFactorOptionsComponent implements OnInit {
   ) {}
 
   ngOnInit() {
-    this.providers = this.authService.getSupportedTwoFactorProviders(this.win);
+    this.providers = this.twoFactorService.getSupportedProviders(this.win);
   }
 
   choose(p: any) {
diff --git a/angular/src/components/two-factor.component.ts b/angular/src/components/two-factor.component.ts
index f3813a9664..16a5873a7b 100644
--- a/angular/src/components/two-factor.component.ts
+++ b/angular/src/components/two-factor.component.ts
@@ -18,9 +18,10 @@ import { LogService } from "jslib-common/abstractions/log.service";
 import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
 import { StateService } from "jslib-common/abstractions/state.service";
 
-import { TwoFactorProviders } from "jslib-common/services/auth.service";
+import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
 
 import * as DuoWebSDK from "duo_web_sdk";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
 import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
 
 @Directive()
@@ -59,13 +60,14 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
     protected environmentService: EnvironmentService,
     protected stateService: StateService,
     protected route: ActivatedRoute,
-    protected logService: LogService
+    protected logService: LogService,
+    protected twoFactorService: TwoFactorService
   ) {
     this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
   }
 
   async ngOnInit() {
-    if (!this.authing || this.authService.twoFactorProvidersData == null) {
+    if (!this.authing || this.twoFactorService.getProviders() == null) {
       this.router.navigate([this.loginRoute]);
       return;
     }
@@ -103,9 +105,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
       );
     }
 
-    this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(
-      this.webAuthnSupported
-    );
+    this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported);
     await this.init();
   }
 
@@ -122,7 +122,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
 
     this.cleanupWebAuthn();
     this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
-    const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
+    const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
     switch (this.selectedProviderType) {
       case TwoFactorProviderType.WebAuthn:
         if (!this.webAuthnNewTab) {
@@ -150,7 +150,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
         break;
       case TwoFactorProviderType.Email:
         this.twoFactorEmail = providerData.Email;
-        if (this.authService.twoFactorProvidersData.size > 1) {
+        if (this.twoFactorService.getProviders().size > 1) {
           await this.sendEmail(false);
         }
         break;
@@ -192,11 +192,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
   }
 
   async doSubmit() {
-    this.formPromise = this.authService.logInTwoFactor(
-      this.selectedProviderType,
-      this.token,
-      this.remember
-    );
+    this.formPromise = this.authService.logInTwoFactor({
+      provider: this.selectedProviderType,
+      token: this.token,
+      remember: this.remember,
+    });
     const response: AuthResult = await this.formPromise;
     const disableFavicon = await this.stateService.getDisableFavicon();
     await this.stateService.setDisableFavicon(!!disableFavicon);
@@ -250,7 +250,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
   }
 
   authWebAuthn() {
-    const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
+    const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
 
     if (!this.webAuthnSupported || this.webAuthn == null) {
       return;
diff --git a/angular/src/services/jslib-services.module.ts b/angular/src/services/jslib-services.module.ts
index a08bce2410..ec6261cf97 100644
--- a/angular/src/services/jslib-services.module.ts
+++ b/angular/src/services/jslib-services.module.ts
@@ -27,6 +27,7 @@ import { StateMigrationService } from "jslib-common/services/stateMigration.serv
 import { SyncService } from "jslib-common/services/sync.service";
 import { TokenService } from "jslib-common/services/token.service";
 import { TotpService } from "jslib-common/services/totp.service";
+import { TwoFactorService } from "jslib-common/services/twoFactor.service";
 import { UserVerificationService } from "jslib-common/services/userVerification.service";
 import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
 import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
@@ -65,6 +66,7 @@ import { StorageService as StorageServiceAbstraction } from "jslib-common/abstra
 import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service";
 import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
 import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service";
+import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
 import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service";
 import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
 
@@ -114,15 +116,13 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
         ApiServiceAbstraction,
         TokenServiceAbstraction,
         AppIdServiceAbstraction,
-        I18nServiceAbstraction,
         PlatformUtilsServiceAbstraction,
         MessagingServiceAbstraction,
-        VaultTimeoutServiceAbstraction,
         LogService,
-        CryptoFunctionServiceAbstraction,
         KeyConnectorServiceAbstraction,
         EnvironmentServiceAbstraction,
         StateServiceAbstraction,
+        TwoFactorServiceAbstraction,
       ],
     },
     {
@@ -455,6 +455,7 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
         TokenServiceAbstraction,
         LogService,
         OrganizationServiceAbstraction,
+        CryptoFunctionServiceAbstraction,
       ],
     },
     {
@@ -473,6 +474,11 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
       useClass: ProviderService,
       deps: [StateServiceAbstraction],
     },
+    {
+      provide: TwoFactorServiceAbstraction,
+      useClass: TwoFactorService,
+      deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
+    },
   ],
 })
 export class JslibServicesModule {}
diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts
index 4ef3c1a93c..a23b311bbf 100644
--- a/common/src/abstractions/api.service.ts
+++ b/common/src/abstractions/api.service.ts
@@ -75,7 +75,6 @@ import { SendRequest } from "../models/request/sendRequest";
 import { SetPasswordRequest } from "../models/request/setPasswordRequest";
 import { StorageRequest } from "../models/request/storageRequest";
 import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
-import { TokenRequest } from "../models/request/tokenRequest";
 import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
 import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
 import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
@@ -93,6 +92,10 @@ import { VerifyBankRequest } from "../models/request/verifyBankRequest";
 import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest";
 import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
 
+import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
+import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
+import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
+
 import { ApiKeyResponse } from "../models/response/apiKeyResponse";
 import { AttachmentResponse } from "../models/response/attachmentResponse";
 import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
@@ -171,7 +174,7 @@ import { SendAccessView } from "../models/view/sendAccessView";
 
 export abstract class ApiService {
   postIdentityToken: (
-    request: TokenRequest
+    request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
   ) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
   refreshIdentityToken: () => Promise<any>;
 
diff --git a/common/src/abstractions/auth.service.ts b/common/src/abstractions/auth.service.ts
index 10dff3a87d..ec36acc8bb 100644
--- a/common/src/abstractions/auth.service.ts
+++ b/common/src/abstractions/auth.service.ts
@@ -1,58 +1,21 @@
-import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
-
 import { AuthResult } from "../models/domain/authResult";
+import {
+  ApiLogInCredentials,
+  PasswordLogInCredentials,
+  SsoLogInCredentials,
+} from "../models/domain/logInCredentials";
 import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
 
-export abstract class AuthService {
-  email: string;
-  masterPasswordHash: string;
-  code: string;
-  codeVerifier: string;
-  ssoRedirectUrl: string;
-  clientId: string;
-  clientSecret: string;
-  twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
-  selectedTwoFactorProviderType: TwoFactorProviderType;
+import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
 
-  logIn: (email: string, masterPassword: string, captchaToken?: string) => Promise<AuthResult>;
-  logInSso: (
-    code: string,
-    codeVerifier: string,
-    redirectUrl: string,
-    orgId: string
-  ) => Promise<AuthResult>;
-  logInApiKey: (clientId: string, clientSecret: string) => Promise<AuthResult>;
-  logInTwoFactor: (
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
-  ) => Promise<AuthResult>;
-  logInComplete: (
-    email: string,
-    masterPassword: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean,
-    captchaToken?: string
-  ) => Promise<AuthResult>;
-  logInSsoComplete: (
-    code: string,
-    codeVerifier: string,
-    redirectUrl: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
-  ) => Promise<AuthResult>;
-  logInApiKeyComplete: (
-    clientId: string,
-    clientSecret: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
+export abstract class AuthService {
+  masterPasswordHash: string;
+  email: string;
+  logIn: (
+    credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
   ) => Promise<AuthResult>;
+  logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise<AuthResult>;
   logOut: (callback: Function) => void;
-  getSupportedTwoFactorProviders: (win: Window) => any[];
-  getDefaultTwoFactorProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
   makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
   authingWithApiKey: () => boolean;
   authingWithSso: () => boolean;
diff --git a/common/src/abstractions/keyConnector.service.ts b/common/src/abstractions/keyConnector.service.ts
index ca57bbf6bc..5b1ca3a36e 100644
--- a/common/src/abstractions/keyConnector.service.ts
+++ b/common/src/abstractions/keyConnector.service.ts
@@ -1,11 +1,17 @@
 import { Organization } from "../models/domain/organization";
 
+import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
+
 export abstract class KeyConnectorService {
   getAndSetKey: (url?: string) => Promise<void>;
   getManagingOrganization: () => Promise<Organization>;
   getUsesKeyConnector: () => Promise<boolean>;
   migrateUser: () => Promise<void>;
   userNeedsMigration: () => Promise<boolean>;
+  convertNewSsoUserToKeyConnector: (
+    tokenResponse: IdentityTokenResponse,
+    orgId: string
+  ) => Promise<void>;
   setUsesKeyConnector: (enabled: boolean) => Promise<void>;
   setConvertAccountRequired: (status: boolean) => Promise<void>;
   getConvertAccountRequired: () => Promise<boolean>;
diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts
index 6006ba10dd..89aec53601 100644
--- a/common/src/abstractions/token.service.ts
+++ b/common/src/abstractions/token.service.ts
@@ -1,3 +1,5 @@
+import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
+
 export abstract class TokenService {
   setTokens: (
     accessToken: string,
@@ -13,9 +15,9 @@ export abstract class TokenService {
   setClientSecret: (clientSecret: string) => Promise<any>;
   getClientSecret: () => Promise<string>;
   toggleTokens: () => Promise<any>;
-  setTwoFactorToken: (token: string, email: string) => Promise<any>;
-  getTwoFactorToken: (email: string) => Promise<string>;
-  clearTwoFactorToken: (email: string) => Promise<any>;
+  setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
+  getTwoFactorToken: () => Promise<string>;
+  clearTwoFactorToken: () => Promise<any>;
   clearToken: (userId?: string) => Promise<any>;
   decodeToken: (token?: string) => any;
   getTokenExpirationDate: () => Promise<Date>;
diff --git a/common/src/abstractions/twoFactor.service.ts b/common/src/abstractions/twoFactor.service.ts
new file mode 100644
index 0000000000..071f6e7e6e
--- /dev/null
+++ b/common/src/abstractions/twoFactor.service.ts
@@ -0,0 +1,24 @@
+import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
+
+import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
+
+export interface TwoFactorProviderDetails {
+  type: TwoFactorProviderType;
+  name: string;
+  description: string;
+  priority: number;
+  sort: number;
+  premium: boolean;
+}
+
+export abstract class TwoFactorService {
+  init: () => void;
+  getSupportedProviders: (win: Window) => TwoFactorProviderDetails[];
+  getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
+  setSelectedProvider: (type: TwoFactorProviderType) => void;
+  clearSelectedProvider: () => void;
+
+  setProviders: (response: IdentityTwoFactorResponse) => void;
+  clearProviders: () => void;
+  getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>;
+}
diff --git a/common/src/misc/logInStrategies/apiLogin.strategy.ts b/common/src/misc/logInStrategies/apiLogin.strategy.ts
new file mode 100644
index 0000000000..65463cc0ea
--- /dev/null
+++ b/common/src/misc/logInStrategies/apiLogin.strategy.ts
@@ -0,0 +1,73 @@
+import { LogInStrategy } from "./logIn.strategy";
+
+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 { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
+
+import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
+
+import { ApiLogInCredentials } from "../../models/domain/logInCredentials";
+
+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/common/src/misc/logInStrategies/logIn.strategy.ts b/common/src/misc/logInStrategies/logIn.strategy.ts
new file mode 100644
index 0000000000..9cb18aac8a
--- /dev/null
+++ b/common/src/misc/logInStrategies/logIn.strategy.ts
@@ -0,0 +1,177 @@
+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/tokenRequest";
+import { KeysRequest } from "../../models/request/keysRequest";
+
+import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
+import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
+import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFactorResponse";
+
+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";
+
+export abstract class LogInStrategy {
+  protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
+
+  constructor(
+    protected cryptoService: CryptoService,
+    protected apiService: ApiService,
+    protected tokenService: TokenService,
+    protected appIdService: AppIdService,
+    protected platformUtilsService: PlatformUtilsService,
+    protected messagingService: MessagingService,
+    protected logService: LogService,
+    protected stateService: StateService,
+    protected twoFactorService: TwoFactorService
+  ) {}
+
+  abstract logIn(
+    credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
+  ): Promise<AuthResult>;
+
+  async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
+    this.tokenRequest.setTwoFactor(twoFactor);
+    return this.startLogIn();
+  }
+
+  protected async startLogIn(): Promise<AuthResult> {
+    this.twoFactorService.clearSelectedProvider();
+
+    const response = await this.apiService.postIdentityToken(this.tokenRequest);
+
+    if (response instanceof IdentityTwoFactorResponse) {
+      return this.processTwoFactorResponse(response);
+    } else if (response instanceof IdentityCaptchaResponse) {
+      return this.processCaptchaResponse(response);
+    } else if (response instanceof IdentityTokenResponse) {
+      return this.processTokenResponse(response);
+    }
+
+    throw new Error("Invalid response object.");
+  }
+
+  protected onSuccessfulLogin(response: IdentityTokenResponse): Promise<void> {
+    // Implemented in subclass if required
+    return null;
+  }
+
+  protected async buildDeviceRequest() {
+    const appId = await this.appIdService.getAppId();
+    return new DeviceRequest(appId, this.platformUtilsService);
+  }
+
+  protected async buildTwoFactor(userProvidedTwoFactor?: TokenRequestTwoFactor) {
+    if (userProvidedTwoFactor != null) {
+      return userProvidedTwoFactor;
+    }
+
+    const storedTwoFactorToken = await this.tokenService.getTwoFactorToken();
+    if (storedTwoFactorToken != null) {
+      return {
+        token: storedTwoFactorToken,
+        provider: TwoFactorProviderType.Remember,
+        remember: false,
+      };
+    }
+
+    return {
+      token: null,
+      provider: null,
+      remember: false,
+    };
+  }
+
+  protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
+    const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
+    await this.stateService.addAccount(
+      new Account({
+        profile: {
+          ...new AccountProfile(),
+          ...{
+            userId: accountInformation.sub,
+            email: accountInformation.email,
+            hasPremiumPersonally: accountInformation.premium,
+            kdfIterations: tokenResponse.kdfIterations,
+            kdfType: tokenResponse.kdf,
+          },
+        },
+        tokens: {
+          ...new AccountTokens(),
+          ...{
+            accessToken: tokenResponse.accessToken,
+            refreshToken: tokenResponse.refreshToken,
+          },
+        },
+      })
+    );
+  }
+
+  protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
+    const result = new AuthResult();
+    result.resetMasterPassword = response.resetMasterPassword;
+    result.forcePasswordReset = response.forcePasswordReset;
+
+    await this.saveAccountInformation(response);
+
+    if (response.twoFactorToken != null) {
+      await this.tokenService.setTwoFactorToken(response);
+    }
+
+    const newSsoUser = response.key == null;
+    if (!newSsoUser) {
+      await this.cryptoService.setEncKey(response.key);
+      await this.cryptoService.setEncPrivateKey(
+        response.privateKey ?? (await this.createKeyPairForOldAccount())
+      );
+    }
+
+    await this.onSuccessfulLogin(response);
+
+    await this.stateService.setBiometricLocked(false);
+    this.messagingService.send("loggedIn");
+
+    return result;
+  }
+
+  private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
+    const result = new AuthResult();
+    result.twoFactorProviders = response.twoFactorProviders2;
+    this.twoFactorService.setProviders(response);
+    return result;
+  }
+
+  private async processCaptchaResponse(response: IdentityCaptchaResponse): Promise<AuthResult> {
+    const result = new AuthResult();
+    result.captchaSiteKey = response.siteKey;
+    return result;
+  }
+
+  private async createKeyPairForOldAccount() {
+    try {
+      const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
+      await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
+      return privateKey.encryptedString;
+    } catch (e) {
+      this.logService.error(e);
+    }
+  }
+}
diff --git a/common/src/misc/logInStrategies/passwordLogin.strategy.ts b/common/src/misc/logInStrategies/passwordLogin.strategy.ts
new file mode 100644
index 0000000000..cdbe3bdcf5
--- /dev/null
+++ b/common/src/misc/logInStrategies/passwordLogin.strategy.ts
@@ -0,0 +1,88 @@
+import { LogInStrategy } from "./logIn.strategy";
+
+import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
+
+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 { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
+import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
+
+import { HashPurpose } from "../../enums/hashPurpose";
+
+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 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/common/src/misc/logInStrategies/ssoLogin.strategy.ts b/common/src/misc/logInStrategies/ssoLogin.strategy.ts
new file mode 100644
index 0000000000..d946764d36
--- /dev/null
+++ b/common/src/misc/logInStrategies/ssoLogin.strategy.ts
@@ -0,0 +1,73 @@
+import { LogInStrategy } from "./logIn.strategy";
+
+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";
+
+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/common/src/models/domain/authResult.ts b/common/src/models/domain/authResult.ts
index eadad50f77..bba3f7f300 100644
--- a/common/src/models/domain/authResult.ts
+++ b/common/src/models/domain/authResult.ts
@@ -1,9 +1,18 @@
 import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
 
+import { Utils } from "../../misc/utils";
+
 export class AuthResult {
-  twoFactor: boolean = false;
   captchaSiteKey: string = "";
   resetMasterPassword: boolean = false;
   forcePasswordReset: boolean = false;
   twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
+
+  get requiresCaptcha() {
+    return !Utils.isNullOrWhitespace(this.captchaSiteKey);
+  }
+
+  get requiresTwoFactor() {
+    return this.twoFactorProviders != null;
+  }
 }
diff --git a/common/src/models/domain/logInCredentials.ts b/common/src/models/domain/logInCredentials.ts
new file mode 100644
index 0000000000..d53da02ee0
--- /dev/null
+++ b/common/src/models/domain/logInCredentials.ts
@@ -0,0 +1,24 @@
+import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequest";
+
+export class PasswordLogInCredentials {
+  constructor(
+    public email: string,
+    public masterPassword: string,
+    public captchaToken?: string,
+    public twoFactor?: TokenRequestTwoFactor
+  ) {}
+}
+
+export class SsoLogInCredentials {
+  constructor(
+    public code: string,
+    public codeVerifier: string,
+    public redirectUrl: string,
+    public orgId: string,
+    public twoFactor?: TokenRequestTwoFactor
+  ) {}
+}
+
+export class ApiLogInCredentials {
+  constructor(public clientId: string, public clientSecret: string) {}
+}
diff --git a/common/src/models/request/identityToken/apiTokenRequest.ts b/common/src/models/request/identityToken/apiTokenRequest.ts
new file mode 100644
index 0000000000..b8f2c21fbd
--- /dev/null
+++ b/common/src/models/request/identityToken/apiTokenRequest.ts
@@ -0,0 +1,24 @@
+import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
+
+import { DeviceRequest } from "../deviceRequest";
+
+export class ApiTokenRequest extends TokenRequest {
+  constructor(
+    public clientId: string,
+    public clientSecret: string,
+    protected twoFactor: TokenRequestTwoFactor,
+    device?: DeviceRequest
+  ) {
+    super(twoFactor, device);
+  }
+
+  toIdentityToken() {
+    const obj = super.toIdentityToken(this.clientId);
+
+    obj.scope = this.clientId.startsWith("organization") ? "api.organization" : "api";
+    obj.grant_type = "client_credentials";
+    obj.client_secret = this.clientSecret;
+
+    return obj;
+  }
+}
diff --git a/common/src/models/request/identityToken/passwordTokenRequest.ts b/common/src/models/request/identityToken/passwordTokenRequest.ts
new file mode 100644
index 0000000000..a1d8466ba6
--- /dev/null
+++ b/common/src/models/request/identityToken/passwordTokenRequest.ts
@@ -0,0 +1,36 @@
+import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
+
+import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
+import { DeviceRequest } from "../deviceRequest";
+
+import { Utils } from "../../../misc/utils";
+
+export class PasswordTokenRequest extends TokenRequest implements CaptchaProtectedRequest {
+  constructor(
+    public email: string,
+    public masterPasswordHash: string,
+    public captchaResponse: string,
+    protected twoFactor: TokenRequestTwoFactor,
+    device?: DeviceRequest
+  ) {
+    super(twoFactor, device);
+  }
+
+  toIdentityToken(clientId: string) {
+    const obj = super.toIdentityToken(clientId);
+
+    obj.grant_type = "password";
+    obj.username = this.email;
+    obj.password = this.masterPasswordHash;
+
+    if (this.captchaResponse != null) {
+      obj.captchaResponse = this.captchaResponse;
+    }
+
+    return obj;
+  }
+
+  alterIdentityTokenHeaders(headers: Headers) {
+    headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
+  }
+}
diff --git a/common/src/models/request/identityToken/ssoTokenRequest.ts b/common/src/models/request/identityToken/ssoTokenRequest.ts
new file mode 100644
index 0000000000..009b8cf843
--- /dev/null
+++ b/common/src/models/request/identityToken/ssoTokenRequest.ts
@@ -0,0 +1,26 @@
+import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
+
+import { DeviceRequest } from "../deviceRequest";
+
+export class SsoTokenRequest extends TokenRequest {
+  constructor(
+    public code: string,
+    public codeVerifier: string,
+    public redirectUri: string,
+    protected twoFactor: TokenRequestTwoFactor,
+    device?: DeviceRequest
+  ) {
+    super(twoFactor, device);
+  }
+
+  toIdentityToken(clientId: string) {
+    const obj = super.toIdentityToken(clientId);
+
+    obj.grant_type = "authorization_code";
+    obj.code = this.code;
+    obj.code_verifier = this.codeVerifier;
+    obj.redirect_uri = this.redirectUri;
+
+    return obj;
+  }
+}
diff --git a/common/src/models/request/identityToken/tokenRequest.ts b/common/src/models/request/identityToken/tokenRequest.ts
new file mode 100644
index 0000000000..4e193cc039
--- /dev/null
+++ b/common/src/models/request/identityToken/tokenRequest.ts
@@ -0,0 +1,48 @@
+import { TwoFactorProviderType } from "../../../enums/twoFactorProviderType";
+
+import { DeviceRequest } from "../deviceRequest";
+
+export interface TokenRequestTwoFactor {
+  provider: TwoFactorProviderType;
+  token: string;
+  remember: boolean;
+}
+
+export abstract class TokenRequest {
+  protected device?: DeviceRequest;
+
+  constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
+    this.device = device != null ? device : null;
+  }
+
+  alterIdentityTokenHeaders(headers: Headers) {
+    // Implemented in subclass if required
+  }
+
+  setTwoFactor(twoFactor: TokenRequestTwoFactor) {
+    this.twoFactor = twoFactor;
+  }
+
+  protected toIdentityToken(clientId: string) {
+    const obj: any = {
+      scope: "api offline_access",
+      client_id: clientId,
+    };
+
+    if (this.device) {
+      obj.deviceType = this.device.type;
+      obj.deviceIdentifier = this.device.identifier;
+      obj.deviceName = this.device.name;
+      // no push tokens for browser apps yet
+      // obj.devicePushToken = this.device.pushToken;
+    }
+
+    if (this.twoFactor.token && this.twoFactor.provider != null) {
+      obj.twoFactorToken = this.twoFactor.token;
+      obj.twoFactorProvider = this.twoFactor.provider;
+      obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
+    }
+
+    return obj;
+  }
+}
diff --git a/common/src/models/request/tokenRequest.ts b/common/src/models/request/tokenRequest.ts
deleted file mode 100644
index 43e344ae91..0000000000
--- a/common/src/models/request/tokenRequest.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
-
-import { CaptchaProtectedRequest } from "./captchaProtectedRequest";
-import { DeviceRequest } from "./deviceRequest";
-
-import { Utils } from "../../misc/utils";
-
-export class TokenRequest implements CaptchaProtectedRequest {
-  email: string;
-  masterPasswordHash: string;
-  code: string;
-  codeVerifier: string;
-  redirectUri: string;
-  clientId: string;
-  clientSecret: string;
-  device?: DeviceRequest;
-
-  constructor(
-    credentials: string[],
-    codes: string[],
-    clientIdClientSecret: string[],
-    public provider: TwoFactorProviderType,
-    public token: string,
-    public remember: boolean,
-    public captchaResponse: string,
-    device?: DeviceRequest
-  ) {
-    if (credentials != null && credentials.length > 1) {
-      this.email = credentials[0];
-      this.masterPasswordHash = credentials[1];
-    } else if (codes != null && codes.length > 2) {
-      this.code = codes[0];
-      this.codeVerifier = codes[1];
-      this.redirectUri = codes[2];
-    } else if (clientIdClientSecret != null && clientIdClientSecret.length > 1) {
-      this.clientId = clientIdClientSecret[0];
-      this.clientSecret = clientIdClientSecret[1];
-    }
-    this.device = device != null ? device : null;
-  }
-
-  toIdentityToken(clientId: string) {
-    const obj: any = {
-      scope: "api offline_access",
-      client_id: clientId,
-    };
-
-    if (this.clientSecret != null) {
-      obj.scope = clientId.startsWith("organization") ? "api.organization" : "api";
-      obj.grant_type = "client_credentials";
-      obj.client_secret = this.clientSecret;
-    } else if (this.masterPasswordHash != null && this.email != null) {
-      obj.grant_type = "password";
-      obj.username = this.email;
-      obj.password = this.masterPasswordHash;
-    } else if (this.code != null && this.codeVerifier != null && this.redirectUri != null) {
-      obj.grant_type = "authorization_code";
-      obj.code = this.code;
-      obj.code_verifier = this.codeVerifier;
-      obj.redirect_uri = this.redirectUri;
-    } else {
-      throw new Error("must provide credentials or codes");
-    }
-
-    if (this.device) {
-      obj.deviceType = this.device.type;
-      obj.deviceIdentifier = this.device.identifier;
-      obj.deviceName = this.device.name;
-      // no push tokens for browser apps yet
-      // obj.devicePushToken = this.device.pushToken;
-    }
-
-    if (this.token && this.provider != null) {
-      obj.twoFactorToken = this.token;
-      obj.twoFactorProvider = this.provider;
-      obj.twoFactorRemember = this.remember ? "1" : "0";
-    }
-
-    if (this.captchaResponse != null) {
-      obj.captchaResponse = this.captchaResponse;
-    }
-
-    return obj;
-  }
-
-  alterIdentityTokenHeaders(headers: Headers) {
-    if (this.clientSecret == null && this.masterPasswordHash != null && this.email != null) {
-      headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
-    }
-  }
-}
diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts
index 16b94b091c..ba2dd046ab 100644
--- a/common/src/services/api.service.ts
+++ b/common/src/services/api.service.ts
@@ -28,6 +28,9 @@ import { EventRequest } from "../models/request/eventRequest";
 import { FolderRequest } from "../models/request/folderRequest";
 import { GroupRequest } from "../models/request/groupRequest";
 import { IapCheckRequest } from "../models/request/iapCheckRequest";
+import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
+import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
+import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
 import { ImportCiphersRequest } from "../models/request/importCiphersRequest";
 import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest";
 import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
@@ -76,7 +79,6 @@ import { SendRequest } from "../models/request/sendRequest";
 import { SetPasswordRequest } from "../models/request/setPasswordRequest";
 import { StorageRequest } from "../models/request/storageRequest";
 import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
-import { TokenRequest } from "../models/request/tokenRequest";
 import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
 import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
 import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
@@ -208,7 +210,7 @@ export class ApiService implements ApiServiceAbstraction {
   // Auth APIs
 
   async postIdentityToken(
-    request: TokenRequest
+    request: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest
   ): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
     const headers = new Headers({
       "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
@@ -219,11 +221,15 @@ export class ApiService implements ApiServiceAbstraction {
       headers.set("User-Agent", this.customUserAgent);
     }
     request.alterIdentityTokenHeaders(headers);
+
+    const identityToken =
+      request instanceof ApiTokenRequest
+        ? request.toIdentityToken()
+        : request.toIdentityToken(this.platformUtilsService.identityClientId);
+
     const response = await this.fetch(
       new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
-        body: this.qsStringify(
-          request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)
-        ),
+        body: this.qsStringify(identityToken),
         credentials: this.getCredentials(),
         cache: "no-store",
         headers: headers,
@@ -244,7 +250,7 @@ export class ApiService implements ApiServiceAbstraction {
         responseJson.TwoFactorProviders2 &&
         Object.keys(responseJson.TwoFactorProviders2).length
       ) {
-        await this.tokenService.clearTwoFactorToken(request.email);
+        await this.tokenService.clearTwoFactorToken();
         return new IdentityTwoFactorResponse(responseJson);
       } else if (
         response.status === 400 &&
diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts
index 3d7506d38a..93f85c4d66 100644
--- a/common/src/services/auth.service.ts
+++ b/common/src/services/auth.service.ts
@@ -1,332 +1,125 @@
-import { HashPurpose } from "../enums/hashPurpose";
 import { KdfType } from "../enums/kdfType";
-import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
 
-import {
-  Account,
-  AccountData,
-  AccountKeys,
-  AccountProfile,
-  AccountTokens,
-} from "../models/domain/account";
+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 { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
-import { DeviceRequest } from "../models/request/deviceRequest";
-import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
-import { KeysRequest } from "../models/request/keysRequest";
 import { PreloginRequest } from "../models/request/preloginRequest";
-import { TokenRequest } from "../models/request/tokenRequest";
 
-import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
-import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
+import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
 
 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 { CryptoFunctionService } from "../abstractions/cryptoFunction.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 { VaultTimeoutService } from "../abstractions/vaultTimeout.service";
-
-import { Utils } from "../misc/utils";
-
-export const TwoFactorProviders = {
-  [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,
-  },
-};
+import { TwoFactorService } from "../abstractions/twoFactor.service";
 
 export class AuthService implements AuthServiceAbstraction {
-  email: string;
-  masterPasswordHash: string;
-  localMasterPasswordHash: string;
-  code: string;
-  codeVerifier: string;
-  ssoRedirectUrl: string;
-  clientId: string;
-  clientSecret: string;
-  twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
-  selectedTwoFactorProviderType: TwoFactorProviderType = null;
-  captchaToken: string;
+  get email(): string {
+    return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
+  }
 
-  private key: SymmetricCryptoKey;
+  get masterPasswordHash(): string {
+    return this.logInStrategy instanceof PasswordLogInStrategy
+      ? this.logInStrategy.masterPasswordHash
+      : null;
+  }
+
+  private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
 
   constructor(
-    private cryptoService: CryptoService,
+    protected cryptoService: CryptoService,
     protected apiService: ApiService,
     protected tokenService: TokenService,
     protected appIdService: AppIdService,
-    private i18nService: I18nService,
     protected platformUtilsService: PlatformUtilsService,
-    private messagingService: MessagingService,
-    private vaultTimeoutService: VaultTimeoutService,
-    private logService: LogService,
-    protected cryptoFunctionService: CryptoFunctionService,
-    private keyConnectorService: KeyConnectorService,
+    protected messagingService: MessagingService,
+    protected logService: LogService,
+    protected keyConnectorService: KeyConnectorService,
     protected environmentService: EnvironmentService,
     protected stateService: StateService,
-    private setCryptoKeys = true
+    protected twoFactorService: TwoFactorService
   ) {}
 
-  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");
-  }
-
-  async logIn(email: string, masterPassword: string, captchaToken?: string): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    const key = await this.makePreloginKey(masterPassword, email);
-    const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
-    const localHashedPassword = await this.cryptoService.hashPassword(
-      masterPassword,
-      key,
-      HashPurpose.LocalAuthorization
-    );
-    return await this.logInHelper(
-      email,
-      hashedPassword,
-      localHashedPassword,
-      null,
-      null,
-      null,
-      null,
-      null,
-      key,
-      null,
-      null,
-      null,
-      captchaToken,
-      null
-    );
-  }
-
-  async logInSso(
-    code: string,
-    codeVerifier: string,
-    redirectUrl: string,
-    orgId: string
+  async logIn(
+    credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
   ): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    return await this.logInHelper(
-      null,
-      null,
-      null,
-      code,
-      codeVerifier,
-      redirectUrl,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      orgId
-    );
+    this.clearState();
+
+    let result: AuthResult;
+    let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
+
+    if (credentials instanceof PasswordLogInCredentials) {
+      strategy = new PasswordLogInStrategy(
+        this.cryptoService,
+        this.apiService,
+        this.tokenService,
+        this.appIdService,
+        this.platformUtilsService,
+        this.messagingService,
+        this.logService,
+        this.stateService,
+        this.twoFactorService,
+        this
+      );
+      result = await strategy.logIn(credentials);
+    } else if (credentials instanceof SsoLogInCredentials) {
+      strategy = new SsoLogInStrategy(
+        this.cryptoService,
+        this.apiService,
+        this.tokenService,
+        this.appIdService,
+        this.platformUtilsService,
+        this.messagingService,
+        this.logService,
+        this.stateService,
+        this.twoFactorService,
+        this.keyConnectorService
+      );
+      result = await strategy.logIn(credentials);
+    } else if (credentials instanceof ApiLogInCredentials) {
+      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
+      );
+      result = await strategy.logIn(credentials);
+    }
+
+    if (result?.requiresTwoFactor) {
+      this.saveState(strategy);
+    }
+    return result;
   }
 
-  async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    return await this.logInHelper(
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      clientId,
-      clientSecret,
-      null,
-      null,
-      null,
-      null,
-      null,
-      null
-    );
-  }
-
-  async logInTwoFactor(
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
-  ): Promise<AuthResult> {
-    return await this.logInHelper(
-      this.email,
-      this.masterPasswordHash,
-      this.localMasterPasswordHash,
-      this.code,
-      this.codeVerifier,
-      this.ssoRedirectUrl,
-      this.clientId,
-      this.clientSecret,
-      this.key,
-      twoFactorProvider,
-      twoFactorToken,
-      remember,
-      this.captchaToken,
-      null
-    );
-  }
-
-  async logInComplete(
-    email: string,
-    masterPassword: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean,
-    captchaToken?: string
-  ): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    const key = await this.makePreloginKey(masterPassword, email);
-    const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
-    const localHashedPassword = await this.cryptoService.hashPassword(
-      masterPassword,
-      key,
-      HashPurpose.LocalAuthorization
-    );
-    return await this.logInHelper(
-      email,
-      hashedPassword,
-      localHashedPassword,
-      null,
-      null,
-      null,
-      null,
-      null,
-      key,
-      twoFactorProvider,
-      twoFactorToken,
-      remember,
-      captchaToken,
-      null
-    );
-  }
-
-  async logInSsoComplete(
-    code: string,
-    codeVerifier: string,
-    redirectUrl: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
-  ): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    return await this.logInHelper(
-      null,
-      null,
-      null,
-      code,
-      codeVerifier,
-      redirectUrl,
-      null,
-      null,
-      null,
-      twoFactorProvider,
-      twoFactorToken,
-      remember,
-      null,
-      null
-    );
-  }
-
-  async logInApiKeyComplete(
-    clientId: string,
-    clientSecret: string,
-    twoFactorProvider: TwoFactorProviderType,
-    twoFactorToken: string,
-    remember?: boolean
-  ): Promise<AuthResult> {
-    this.selectedTwoFactorProviderType = null;
-    return await this.logInHelper(
-      null,
-      null,
-      null,
-      null,
-      null,
-      null,
-      clientId,
-      clientSecret,
-      null,
-      twoFactorProvider,
-      twoFactorToken,
-      remember,
-      null,
-      null
-    );
+  async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
+    try {
+      return await this.logInStrategy.logInTwoFactor(twoFactor);
+    } finally {
+      this.clearState();
+    }
   }
 
   logOut(callback: Function) {
@@ -334,75 +127,16 @@ export class AuthService implements AuthServiceAbstraction {
     this.messagingService.send("loggedOut");
   }
 
-  getSupportedTwoFactorProviders(win: Window): any[] {
-    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;
+  authingWithApiKey(): boolean {
+    return this.logInStrategy instanceof ApiLogInStrategy;
   }
 
-  getDefaultTwoFactorProvider(webAuthnSupported: boolean): TwoFactorProviderType {
-    if (this.twoFactorProvidersData == null) {
-      return null;
-    }
+  authingWithSso(): boolean {
+    return this.logInStrategy instanceof SsoLogInStrategy;
+  }
 
-    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;
+  authingWithPassword(): boolean {
+    return this.logInStrategy instanceof PasswordLogInStrategy;
   }
 
   async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
@@ -423,249 +157,11 @@ export class AuthService implements AuthServiceAbstraction {
     return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
   }
 
-  authingWithApiKey(): boolean {
-    return this.clientId != null && this.clientSecret != null;
+  private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
+    this.logInStrategy = strategy;
   }
 
-  authingWithSso(): boolean {
-    return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null;
-  }
-
-  authingWithPassword(): boolean {
-    return this.email != null && this.masterPasswordHash != null;
-  }
-
-  private async logInHelper(
-    email: string,
-    hashedPassword: string,
-    localHashedPassword: string,
-    code: string,
-    codeVerifier: string,
-    redirectUrl: string,
-    clientId: string,
-    clientSecret: string,
-    key: SymmetricCryptoKey,
-    twoFactorProvider?: TwoFactorProviderType,
-    twoFactorToken?: string,
-    remember?: boolean,
-    captchaToken?: string,
-    orgId?: string
-  ): Promise<AuthResult> {
-    const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email);
-    const appId = await this.appIdService.getAppId();
-    const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
-
-    let emailPassword: string[] = [];
-    let codeCodeVerifier: string[] = [];
-    let clientIdClientSecret: [string, string] = [null, null];
-
-    if (email != null && hashedPassword != null) {
-      emailPassword = [email, hashedPassword];
-    } else {
-      emailPassword = null;
-    }
-    if (code != null && codeVerifier != null && redirectUrl != null) {
-      codeCodeVerifier = [code, codeVerifier, redirectUrl];
-    } else {
-      codeCodeVerifier = null;
-    }
-    if (clientId != null && clientSecret != null) {
-      clientIdClientSecret = [clientId, clientSecret];
-    } else {
-      clientIdClientSecret = null;
-    }
-
-    let request: TokenRequest;
-    if (twoFactorToken != null && twoFactorProvider != null) {
-      request = new TokenRequest(
-        emailPassword,
-        codeCodeVerifier,
-        clientIdClientSecret,
-        twoFactorProvider,
-        twoFactorToken,
-        remember,
-        captchaToken,
-        deviceRequest
-      );
-    } else if (storedTwoFactorToken != null) {
-      request = new TokenRequest(
-        emailPassword,
-        codeCodeVerifier,
-        clientIdClientSecret,
-        TwoFactorProviderType.Remember,
-        storedTwoFactorToken,
-        false,
-        captchaToken,
-        deviceRequest
-      );
-    } else {
-      request = new TokenRequest(
-        emailPassword,
-        codeCodeVerifier,
-        clientIdClientSecret,
-        null,
-        null,
-        false,
-        captchaToken,
-        deviceRequest
-      );
-    }
-
-    const response = await this.apiService.postIdentityToken(request);
-
-    this.clearState();
-    const result = new AuthResult();
-    result.captchaSiteKey = (response as any).siteKey;
-    if (!!result.captchaSiteKey) {
-      return result;
-    }
-    result.twoFactor = !!(response as any).twoFactorProviders2;
-
-    if (result.twoFactor) {
-      // two factor required
-      this.email = email;
-      this.masterPasswordHash = hashedPassword;
-      this.localMasterPasswordHash = localHashedPassword;
-      this.code = code;
-      this.codeVerifier = codeVerifier;
-      this.ssoRedirectUrl = redirectUrl;
-      this.clientId = clientId;
-      this.clientSecret = clientSecret;
-      this.key = this.setCryptoKeys ? key : null;
-      const twoFactorResponse = response as IdentityTwoFactorResponse;
-      this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2;
-      result.twoFactorProviders = twoFactorResponse.twoFactorProviders2;
-      this.captchaToken = twoFactorResponse.captchaToken;
-      return result;
-    }
-
-    const tokenResponse = response as IdentityTokenResponse;
-    result.resetMasterPassword = tokenResponse.resetMasterPassword;
-    result.forcePasswordReset = tokenResponse.forcePasswordReset;
-
-    const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
-    await this.stateService.addAccount(
-      new Account({
-        profile: {
-          ...new AccountProfile(),
-          ...{
-            userId: accountInformation.sub,
-            email: accountInformation.email,
-            apiKeyClientId: clientId,
-            hasPremiumPersonally: accountInformation.premium,
-            kdfIterations: tokenResponse.kdfIterations,
-            kdfType: tokenResponse.kdf,
-          },
-        },
-        keys: {
-          ...new AccountKeys(),
-          ...{
-            apiKeyClientSecret: clientSecret,
-          },
-        },
-        tokens: {
-          ...new AccountTokens(),
-          ...{
-            accessToken: tokenResponse.accessToken,
-            refreshToken: tokenResponse.refreshToken,
-          },
-        },
-      })
-    );
-
-    if (tokenResponse.twoFactorToken != null) {
-      await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email);
-    }
-
-    if (this.setCryptoKeys) {
-      if (key != null) {
-        await this.cryptoService.setKey(key);
-      }
-      if (localHashedPassword != null) {
-        await this.cryptoService.setKeyHash(localHashedPassword);
-      }
-
-      // Skip this step during SSO new user flow. No key is returned from server.
-      if (code == null || tokenResponse.key != null) {
-        if (tokenResponse.keyConnectorUrl != null) {
-          await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
-        } else if (tokenResponse.apiUseKeyConnector) {
-          const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
-          await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
-        }
-
-        await this.cryptoService.setEncKey(tokenResponse.key);
-
-        // User doesn't have a key pair yet (old account), let's generate one for them
-        if (tokenResponse.privateKey == null) {
-          try {
-            const keyPair = await this.cryptoService.makeKeyPair();
-            await this.apiService.postAccountKeys(
-              new KeysRequest(keyPair[0], keyPair[1].encryptedString)
-            );
-            tokenResponse.privateKey = keyPair[1].encryptedString;
-          } catch (e) {
-            this.logService.error(e);
-          }
-        }
-
-        await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey);
-      } else if (tokenResponse.keyConnectorUrl != null) {
-        const password = await this.cryptoFunctionService.randomBytes(64);
-
-        const k = await this.cryptoService.makeKey(
-          Utils.fromBufferToB64(password),
-          await this.tokenService.getEmail(),
-          tokenResponse.kdf,
-          tokenResponse.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(
-            tokenResponse.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,
-          tokenResponse.kdf,
-          tokenResponse.kdfIterations,
-          orgId,
-          keys
-        );
-        await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
-      }
-    }
-
-    if (this.vaultTimeoutService != null) {
-      await this.stateService.setBiometricLocked(false);
-    }
-    this.messagingService.send("loggedIn");
-    return result;
-  }
-
-  private clearState(): void {
-    this.key = null;
-    this.email = null;
-    this.masterPasswordHash = null;
-    this.localMasterPasswordHash = null;
-    this.code = null;
-    this.codeVerifier = null;
-    this.ssoRedirectUrl = null;
-    this.clientId = null;
-    this.clientSecret = null;
-    this.twoFactorProvidersData = null;
-    this.selectedTwoFactorProviderType = null;
+  private clearState() {
+    this.logInStrategy = null;
   }
 }
diff --git a/common/src/services/keyConnector.service.ts b/common/src/services/keyConnector.service.ts
index 7296d9b1f4..847f4862df 100644
--- a/common/src/services/keyConnector.service.ts
+++ b/common/src/services/keyConnector.service.ts
@@ -1,5 +1,6 @@
 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";
@@ -12,7 +13,11 @@ 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(
@@ -21,7 +26,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
     private apiService: ApiService,
     private tokenService: TokenService,
     private logService: LogService,
-    private organizationService: OrganizationService
+    private organizationService: OrganizationService,
+    private cryptoFunctionService: CryptoFunctionService
   ) {}
 
   setUsesKeyConnector(usesKeyConnector: boolean) {
@@ -80,6 +86,41 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
     );
   }
 
+  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);
   }
diff --git a/common/src/services/token.service.ts b/common/src/services/token.service.ts
index bad5ce620b..052d67ae15 100644
--- a/common/src/services/token.service.ts
+++ b/common/src/services/token.service.ts
@@ -3,6 +3,8 @@ import { TokenService as TokenServiceAbstraction } from "../abstractions/token.s
 
 import { Utils } from "../misc/utils";
 
+import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
+
 export class TokenService implements TokenServiceAbstraction {
   constructor(private stateService: StateService) {}
 
@@ -79,8 +81,8 @@ export class TokenService implements TokenServiceAbstraction {
     await this.setClientSecret(clientSecret);
   }
 
-  async setTwoFactorToken(token: string): Promise<any> {
-    return await this.stateService.setTwoFactorToken(token);
+  async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
+    return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
   }
 
   async getTwoFactorToken(): Promise<string> {
diff --git a/common/src/services/twoFactor.service.ts b/common/src/services/twoFactor.service.ts
new file mode 100644
index 0000000000..facd85105a
--- /dev/null
+++ b/common/src/services/twoFactor.service.ts
@@ -0,0 +1,188 @@
+import { I18nService } from "../abstractions/i18n.service";
+import { PlatformUtilsService } from "../abstractions/platformUtils.service";
+import {
+  TwoFactorProviderDetails,
+  TwoFactorService as TwoFactorServiceAbstraction,
+} from "../abstractions/twoFactor.service";
+
+import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
+
+import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
+
+export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
+  {
+    [TwoFactorProviderType.Authenticator]: {
+      type: TwoFactorProviderType.Authenticator,
+      name: null as string,
+      description: null as string,
+      priority: 1,
+      sort: 1,
+      premium: false,
+    },
+    [TwoFactorProviderType.Yubikey]: {
+      type: TwoFactorProviderType.Yubikey,
+      name: null as string,
+      description: null as string,
+      priority: 3,
+      sort: 2,
+      premium: true,
+    },
+    [TwoFactorProviderType.Duo]: {
+      type: TwoFactorProviderType.Duo,
+      name: "Duo",
+      description: null as string,
+      priority: 2,
+      sort: 3,
+      premium: true,
+    },
+    [TwoFactorProviderType.OrganizationDuo]: {
+      type: TwoFactorProviderType.OrganizationDuo,
+      name: "Duo (Organization)",
+      description: null as string,
+      priority: 10,
+      sort: 4,
+      premium: false,
+    },
+    [TwoFactorProviderType.Email]: {
+      type: TwoFactorProviderType.Email,
+      name: null as string,
+      description: null as string,
+      priority: 0,
+      sort: 6,
+      premium: false,
+    },
+    [TwoFactorProviderType.WebAuthn]: {
+      type: TwoFactorProviderType.WebAuthn,
+      name: null as string,
+      description: null as string,
+      priority: 4,
+      sort: 5,
+      premium: true,
+    },
+  };
+
+export class TwoFactorService implements TwoFactorServiceAbstraction {
+  private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
+  private selectedTwoFactorProviderType: TwoFactorProviderType = null;
+
+  constructor(
+    private i18nService: I18nService,
+    private platformUtilsService: PlatformUtilsService
+  ) {}
+
+  init() {
+    TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
+    TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDesc");
+
+    TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
+      this.i18nService.t("authenticatorAppTitle");
+    TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
+      this.i18nService.t("authenticatorAppDesc");
+
+    TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDesc");
+
+    TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
+      "Duo (" + this.i18nService.t("organization") + ")";
+    TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
+      this.i18nService.t("duoOrganizationDesc");
+
+    TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
+    TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
+      this.i18nService.t("webAuthnDesc");
+
+    TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitle");
+    TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
+      this.i18nService.t("yubiKeyDesc");
+  }
+
+  getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
+    const providers: any[] = [];
+    if (this.twoFactorProvidersData == null) {
+      return providers;
+    }
+
+    if (
+      this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
+      this.platformUtilsService.supportsDuo()
+    ) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
+    }
+
+    if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
+    }
+
+    if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
+    }
+
+    if (
+      this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
+      this.platformUtilsService.supportsDuo()
+    ) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
+    }
+
+    if (
+      this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
+      this.platformUtilsService.supportsWebAuthn(win)
+    ) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
+    }
+
+    if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
+      providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
+    }
+
+    return providers;
+  }
+
+  getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
+    if (this.twoFactorProvidersData == null) {
+      return null;
+    }
+
+    if (
+      this.selectedTwoFactorProviderType != null &&
+      this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)
+    ) {
+      return this.selectedTwoFactorProviderType;
+    }
+
+    let providerType: TwoFactorProviderType = null;
+    let providerPriority = -1;
+    this.twoFactorProvidersData.forEach((_value, type) => {
+      const provider = (TwoFactorProviders as any)[type];
+      if (provider != null && provider.priority > providerPriority) {
+        if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
+          return;
+        }
+
+        providerType = type;
+        providerPriority = provider.priority;
+      }
+    });
+
+    return providerType;
+  }
+
+  setSelectedProvider(type: TwoFactorProviderType) {
+    this.selectedTwoFactorProviderType = type;
+  }
+
+  clearSelectedProvider() {
+    this.selectedTwoFactorProviderType = null;
+  }
+
+  setProviders(response: IdentityTwoFactorResponse) {
+    this.twoFactorProvidersData = response.twoFactorProviders2;
+  }
+
+  clearProviders() {
+    this.twoFactorProvidersData = null;
+  }
+
+  getProviders() {
+    return this.twoFactorProvidersData;
+  }
+}
diff --git a/node/src/cli/commands/login.command.ts b/node/src/cli/commands/login.command.ts
index 8dfac8ec85..77eab11e48 100644
--- a/node/src/cli/commands/login.command.ts
+++ b/node/src/cli/commands/login.command.ts
@@ -5,6 +5,11 @@ import * as inquirer from "inquirer";
 import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
 
 import { AuthResult } from "jslib-common/models/domain/authResult";
+import {
+  ApiLogInCredentials,
+  PasswordLogInCredentials,
+  SsoLogInCredentials,
+} from "jslib-common/models/domain/logInCredentials";
 import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
 import { ErrorResponse } from "jslib-common/models/response/errorResponse";
 
@@ -18,6 +23,7 @@ import { PasswordGenerationService } from "jslib-common/abstractions/passwordGen
 import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
 import { PolicyService } from "jslib-common/abstractions/policy.service";
 import { StateService } from "jslib-common/abstractions/state.service";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
 
 import { Response } from "../models/response";
 
@@ -28,6 +34,8 @@ import { MessageResponse } from "../models/response/messageResponse";
 import { NodeUtils } from "jslib-common/misc/nodeUtils";
 import { Utils } from "jslib-common/misc/utils";
 
+import Separator from "inquirer/lib/objects/separator";
+
 // tslint:disable-next-line
 const open = require("open");
 
@@ -53,6 +61,7 @@ export class LoginCommand {
     protected stateService: StateService,
     protected cryptoService: CryptoService,
     protected policyService: PolicyService,
+    protected twoFactorService: TwoFactorService,
     clientId: string
   ) {
     this.clientId = clientId;
@@ -143,163 +152,146 @@ export class LoginCommand {
       return Response.error("Invalid two-step login method.");
     }
 
+    const twoFactor =
+      twoFactorToken == null
+        ? null
+        : {
+            provider: twoFactorMethod,
+            token: twoFactorToken,
+            remember: false,
+          };
+
     try {
       if (this.validatedParams != null) {
         await this.validatedParams();
       }
 
       let response: AuthResult = null;
-      if (twoFactorToken != null && twoFactorMethod != null) {
-        if (clientId != null && clientSecret != null) {
-          response = await this.authService.logInApiKeyComplete(
-            clientId,
-            clientSecret,
-            twoFactorMethod,
-            twoFactorToken,
-            false
-          );
-        } else if (ssoCode != null && ssoCodeVerifier != null) {
-          response = await this.authService.logInSsoComplete(
+      if (clientId != null && clientSecret != null) {
+        response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
+      } else if (ssoCode != null && ssoCodeVerifier != null) {
+        response = await this.authService.logIn(
+          new SsoLogInCredentials(
             ssoCode,
             ssoCodeVerifier,
             this.ssoRedirectUri,
-            twoFactorMethod,
-            twoFactorToken,
-            false
-          );
-        } else {
-          response = await this.authService.logInComplete(
-            email,
-            password,
-            twoFactorMethod,
-            twoFactorToken,
-            false,
-            this.clientSecret
-          );
-        }
+            orgIdentifier,
+            twoFactor
+          )
+        );
       } else {
-        if (clientId != null && clientSecret != null) {
-          response = await this.authService.logInApiKey(clientId, clientSecret);
-        } else if (ssoCode != null && ssoCodeVerifier != null) {
-          response = await this.authService.logInSso(
-            ssoCode,
-            ssoCodeVerifier,
-            this.ssoRedirectUri,
-            orgIdentifier
+        response = await this.authService.logIn(
+          new PasswordLogInCredentials(email, password, null, twoFactor)
+        );
+      }
+      if (response.captchaSiteKey) {
+        const badCaptcha = Response.badRequest(
+          "Your authentication request appears to be coming from a bot\n" +
+            "Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
+            "(https://bitwarden.com/help/article/cli-auth-challenges)"
+        );
+
+        try {
+          const captchaClientSecret = await this.apiClientSecret(true);
+          if (Utils.isNullOrWhitespace(captchaClientSecret)) {
+            return badCaptcha;
+          }
+
+          const secondResponse = await this.authService.logIn(
+            new PasswordLogInCredentials(email, password, captchaClientSecret, {
+              provider: twoFactorMethod,
+              token: twoFactorToken,
+              remember: false,
+            })
           );
-        } else {
-          response = await this.authService.logIn(email, password);
-        }
-        if (response.captchaSiteKey) {
-          const badCaptcha = Response.badRequest(
-            "Your authentication request appears to be coming from a bot\n" +
-              "Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
-              "(https://bitwarden.com/help/article/cli-auth-challenges)"
-          );
-
-          try {
-            const captchaClientSecret = await this.apiClientSecret(true);
-            if (Utils.isNullOrWhitespace(captchaClientSecret)) {
-              return badCaptcha;
-            }
-
-            const secondResponse = await this.authService.logInComplete(
-              email,
-              password,
-              twoFactorMethod,
-              twoFactorToken,
-              false,
-              captchaClientSecret
-            );
-            response = secondResponse;
-          } catch (e) {
-            if (
-              (e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
-              (e as ErrorResponse).message.includes("Captcha is invalid")
-            ) {
-              return badCaptcha;
-            } else {
-              throw e;
-            }
-          }
-        }
-        if (response.twoFactor) {
-          let selectedProvider: any = null;
-          const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null);
-          if (twoFactorProviders.length === 0) {
-            return Response.badRequest("No providers available for this client.");
-          }
-
-          if (twoFactorMethod != null) {
-            try {
-              selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
-            } catch (e) {
-              return Response.error("Invalid two-step login method.");
-            }
-          }
-
-          if (selectedProvider == null) {
-            if (twoFactorProviders.length === 1) {
-              selectedProvider = twoFactorProviders[0];
-            } else if (this.canInteract) {
-              const twoFactorOptions = twoFactorProviders.map((p) => p.name);
-              twoFactorOptions.push(new inquirer.Separator());
-              twoFactorOptions.push("Cancel");
-              const answer: inquirer.Answers = await inquirer.createPromptModule({
-                output: process.stderr,
-              })({
-                type: "list",
-                name: "method",
-                message: "Two-step login method:",
-                choices: twoFactorOptions,
-              });
-              const i = twoFactorOptions.indexOf(answer.method);
-              if (i === twoFactorOptions.length - 1) {
-                return Response.error("Login failed.");
-              }
-              selectedProvider = twoFactorProviders[i];
-            }
-            if (selectedProvider == null) {
-              return Response.error("Login failed. No provider selected.");
-            }
-          }
-
+          response = secondResponse;
+        } catch (e) {
           if (
-            twoFactorToken == null &&
-            response.twoFactorProviders.size > 1 &&
-            selectedProvider.type === TwoFactorProviderType.Email
+            (e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
+            (e as ErrorResponse).message.includes("Captcha is invalid")
           ) {
-            const emailReq = new TwoFactorEmailRequest();
-            emailReq.email = this.authService.email;
-            emailReq.masterPasswordHash = this.authService.masterPasswordHash;
-            await this.apiService.postTwoFactorEmail(emailReq);
+            return badCaptcha;
+          } else {
+            throw e;
           }
-
-          if (twoFactorToken == null) {
-            if (this.canInteract) {
-              const answer: inquirer.Answers = await inquirer.createPromptModule({
-                output: process.stderr,
-              })({
-                type: "input",
-                name: "token",
-                message: "Two-step login code:",
-              });
-              twoFactorToken = answer.token;
-            }
-            if (twoFactorToken == null || twoFactorToken === "") {
-              return Response.badRequest("Code is required.");
-            }
-          }
-
-          response = await this.authService.logInTwoFactor(
-            selectedProvider.type,
-            twoFactorToken,
-            false
-          );
         }
       }
+      if (response.requiresTwoFactor) {
+        let selectedProvider: any = null;
+        const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
+        if (twoFactorProviders.length === 0) {
+          return Response.badRequest("No providers available for this client.");
+        }
 
-      if (response.twoFactor) {
+        if (twoFactorMethod != null) {
+          try {
+            selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
+          } catch (e) {
+            return Response.error("Invalid two-step login method.");
+          }
+        }
+
+        if (selectedProvider == null) {
+          if (twoFactorProviders.length === 1) {
+            selectedProvider = twoFactorProviders[0];
+          } else if (this.canInteract) {
+            const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
+            twoFactorOptions.push(new inquirer.Separator());
+            twoFactorOptions.push("Cancel");
+            const answer: inquirer.Answers = await inquirer.createPromptModule({
+              output: process.stderr,
+            })({
+              type: "list",
+              name: "method",
+              message: "Two-step login method:",
+              choices: twoFactorOptions,
+            });
+            const i = twoFactorOptions.indexOf(answer.method);
+            if (i === twoFactorOptions.length - 1) {
+              return Response.error("Login failed.");
+            }
+            selectedProvider = twoFactorProviders[i];
+          }
+          if (selectedProvider == null) {
+            return Response.error("Login failed. No provider selected.");
+          }
+        }
+
+        if (
+          twoFactorToken == null &&
+          response.twoFactorProviders.size > 1 &&
+          selectedProvider.type === TwoFactorProviderType.Email
+        ) {
+          const emailReq = new TwoFactorEmailRequest();
+          emailReq.email = this.authService.email;
+          emailReq.masterPasswordHash = this.authService.masterPasswordHash;
+          await this.apiService.postTwoFactorEmail(emailReq);
+        }
+
+        if (twoFactorToken == null) {
+          if (this.canInteract) {
+            const answer: inquirer.Answers = await inquirer.createPromptModule({
+              output: process.stderr,
+            })({
+              type: "input",
+              name: "token",
+              message: "Two-step login code:",
+            });
+            twoFactorToken = answer.token;
+          }
+          if (twoFactorToken == null || twoFactorToken === "") {
+            return Response.badRequest("Code is required.");
+          }
+        }
+
+        response = await this.authService.logInTwoFactor({
+          provider: selectedProvider.type,
+          token: twoFactorToken,
+          remember: false,
+        });
+      }
+
+      if (response.requiresTwoFactor) {
         return Response.error("Login failed.");
       }
 
diff --git a/spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts b/spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts
new file mode 100644
index 0000000000..6ce8b4c9e2
--- /dev/null
+++ b/spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts
@@ -0,0 +1,116 @@
+import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
+
+import { ApiService } from "jslib-common/abstractions/api.service";
+import { AppIdService } from "jslib-common/abstractions/appId.service";
+import { CryptoService } from "jslib-common/abstractions/crypto.service";
+import { EnvironmentService } from "jslib-common/abstractions/environment.service";
+import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
+import { LogService } from "jslib-common/abstractions/log.service";
+import { MessagingService } from "jslib-common/abstractions/messaging.service";
+import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
+import { StateService } from "jslib-common/abstractions/state.service";
+import { TokenService } from "jslib-common/abstractions/token.service";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
+
+import { ApiLogInStrategy } from "jslib-common/misc/logInStrategies/apiLogin.strategy";
+import { Utils } from "jslib-common/misc/utils";
+
+import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
+
+import { identityTokenResponseFactory } from "./logIn.strategy.spec";
+
+describe("ApiLogInStrategy", () => {
+  let cryptoService: SubstituteOf<CryptoService>;
+  let apiService: SubstituteOf<ApiService>;
+  let tokenService: SubstituteOf<TokenService>;
+  let appIdService: SubstituteOf<AppIdService>;
+  let platformUtilsService: SubstituteOf<PlatformUtilsService>;
+  let messagingService: SubstituteOf<MessagingService>;
+  let logService: SubstituteOf<LogService>;
+  let environmentService: SubstituteOf<EnvironmentService>;
+  let keyConnectorService: SubstituteOf<KeyConnectorService>;
+  let stateService: SubstituteOf<StateService>;
+  let twoFactorService: SubstituteOf<TwoFactorService>;
+
+  let apiLogInStrategy: ApiLogInStrategy;
+  let credentials: ApiLogInCredentials;
+
+  const deviceId = Utils.newGuid();
+  const keyConnectorUrl = "KEY_CONNECTOR_URL";
+  const apiClientId = "API_CLIENT_ID";
+  const apiClientSecret = "API_CLIENT_SECRET";
+
+  beforeEach(async () => {
+    cryptoService = Substitute.for<CryptoService>();
+    apiService = Substitute.for<ApiService>();
+    tokenService = Substitute.for<TokenService>();
+    appIdService = Substitute.for<AppIdService>();
+    platformUtilsService = Substitute.for<PlatformUtilsService>();
+    messagingService = Substitute.for<MessagingService>();
+    logService = Substitute.for<LogService>();
+    environmentService = Substitute.for<EnvironmentService>();
+    stateService = Substitute.for<StateService>();
+    keyConnectorService = Substitute.for<KeyConnectorService>();
+    twoFactorService = Substitute.for<TwoFactorService>();
+
+    appIdService.getAppId().resolves(deviceId);
+    tokenService.getTwoFactorToken().resolves(null);
+
+    apiLogInStrategy = new ApiLogInStrategy(
+      cryptoService,
+      apiService,
+      tokenService,
+      appIdService,
+      platformUtilsService,
+      messagingService,
+      logService,
+      stateService,
+      twoFactorService,
+      environmentService,
+      keyConnectorService
+    );
+
+    credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
+  });
+
+  it("sends api key credentials to the server", async () => {
+    apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+    await apiLogInStrategy.logIn(credentials);
+
+    apiService.received(1).postIdentityToken(
+      Arg.is((actual) => {
+        const apiTokenRequest = actual as any;
+        return (
+          apiTokenRequest.clientId === apiClientId &&
+          apiTokenRequest.clientSecret === apiClientSecret &&
+          apiTokenRequest.device.identifier === deviceId &&
+          apiTokenRequest.twoFactor.provider == null &&
+          apiTokenRequest.twoFactor.token == null &&
+          apiTokenRequest.captchaResponse == null
+        );
+      })
+    );
+  });
+
+  it("sets the local environment after a successful login", async () => {
+    apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+
+    await apiLogInStrategy.logIn(credentials);
+
+    stateService.received(1).setApiKeyClientId(apiClientId);
+    stateService.received(1).setApiKeyClientSecret(apiClientSecret);
+    stateService.received(1).addAccount(Arg.any());
+  });
+
+  it("gets and sets the Key Connector key from environmentUrl", async () => {
+    const tokenResponse = identityTokenResponseFactory();
+    tokenResponse.apiUseKeyConnector = true;
+
+    apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+    environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
+
+    await apiLogInStrategy.logIn(credentials);
+
+    keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
+  });
+});
diff --git a/spec/common/misc/logInStrategies/logIn.strategy.spec.ts b/spec/common/misc/logInStrategies/logIn.strategy.spec.ts
new file mode 100644
index 0000000000..c3d97f3dbd
--- /dev/null
+++ b/spec/common/misc/logInStrategies/logIn.strategy.spec.ts
@@ -0,0 +1,293 @@
+import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
+
+import { ApiService } from "jslib-common/abstractions/api.service";
+import { AppIdService } from "jslib-common/abstractions/appId.service";
+import { AuthService } from "jslib-common/abstractions/auth.service";
+import { CryptoService } from "jslib-common/abstractions/crypto.service";
+import { LogService } from "jslib-common/abstractions/log.service";
+import { MessagingService } from "jslib-common/abstractions/messaging.service";
+import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
+import { StateService } from "jslib-common/abstractions/state.service";
+import { TokenService } from "jslib-common/abstractions/token.service";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
+
+import { PasswordLogInStrategy } from "jslib-common/misc/logInStrategies/passwordLogin.strategy";
+import { Utils } from "jslib-common/misc/utils";
+
+import { Account, AccountProfile, AccountTokens } from "jslib-common/models/domain/account";
+import { AuthResult } from "jslib-common/models/domain/authResult";
+import { EncString } from "jslib-common/models/domain/encString";
+import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
+
+import { PasswordTokenRequest } from "jslib-common/models/request/identityToken/passwordTokenRequest";
+
+import { IdentityCaptchaResponse } from "jslib-common/models/response/identityCaptchaResponse";
+import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
+import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse";
+
+import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
+
+const email = "hello@world.com";
+const masterPassword = "password";
+
+const deviceId = Utils.newGuid();
+const accessToken = "ACCESS_TOKEN";
+const refreshToken = "REFRESH_TOKEN";
+const encKey = "ENC_KEY";
+const privateKey = "PRIVATE_KEY";
+const captchaSiteKey = "CAPTCHA_SITE_KEY";
+const kdf = 0;
+const kdfIterations = 10000;
+const userId = Utils.newGuid();
+const masterPasswordHash = "MASTER_PASSWORD_HASH";
+
+const decodedToken = {
+  sub: userId,
+  email: email,
+  premium: false,
+};
+
+const twoFactorProviderType = TwoFactorProviderType.Authenticator;
+const twoFactorToken = "TWO_FACTOR_TOKEN";
+const twoFactorRemember = true;
+
+export function identityTokenResponseFactory() {
+  return new IdentityTokenResponse({
+    ForcePasswordReset: false,
+    Kdf: kdf,
+    KdfIterations: kdfIterations,
+    Key: encKey,
+    PrivateKey: privateKey,
+    ResetMasterPassword: false,
+    access_token: accessToken,
+    expires_in: 3600,
+    refresh_token: refreshToken,
+    scope: "api offline_access",
+    token_type: "Bearer",
+  });
+}
+
+describe("LogInStrategy", () => {
+  let cryptoService: SubstituteOf<CryptoService>;
+  let apiService: SubstituteOf<ApiService>;
+  let tokenService: SubstituteOf<TokenService>;
+  let appIdService: SubstituteOf<AppIdService>;
+  let platformUtilsService: SubstituteOf<PlatformUtilsService>;
+  let messagingService: SubstituteOf<MessagingService>;
+  let logService: SubstituteOf<LogService>;
+  let stateService: SubstituteOf<StateService>;
+  let twoFactorService: SubstituteOf<TwoFactorService>;
+  let authService: SubstituteOf<AuthService>;
+
+  let passwordLogInStrategy: PasswordLogInStrategy;
+  let credentials: PasswordLogInCredentials;
+
+  beforeEach(async () => {
+    cryptoService = Substitute.for<CryptoService>();
+    apiService = Substitute.for<ApiService>();
+    tokenService = Substitute.for<TokenService>();
+    appIdService = Substitute.for<AppIdService>();
+    platformUtilsService = Substitute.for<PlatformUtilsService>();
+    messagingService = Substitute.for<MessagingService>();
+    logService = Substitute.for<LogService>();
+    stateService = Substitute.for<StateService>();
+    twoFactorService = Substitute.for<TwoFactorService>();
+    authService = Substitute.for<AuthService>();
+
+    appIdService.getAppId().resolves(deviceId);
+
+    // The base class is abstract so we test it via PasswordLogInStrategy
+    passwordLogInStrategy = new PasswordLogInStrategy(
+      cryptoService,
+      apiService,
+      tokenService,
+      appIdService,
+      platformUtilsService,
+      messagingService,
+      logService,
+      stateService,
+      twoFactorService,
+      authService
+    );
+    credentials = new PasswordLogInCredentials(email, masterPassword);
+  });
+
+  describe("base class", () => {
+    it("sets the local environment after a successful login", async () => {
+      apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+      tokenService.decodeToken(accessToken).resolves(decodedToken);
+
+      await passwordLogInStrategy.logIn(credentials);
+
+      stateService.received(1).addAccount(
+        new Account({
+          profile: {
+            ...new AccountProfile(),
+            ...{
+              userId: userId,
+              email: email,
+              hasPremiumPersonally: false,
+              kdfIterations: kdfIterations,
+              kdfType: kdf,
+            },
+          },
+          tokens: {
+            ...new AccountTokens(),
+            ...{
+              accessToken: accessToken,
+              refreshToken: refreshToken,
+            },
+          },
+        })
+      );
+      cryptoService.received(1).setEncKey(encKey);
+      cryptoService.received(1).setEncPrivateKey(privateKey);
+
+      stateService.received(1).setBiometricLocked(false);
+      messagingService.received(1).send("loggedIn");
+    });
+
+    it("builds AuthResult", async () => {
+      const tokenResponse = identityTokenResponseFactory();
+      tokenResponse.forcePasswordReset = true;
+      tokenResponse.resetMasterPassword = true;
+
+      apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+      const result = await passwordLogInStrategy.logIn(credentials);
+
+      const expected = new AuthResult();
+      expected.forcePasswordReset = true;
+      expected.resetMasterPassword = true;
+      expected.twoFactorProviders = null;
+      expected.captchaSiteKey = "";
+      expect(result).toEqual(expected);
+    });
+
+    it("rejects login if CAPTCHA is required", async () => {
+      // Sample CAPTCHA response
+      const tokenResponse = new IdentityCaptchaResponse({
+        error: "invalid_grant",
+        error_description: "Captcha required.",
+        HCaptcha_SiteKey: captchaSiteKey,
+      });
+
+      apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+      const result = await passwordLogInStrategy.logIn(credentials);
+
+      stateService.didNotReceive().addAccount(Arg.any());
+      messagingService.didNotReceive().send(Arg.any());
+
+      const expected = new AuthResult();
+      expected.captchaSiteKey = captchaSiteKey;
+      expect(result).toEqual(expected);
+    });
+
+    it("makes a new public and private key for an old account", async () => {
+      const tokenResponse = identityTokenResponseFactory();
+      tokenResponse.privateKey = null;
+      cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
+
+      apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+      await passwordLogInStrategy.logIn(credentials);
+
+      apiService.received(1).postAccountKeys(Arg.any());
+    });
+  });
+
+  describe("Two-factor authentication", () => {
+    it("rejects login if 2FA is required", async () => {
+      // Sample response where TOTP 2FA required
+      const tokenResponse = new IdentityTwoFactorResponse({
+        TwoFactorProviders: ["0"],
+        TwoFactorProviders2: { 0: null },
+        error: "invalid_grant",
+        error_description: "Two factor required.",
+      });
+
+      apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+      const result = await passwordLogInStrategy.logIn(credentials);
+
+      stateService.didNotReceive().addAccount(Arg.any());
+      messagingService.didNotReceive().send(Arg.any());
+
+      const expected = new AuthResult();
+      expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
+      expected.twoFactorProviders.set(0, null);
+      expect(result).toEqual(expected);
+    });
+
+    it("sends stored 2FA token to server", async () => {
+      tokenService.getTwoFactorToken().resolves(twoFactorToken);
+      apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+
+      await passwordLogInStrategy.logIn(credentials);
+
+      apiService.received(1).postIdentityToken(
+        Arg.is((actual) => {
+          const passwordTokenRequest = actual as any;
+          return (
+            passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
+            passwordTokenRequest.twoFactor.token === twoFactorToken &&
+            passwordTokenRequest.twoFactor.remember === false
+          );
+        })
+      );
+    });
+
+    it("sends 2FA token provided by user to server (single step)", async () => {
+      // This occurs if the user enters the 2FA code as an argument in the CLI
+      apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+      credentials.twoFactor = {
+        provider: twoFactorProviderType,
+        token: twoFactorToken,
+        remember: 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({
+        provider: twoFactorProviderType,
+        token: twoFactorToken,
+        remember: twoFactorRemember,
+      });
+
+      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/spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts b/spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts
new file mode 100644
index 0000000000..4d4aa1dda9
--- /dev/null
+++ b/spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts
@@ -0,0 +1,113 @@
+import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
+
+import { ApiService } from "jslib-common/abstractions/api.service";
+import { AppIdService } from "jslib-common/abstractions/appId.service";
+import { AuthService } from "jslib-common/abstractions/auth.service";
+import { CryptoService } from "jslib-common/abstractions/crypto.service";
+import { LogService } from "jslib-common/abstractions/log.service";
+import { MessagingService } from "jslib-common/abstractions/messaging.service";
+import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
+import { StateService } from "jslib-common/abstractions/state.service";
+import { TokenService } from "jslib-common/abstractions/token.service";
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
+
+import { PasswordLogInStrategy } from "jslib-common/misc/logInStrategies/passwordLogin.strategy";
+import { Utils } from "jslib-common/misc/utils";
+
+import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
+import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
+
+import { HashPurpose } from "jslib-common/enums/hashPurpose";
+
+import { identityTokenResponseFactory } from "./logIn.strategy.spec";
+
+const email = "hello@world.com";
+const masterPassword = "password";
+const hashedPassword = "HASHED_PASSWORD";
+const localHashedPassword = "LOCAL_HASHED_PASSWORD";
+const preloginKey = new SymmetricCryptoKey(
+  Utils.fromB64ToArray(
+    "N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
+  )
+);
+const deviceId = Utils.newGuid();
+
+describe("PasswordLogInStrategy", () => {
+  let cryptoService: SubstituteOf<CryptoService>;
+  let apiService: SubstituteOf<ApiService>;
+  let tokenService: SubstituteOf<TokenService>;
+  let appIdService: SubstituteOf<AppIdService>;
+  let platformUtilsService: SubstituteOf<PlatformUtilsService>;
+  let messagingService: SubstituteOf<MessagingService>;
+  let logService: SubstituteOf<LogService>;
+  let stateService: SubstituteOf<StateService>;
+  let twoFactorService: SubstituteOf<TwoFactorService>;
+  let authService: SubstituteOf<AuthService>;
+
+  let passwordLogInStrategy: PasswordLogInStrategy;
+  let credentials: PasswordLogInCredentials;
+
+  beforeEach(async () => {
+    cryptoService = Substitute.for<CryptoService>();
+    apiService = Substitute.for<ApiService>();
+    tokenService = Substitute.for<TokenService>();
+    appIdService = Substitute.for<AppIdService>();
+    platformUtilsService = Substitute.for<PlatformUtilsService>();
+    messagingService = Substitute.for<MessagingService>();
+    logService = Substitute.for<LogService>();
+    stateService = Substitute.for<StateService>();
+    twoFactorService = Substitute.for<TwoFactorService>();
+    authService = Substitute.for<AuthService>();
+
+    appIdService.getAppId().resolves(deviceId);
+    tokenService.getTwoFactorToken().resolves(null);
+
+    authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
+
+    cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
+    cryptoService
+      .hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
+      .resolves(localHashedPassword);
+
+    passwordLogInStrategy = new PasswordLogInStrategy(
+      cryptoService,
+      apiService,
+      tokenService,
+      appIdService,
+      platformUtilsService,
+      messagingService,
+      logService,
+      stateService,
+      twoFactorService,
+      authService
+    );
+    credentials = new PasswordLogInCredentials(email, masterPassword);
+
+    apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+  });
+
+  it("sends master password credentials to the server", async () => {
+    await passwordLogInStrategy.logIn(credentials);
+
+    apiService.received(1).postIdentityToken(
+      Arg.is((actual) => {
+        const passwordTokenRequest = actual as any; // Need to access private fields
+        return (
+          passwordTokenRequest.email === email &&
+          passwordTokenRequest.masterPasswordHash === hashedPassword &&
+          passwordTokenRequest.device.identifier === deviceId &&
+          passwordTokenRequest.twoFactor.provider == null &&
+          passwordTokenRequest.twoFactor.token == null &&
+          passwordTokenRequest.captchaResponse == null
+        );
+      })
+    );
+  });
+
+  it("sets the local environment after a successful login", async () => {
+    await passwordLogInStrategy.logIn(credentials);
+
+    cryptoService.received(1).setKey(preloginKey);
+    cryptoService.received(1).setKeyHash(localHashedPassword);
+  });
+});
diff --git a/spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts b/spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts
new file mode 100644
index 0000000000..fdb20330a4
--- /dev/null
+++ b/spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts
@@ -0,0 +1,130 @@
+import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
+
+import { ApiService } from "jslib-common/abstractions/api.service";
+import { AppIdService } from "jslib-common/abstractions/appId.service";
+import { CryptoService } from "jslib-common/abstractions/crypto.service";
+import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
+import { LogService } from "jslib-common/abstractions/log.service";
+import { MessagingService } from "jslib-common/abstractions/messaging.service";
+import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
+import { StateService } from "jslib-common/abstractions/state.service";
+import { TokenService } from "jslib-common/abstractions/token.service";
+
+import { SsoLogInStrategy } from "jslib-common/misc/logInStrategies/ssoLogin.strategy";
+import { Utils } from "jslib-common/misc/utils";
+
+import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
+
+import { identityTokenResponseFactory } from "./logIn.strategy.spec";
+
+import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
+
+describe("SsoLogInStrategy", () => {
+  let cryptoService: SubstituteOf<CryptoService>;
+  let apiService: SubstituteOf<ApiService>;
+  let tokenService: SubstituteOf<TokenService>;
+  let appIdService: SubstituteOf<AppIdService>;
+  let platformUtilsService: SubstituteOf<PlatformUtilsService>;
+  let messagingService: SubstituteOf<MessagingService>;
+  let logService: SubstituteOf<LogService>;
+  let keyConnectorService: SubstituteOf<KeyConnectorService>;
+  let stateService: SubstituteOf<StateService>;
+  let twoFactorService: SubstituteOf<TwoFactorService>;
+
+  let ssoLogInStrategy: SsoLogInStrategy;
+  let credentials: SsoLogInCredentials;
+
+  const deviceId = Utils.newGuid();
+  const encKey = "ENC_KEY";
+  const privateKey = "PRIVATE_KEY";
+  const keyConnectorUrl = "KEY_CONNECTOR_URL";
+
+  const ssoCode = "SSO_CODE";
+  const ssoCodeVerifier = "SSO_CODE_VERIFIER";
+  const ssoRedirectUrl = "SSO_REDIRECT_URL";
+  const ssoOrgId = "SSO_ORG_ID";
+
+  beforeEach(async () => {
+    cryptoService = Substitute.for<CryptoService>();
+    apiService = Substitute.for<ApiService>();
+    tokenService = Substitute.for<TokenService>();
+    appIdService = Substitute.for<AppIdService>();
+    platformUtilsService = Substitute.for<PlatformUtilsService>();
+    messagingService = Substitute.for<MessagingService>();
+    logService = Substitute.for<LogService>();
+    stateService = Substitute.for<StateService>();
+    keyConnectorService = Substitute.for<KeyConnectorService>();
+    twoFactorService = Substitute.for<TwoFactorService>();
+
+    tokenService.getTwoFactorToken().resolves(null);
+    appIdService.getAppId().resolves(deviceId);
+
+    ssoLogInStrategy = new SsoLogInStrategy(
+      cryptoService,
+      apiService,
+      tokenService,
+      appIdService,
+      platformUtilsService,
+      messagingService,
+      logService,
+      stateService,
+      twoFactorService,
+      keyConnectorService
+    );
+    credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
+  });
+
+  it("sends SSO information to server", async () => {
+    apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
+
+    await ssoLogInStrategy.logIn(credentials);
+
+    apiService.received(1).postIdentityToken(
+      Arg.is((actual) => {
+        const ssoTokenRequest = actual as any;
+        return (
+          ssoTokenRequest.code === ssoCode &&
+          ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
+          ssoTokenRequest.redirectUri === ssoRedirectUrl &&
+          ssoTokenRequest.device.identifier === deviceId &&
+          ssoTokenRequest.twoFactor.provider == null &&
+          ssoTokenRequest.twoFactor.token == null
+        );
+      })
+    );
+  });
+
+  it("does not set keys for new SSO user flow", async () => {
+    const tokenResponse = identityTokenResponseFactory();
+    tokenResponse.key = null;
+    apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+    await ssoLogInStrategy.logIn(credentials);
+
+    cryptoService.didNotReceive().setEncPrivateKey(privateKey);
+    cryptoService.didNotReceive().setEncKey(encKey);
+  });
+
+  it("gets and sets KeyConnector key for enrolled user", async () => {
+    const tokenResponse = identityTokenResponseFactory();
+    tokenResponse.keyConnectorUrl = keyConnectorUrl;
+
+    apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+    await ssoLogInStrategy.logIn(credentials);
+
+    keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
+  });
+
+  it("converts new SSO user to Key Connector on first login", async () => {
+    const tokenResponse = identityTokenResponseFactory();
+    tokenResponse.keyConnectorUrl = keyConnectorUrl;
+    tokenResponse.key = null;
+
+    apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
+
+    await ssoLogInStrategy.logIn(credentials);
+
+    keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
+  });
+});