mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 18:53:29 +00:00
[PM-19212] Consolidate password set routing to AuthGuard using ForceSetPasswordReason (#14356)
* Consolidates component routing, removing routing to update-temp-password from components. All routing to update-temp-password should happen in the AuthGuard now. --------- Co-authored-by: Jared Snider <jsnider@bitwarden.com> Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
@@ -296,13 +296,9 @@ describe("LoginStrategy", () => {
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.userId = userId;
|
||||
expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
expected.resetMasterPassword = true;
|
||||
expected.twoFactorProviders = {} as Partial<
|
||||
Record<TwoFactorProviderType, Record<string, string>>
|
||||
>;
|
||||
expected.captchaSiteKey = "";
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -316,13 +312,9 @@ describe("LoginStrategy", () => {
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.userId = userId;
|
||||
expected.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
expected.resetMasterPassword = false;
|
||||
expected.twoFactorProviders = {} as Partial<
|
||||
Record<TwoFactorProviderType, Record<string, string>>
|
||||
>;
|
||||
expected.captchaSiteKey = "";
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
|
||||
@@ -277,17 +277,7 @@ export abstract class LoginStrategy {
|
||||
|
||||
result.resetMasterPassword = response.resetMasterPassword;
|
||||
|
||||
// Convert boolean to enum and set the state for the master password service to
|
||||
// so we know when we reach the auth guard that we need to guide them properly to admin
|
||||
// password reset.
|
||||
if (response.forcePasswordReset) {
|
||||
result.forcePasswordReset = ForceSetPasswordReason.AdminForcePasswordReset;
|
||||
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
await this.processForceSetPasswordReason(response.forcePasswordReset, userId);
|
||||
|
||||
if (response.twoFactorToken != null) {
|
||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
||||
@@ -318,6 +308,30 @@ export abstract class LoginStrategy {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if adminForcePasswordReset is true and sets the ForceSetPasswordReason.AdminForcePasswordReset flag in the master password service.
|
||||
* @param adminForcePasswordReset - The admin force password reset flag
|
||||
* @param userId - The user ID
|
||||
* @returns a promise that resolves to a boolean indicating whether the admin force password reset flag was set
|
||||
*/
|
||||
async processForceSetPasswordReason(
|
||||
adminForcePasswordReset: boolean,
|
||||
userId: UserId,
|
||||
): Promise<boolean> {
|
||||
if (!adminForcePasswordReset) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// set the flag in the master password service so we know when we reach the auth guard
|
||||
// that we need to guide them properly to admin password reset.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async createKeyPairForOldAccount(userId: UserId) {
|
||||
try {
|
||||
const userKey = await this.keyService.getUserKeyWithLegacySupport(userId);
|
||||
|
||||
@@ -211,20 +211,18 @@ describe("PasswordLoginStrategy", () => {
|
||||
it("does not force the user to update their master password when there are no requirements", async () => {
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory());
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
|
||||
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
|
||||
});
|
||||
|
||||
it("does not force the user to update their master password when it meets requirements", async () => {
|
||||
passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 5 } as any);
|
||||
policyService.evaluateMasterPassword.mockReturnValue(true);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
|
||||
});
|
||||
|
||||
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
|
||||
@@ -232,14 +230,13 @@ describe("PasswordLoginStrategy", () => {
|
||||
policyService.evaluateMasterPassword.mockReturnValue(false);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
expect(result.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
|
||||
});
|
||||
|
||||
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
|
||||
@@ -257,13 +254,13 @@ describe("PasswordLoginStrategy", () => {
|
||||
|
||||
// First login request fails requiring 2FA
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(token2FAResponse);
|
||||
const firstResult = await passwordLoginStrategy.logIn(credentials);
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
// Second login request succeeds
|
||||
apiService.postIdentityToken.mockResolvedValueOnce(
|
||||
identityTokenResponseFactory(masterPasswordPolicy),
|
||||
);
|
||||
const secondResult = await passwordLoginStrategy.logInTwoFactor(
|
||||
await passwordLoginStrategy.logInTwoFactor(
|
||||
{
|
||||
provider: TwoFactorProviderType.Authenticator,
|
||||
token: "123456",
|
||||
@@ -272,15 +269,11 @@ describe("PasswordLoginStrategy", () => {
|
||||
"",
|
||||
);
|
||||
|
||||
// First login attempt should not save the force password reset options
|
||||
expect(firstResult.forcePasswordReset).toEqual(ForceSetPasswordReason.None);
|
||||
|
||||
// Second login attempt should save the force password reset options and return in result
|
||||
// Second login attempt should save the force password reset options
|
||||
expect(masterPasswordService.mock.setForceSetPasswordReason).toHaveBeenCalledWith(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
expect(secondResult.forcePasswordReset).toEqual(ForceSetPasswordReason.WeakMasterPassword);
|
||||
});
|
||||
|
||||
it("handles new device verification login with OTP", async () => {
|
||||
@@ -298,7 +291,6 @@ describe("PasswordLoginStrategy", () => {
|
||||
newDeviceOtp: deviceVerificationOtp,
|
||||
}),
|
||||
);
|
||||
expect(result.forcePasswordReset).toBe(ForceSetPasswordReason.None);
|
||||
expect(result.resetMasterPassword).toBe(false);
|
||||
expect(result.userId).toBe(userId);
|
||||
});
|
||||
|
||||
@@ -109,35 +109,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
await this.evaluateMasterPasswordIfRequired(identityResponse, credentials, authResult);
|
||||
|
||||
// The identity result can contain master password policies for the user's organizations
|
||||
if (masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
// If there is a policy active, evaluate the supplied password before its no longer in memory
|
||||
const meetsRequirements = this.evaluateMasterPassword(
|
||||
credentials,
|
||||
masterPasswordPolicyOptions,
|
||||
);
|
||||
if (meetsRequirements) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
if (identityResponse instanceof IdentityTwoFactorResponse) {
|
||||
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||
});
|
||||
} else {
|
||||
// Authentication was successful, save the force update password options with the state service
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
authResult.userId, // userId is only available on successful login
|
||||
);
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
}
|
||||
return authResult;
|
||||
}
|
||||
|
||||
@@ -151,20 +124,6 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
|
||||
const result = await super.logInTwoFactor(twoFactor);
|
||||
|
||||
// 2FA was successful, save the force update password options with the state service if defined
|
||||
const forcePasswordResetReason = this.cache.value.forcePasswordResetReason;
|
||||
if (
|
||||
!result.requiresTwoFactor &&
|
||||
!result.requiresCaptcha &&
|
||||
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
forcePasswordResetReason,
|
||||
result.userId,
|
||||
);
|
||||
result.forcePasswordReset = forcePasswordResetReason;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -208,13 +167,58 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
return !response.key;
|
||||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response:
|
||||
private async evaluateMasterPasswordIfRequired(
|
||||
identityResponse:
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse,
|
||||
): MasterPasswordPolicyOptions {
|
||||
if (response == null || response instanceof IdentityDeviceVerificationResponse) {
|
||||
credentials: PasswordLoginCredentials,
|
||||
authResult: AuthResult,
|
||||
): Promise<void> {
|
||||
// TODO: PM-21084 - investigate if we should be sending down masterPasswordPolicy on the IdentityDeviceVerificationResponse like we do for the IdentityTwoFactorResponse
|
||||
// If the response is a device verification response, we don't need to evaluate the password
|
||||
if (identityResponse instanceof IdentityDeviceVerificationResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The identity result can contain master password policies for the user's organizations
|
||||
const masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
if (!masterPasswordPolicyOptions?.enforceOnLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If there is a policy active, evaluate the supplied password before its no longer in memory
|
||||
const meetsRequirements = this.evaluateMasterPassword(credentials, masterPasswordPolicyOptions);
|
||||
if (meetsRequirements) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (identityResponse instanceof IdentityTwoFactorResponse) {
|
||||
// Save the flag to this strategy for use in 2fa as the master password is about to pass out of scope
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication was successful, save the force update password options with the state service
|
||||
// if there isn't already a reason set (this would only be AdminForcePasswordReset as that can be set server side
|
||||
// and would have already been processed in the base login strategy processForceSetPasswordReason method)
|
||||
// Note: masterPasswordService.setForceSetPasswordReason will not allow overwriting
|
||||
// AdminForcePasswordReset with any other reason except for None. This is because
|
||||
// an AdminForcePasswordReset will always force a user to update their password to a password that meets the policy.
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
authResult.userId, // userId is only available on successful login
|
||||
);
|
||||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse,
|
||||
): MasterPasswordPolicyOptions | null {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
||||
@@ -246,4 +250,35 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to handle the WeakMasterPassword reason if no other reason is set.
|
||||
* @param authResult - The authentication result
|
||||
* @param userId - The user ID
|
||||
*/
|
||||
override async processForceSetPasswordReason(
|
||||
adminForcePasswordReset: boolean,
|
||||
userId: UserId,
|
||||
): Promise<boolean> {
|
||||
// handle any existing reasons
|
||||
const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason(
|
||||
adminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
|
||||
// If we are already processing an admin force password reset, don't process other reasons
|
||||
if (adminForcePasswordResetFlagSet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a cached weak password reason from login/logInTwoFactor apply it
|
||||
const cachedReason = this.cache.value.forcePasswordResetReason;
|
||||
if (cachedReason !== ForceSetPasswordReason.None) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(cachedReason, userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If none of the conditions are met, return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
@@ -37,10 +37,11 @@ import {
|
||||
AuthRequestServiceAbstraction,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "../abstractions";
|
||||
import { UserDecryptionOptions } from "../models";
|
||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||
import { SsoLoginStrategy } from "./sso-login.strategy";
|
||||
import { SsoLoginStrategy, SsoLoginStrategyData } from "./sso-login.strategy";
|
||||
|
||||
describe("SsoLoginStrategy", () => {
|
||||
let accountService: FakeAccountService;
|
||||
@@ -123,8 +124,11 @@ describe("SsoLoginStrategy", () => {
|
||||
mockVaultTimeoutBSub.asObservable(),
|
||||
);
|
||||
|
||||
const userDecryptionOptions = new UserDecryptionOptions();
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = of(userDecryptionOptions);
|
||||
|
||||
ssoLoginStrategy = new SsoLoginStrategy(
|
||||
null,
|
||||
{} as SsoLoginStrategyData,
|
||||
keyConnectorService,
|
||||
deviceTrustService,
|
||||
authRequestService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { firstValueFrom, Observable, map, BehaviorSubject } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||
@@ -355,4 +356,75 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||
sso: this.cache.value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to handle SSO-specific ForceSetPasswordReason flags,including TdeOffboarding,
|
||||
* TdeUserWithoutPasswordHasPasswordResetPermission, and SsoNewJitProvisionedUser cases.
|
||||
* @param authResult - The authentication result
|
||||
* @param userId - The user ID
|
||||
*/
|
||||
override async processForceSetPasswordReason(
|
||||
adminForcePasswordReset: boolean,
|
||||
userId: UserId,
|
||||
): Promise<boolean> {
|
||||
// handle any existing reasons
|
||||
const adminForcePasswordResetFlagSet = await super.processForceSetPasswordReason(
|
||||
adminForcePasswordReset,
|
||||
userId,
|
||||
);
|
||||
|
||||
// If we are already processing an admin force password reset, don't process other reasons
|
||||
if (adminForcePasswordResetFlagSet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for TDE-related conditions
|
||||
const userDecryptionOptions = await firstValueFrom(
|
||||
this.userDecryptionOptionsService.userDecryptionOptions$,
|
||||
);
|
||||
|
||||
if (!userDecryptionOptions) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for TDE offboarding - user is being offboarded from TDE and needs to set a password
|
||||
if (userDecryptionOptions.trustedDeviceOption?.isTdeOffboarding) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeOffboarding,
|
||||
userId,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if user has permission to set password but hasn't yet
|
||||
if (
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
userDecryptionOptions.trustedDeviceOption?.hasManageResetPasswordPermission
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission,
|
||||
userId,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for new SSO JIT provisioned user
|
||||
// If a user logs in via SSO but has no master password and no alternative encryption methods
|
||||
// Then they must be a newly provisioned user who needs to set up their encryption
|
||||
if (
|
||||
!userDecryptionOptions.hasMasterPassword &&
|
||||
!userDecryptionOptions.keyConnectorOption?.keyConnectorUrl &&
|
||||
!userDecryptionOptions.trustedDeviceOption
|
||||
) {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.SsoNewJitProvisionedUser,
|
||||
userId,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If none of the conditions are met, return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,6 @@ describe("WebAuthnLoginStrategy", () => {
|
||||
expect(authResult).toBeInstanceOf(AuthResult);
|
||||
expect(authResult).toMatchObject({
|
||||
captchaSiteKey: "",
|
||||
forcePasswordReset: 0,
|
||||
resetMasterPassword: false,
|
||||
twoFactorProviders: null,
|
||||
requiresTwoFactor: false,
|
||||
|
||||
Reference in New Issue
Block a user