Skip to content
45 changes: 40 additions & 5 deletions apps/cli/src/auth/commands/login.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -82,6 +83,7 @@ export class LoginCommand {
protected ssoUrlService: SsoUrlService,
protected i18nService: I18nService,
protected masterPasswordService: MasterPasswordServiceAbstraction,
protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
protected encryptedMigrator: EncryptedMigrator,
) {}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
private async validateSsoUserInMpEncryptionOrgHasMp(userId: UserId): Promise<void> {
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.",
);
}
}
}
1 change: 1 addition & 0 deletions apps/cli/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion libs/auth/src/angular/sso/sso.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export function identityTokenResponseFactory(
KdfIterations: kdfIterations,
Key: encryptedUserKey,
PrivateKey: privateKey,
ResetMasterPassword: false,
access_token: accessToken,
expires_in: 3600,
refresh_token: refreshToken,
Expand Down Expand Up @@ -301,7 +300,6 @@ describe("LoginStrategy", () => {
it("builds AuthResult", async () => {
const tokenResponse = identityTokenResponseFactory();
tokenResponse.forcePasswordReset = true;
tokenResponse.resetMasterPassword = true;

apiService.postIdentityToken.mockResolvedValue(tokenResponse);

Expand All @@ -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);
});
Expand All @@ -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);

Expand Down
2 changes: 0 additions & 2 deletions libs/auth/src/common/login-strategies/login.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,6 @@ describe("PasswordLoginStrategy", () => {
newDeviceOtp: deviceVerificationOtp,
}),
);
expect(result.resetMasterPassword).toBe(false);
expect(result.userId).toBe(userId);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ describe("WebAuthnLoginStrategy", () => {

expect(authResult).toBeInstanceOf(AuthResult);
expect(authResult).toMatchObject({
resetMasterPassword: false,
twoFactorProviders: null,
requiresTwoFactor: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
8 changes: 0 additions & 8 deletions libs/common/src/auth/models/domain/auth-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<TwoFactorProviderType, Record<string, string>>> = null;
ssoEmail2FaSessionToken?: string;
email: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export class IdentityTokenResponse extends BaseResponse {
tokenType: string;

// Decryption Information
resetMasterPassword: boolean;
privateKey: string; // userKeyEncryptedPrivateKey
key?: EncString; // masterKeyEncryptedUserKey
twoFactorToken: string;
Expand Down Expand Up @@ -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) {
Expand Down
Loading