diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index d8859318b520..89d774b443b3 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -13,6 +13,7 @@ import { SsoLoginCredentials, SsoUrlService, UserApiLoginCredentials, + UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -82,6 +83,7 @@ export class LoginCommand { protected ssoUrlService: SsoUrlService, protected i18nService: I18nService, protected masterPasswordService: MasterPasswordServiceAbstraction, + protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected encryptedMigrator: EncryptedMigrator, ) {} @@ -361,11 +363,13 @@ export class LoginCommand { return Response.error("Login failed."); } - if (response.resetMasterPassword) { - return Response.error( - "In order to log in with SSO from the CLI, you must first log in" + - " through the web vault to set your master password.", - ); + // If we are in the SSO flow and we got a successful login response (we are past rejection scenarios + // and should always have a userId here), validate that SSO user in MP encryption org has MP set + // This must be done here b/c we have 2 places we try to login with SSO above and neither has a + // common handleSsoAuthnResult method to consoldiate this logic into (1. the normal SSO flow and + // 2. the requiresSso automatic authentication flow) + if (ssoCode != null && ssoCodeVerifier != null && response.userId) { + await this.validateSsoUserInMpEncryptionOrgHasMp(response.userId); } // Check if Key Connector domain confirmation is required @@ -836,4 +840,35 @@ export class LoginCommand { const checkStateSplit = checkState.split("_identifier="); return stateSplit[0] === checkStateSplit[0]; } + + /** + * Validate that a user logging in with SSO that is in an org using MP encryption + * has a MP set. If not, they cannot set a MP in the CLI and must use another client. + * @param userId + * @returns void + */ + private async validateSsoUserInMpEncryptionOrgHasMp(userId: UserId): Promise { + const userDecryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); + + // device trust isn't supported in the CLI as we don't have persistent device key storage. + const notUsingTrustedDeviceEncryption = !userDecryptionOptions.trustedDeviceOption; + const notUsingKeyConnector = !userDecryptionOptions.keyConnectorOption; + + if ( + notUsingTrustedDeviceEncryption && + notUsingKeyConnector && + !userDecryptionOptions.hasMasterPassword + ) { + // If user is in an org that is using MP encryption and they JIT provisioned but + // have not yet set a MP and come to the CLI to login, they won't be able to unlock + // or set a MP in the CLI as it isn't supported. + await this.logoutCallback(); + throw Response.error( + "In order to log in with SSO from the CLI, you must first log in" + + " through the web vault, the desktop, or the extension to set your master password.", + ); + } + } } diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index 3e5b56786296..8065728e6ff7 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -195,6 +195,7 @@ export class Program extends BaseProgram { this.serviceContainer.ssoUrlService, this.serviceContainer.i18nService, this.serviceContainer.masterPasswordService, + this.serviceContainer.userDecryptionOptionsService, this.serviceContainer.encryptedMigrator, ); const response = await command.run(email, password, options); diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index d0cc2bd83e5c..f5167cb84cc4 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -478,7 +478,7 @@ export class SsoComponent implements OnInit { !userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined; - if (requireSetPassword || authResult.resetMasterPassword) { + if (requireSetPassword) { // Change implies going no password -> password in this case return await this.handleChangePasswordRequired(orgSsoIdentifier); } diff --git a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts index 4c143cc59f9f..91d24532a70f 100644 --- a/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts +++ b/libs/auth/src/angular/two-factor-auth/two-factor-auth.component.ts @@ -487,7 +487,7 @@ export class TwoFactorAuthComponent implements OnInit, OnDestroy { !userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined; // New users without a master password must set a master password before advancing. - if (requireSetPassword || authResult.resetMasterPassword) { + if (requireSetPassword) { // Change implies going no password -> password in this case return await this.handleChangePasswordRequired(this.orgSsoIdentifier); } diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 82e1183a1d6a..4bf72b69e1ff 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -101,7 +101,6 @@ export function identityTokenResponseFactory( KdfIterations: kdfIterations, Key: encryptedUserKey, PrivateKey: privateKey, - ResetMasterPassword: false, access_token: accessToken, expires_in: 3600, refresh_token: refreshToken, @@ -301,7 +300,6 @@ describe("LoginStrategy", () => { it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; - tokenResponse.resetMasterPassword = true; apiService.postIdentityToken.mockResolvedValue(tokenResponse); @@ -310,7 +308,6 @@ describe("LoginStrategy", () => { const expected = new AuthResult(); expected.masterPassword = "password"; expected.userId = userId; - expected.resetMasterPassword = true; expected.twoFactorProviders = null; expect(result).toEqual(expected); }); @@ -326,7 +323,6 @@ describe("LoginStrategy", () => { const expected = new AuthResult(); expected.masterPassword = "password"; expected.userId = userId; - expected.resetMasterPassword = false; expected.twoFactorProviders = null; expect(result).toEqual(expected); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index ae375c8b2f50..363bc1903193 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -253,8 +253,6 @@ export abstract class LoginStrategy { const userId = await this.saveAccountInformation(response); result.userId = userId; - result.resetMasterPassword = response.resetMasterPassword; - if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation const userEmail = await this.tokenService.getEmail(); diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 26ae1270f392..2287bfb43e3c 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -390,7 +390,6 @@ describe("PasswordLoginStrategy", () => { newDeviceOtp: deviceVerificationOtp, }), ); - expect(result.resetMasterPassword).toBe(false); expect(result.userId).toBe(userId); }); }); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 53bc1c57905a..9d664a7837cc 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -212,7 +212,6 @@ describe("WebAuthnLoginStrategy", () => { expect(authResult).toBeInstanceOf(AuthResult); expect(authResult).toMatchObject({ - resetMasterPassword: false, twoFactorProviders: null, requiresTwoFactor: false, }); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 20251e339a57..c7e18105a0d0 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -491,7 +491,6 @@ describe("LoginStrategyService", () => { KdfParallelism: 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", - ResetMasterPassword: false, access_token: "ACCESS_TOKEN", expires_in: 3600, refresh_token: "REFRESH_TOKEN", @@ -559,7 +558,6 @@ describe("LoginStrategyService", () => { KdfParallelism: 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", - ResetMasterPassword: false, access_token: "ACCESS_TOKEN", expires_in: 3600, refresh_token: "REFRESH_TOKEN", @@ -625,7 +623,6 @@ describe("LoginStrategyService", () => { KdfIterations: PBKDF2KdfConfig.PRELOGIN_ITERATIONS_MIN - 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", - ResetMasterPassword: false, access_token: "ACCESS_TOKEN", expires_in: 3600, refresh_token: "REFRESH_TOKEN", @@ -689,7 +686,6 @@ describe("LoginStrategyService", () => { KdfParallelism: 1, Key: "KEY", PrivateKey: "PRIVATE_KEY", - ResetMasterPassword: false, access_token: "ACCESS_TOKEN", expires_in: 3600, refresh_token: "REFRESH_TOKEN", diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 178866901d33..34467a18f2c2 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -7,14 +7,6 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; export class AuthResult { userId: UserId; - // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal - /** - * @deprecated - * Replace with using UserDecryptionOptions to determine if the user does - * not have a master password and is not using Key Connector. - * */ - resetMasterPassword = false; - twoFactorProviders: Partial>> = null; ssoEmail2FaSessionToken?: string; email: string; diff --git a/libs/common/src/auth/models/response/identity-token.response.ts b/libs/common/src/auth/models/response/identity-token.response.ts index 59e7eb98ec25..958f520a2c65 100644 --- a/libs/common/src/auth/models/response/identity-token.response.ts +++ b/libs/common/src/auth/models/response/identity-token.response.ts @@ -18,7 +18,6 @@ export class IdentityTokenResponse extends BaseResponse { tokenType: string; // Decryption Information - resetMasterPassword: boolean; privateKey: string; // userKeyEncryptedPrivateKey key?: EncString; // masterKeyEncryptedUserKey twoFactorToken: string; @@ -52,7 +51,6 @@ export class IdentityTokenResponse extends BaseResponse { this.refreshToken = refreshToken; } - this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword"); this.privateKey = this.getResponseProperty("PrivateKey"); const key = this.getResponseProperty("Key"); if (key) {