1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

Auth/PM-17693 - Web - Existing users accepting an org invite are required to update password to meet org policy requirements (#13388)

* PM-17693 - Refactor all post login logic around getting org policies from invite token and restore lost functionality.

* PM-17693 - Add TODO
This commit is contained in:
Jared Snider
2025-02-19 09:18:56 -05:00
committed by GitHub
parent 39f241db3d
commit ae38e40859
6 changed files with 70 additions and 73 deletions

View File

@@ -56,13 +56,6 @@ describe("DefaultLoginComponentService", () => {
expect(service).toBeTruthy();
});
describe("getOrgPolicies", () => {
it("returns null", async () => {
const result = await service.getOrgPolicies();
expect(result).toBeNull();
});
});
describe("isLoginWithPasskeySupported", () => {
it("returns true when clientType is Web", () => {
service["clientType"] = ClientType.Web;

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LoginComponentService, PasswordPolicies } from "@bitwarden/auth/angular";
import { LoginComponentService } from "@bitwarden/auth/angular";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { ClientType } from "@bitwarden/common/enums";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -23,10 +23,6 @@ export class DefaultLoginComponentService implements LoginComponentService {
protected ssoLoginService: SsoLoginServiceAbstraction,
) {}
async getOrgPolicies(): Promise<PasswordPolicies | null> {
return null;
}
isLoginWithPasskeySupported(): boolean {
return this.clientType === ClientType.Web;
}

View File

@@ -23,7 +23,7 @@ export abstract class LoginComponentService {
* Gets the organization policies if there is an organization invite.
* - Used by: Web
*/
getOrgPolicies: () => Promise<PasswordPolicies | null>;
getOrgPoliciesFromOrgInvite?: () => Promise<PasswordPolicies | null>;
/**
* Indicates whether login with passkey is supported on the given client

View File

@@ -12,6 +12,7 @@ import {
PasswordLoginCredentials,
} from "@bitwarden/auth/common";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyData } from "@bitwarden/common/admin-console/models/data/policy.data";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
@@ -30,6 +31,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
@@ -43,7 +45,7 @@ import {
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
import { VaultIcon, WaveIcon } from "../icons";
import { LoginComponentService } from "./login-component.service";
import { LoginComponentService, PasswordPolicies } from "./login-component.service";
const BroadcasterSubscriptionId = "LoginComponent";
@@ -72,7 +74,6 @@ export class LoginComponent implements OnInit, OnDestroy {
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
private destroy$ = new Subject<void>();
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
readonly Icons = { WaveIcon, VaultIcon };
clientType: ClientType;
@@ -97,11 +98,6 @@ export class LoginComponent implements OnInit, OnDestroy {
return this.formGroup.controls.email;
}
// Web properties
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
policies: Policy[] | undefined;
showResetPasswordAutoEnrollWarning = false;
// Desktop properties
deferFocus: boolean | null = null;
@@ -281,18 +277,39 @@ export class LoginComponent implements OnInit, OnDestroy {
return;
}
// User logged in successfully so execute side effects
await this.loginSuccessHandlerService.run(authResult.userId);
this.loginEmailService.clearValues();
// Determine where to send the user next
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
this.loginEmailService.clearValues();
await this.router.navigate(["update-temp-password"]);
return;
}
// If none of the above cases are true, proceed with login...
await this.evaluatePassword();
// TODO: PM-18269 - evaluate if we can combine this with the
// password evaluation done in the password login strategy.
// If there's an existing org invite, use it to get the org's password policies
// so we can evaluate the MP against the org policies
if (this.loginComponentService.getOrgPoliciesFromOrgInvite) {
const orgPolicies: PasswordPolicies | null =
await this.loginComponentService.getOrgPoliciesFromOrgInvite();
this.loginEmailService.clearValues();
if (orgPolicies) {
// Since we have retrieved the policies, we can go ahead and set them into state for future use
// e.g., the update-password page currently only references state for policy data and
// doesn't fallback to pulling them from the server like it should if they are null.
await this.setPoliciesIntoState(authResult.userId, orgPolicies.policies);
const isPasswordChangeRequired = await this.isPasswordChangeRequiredByOrgPolicy(
orgPolicies.enforcedPasswordPolicyOptions,
);
if (isPasswordChangeRequired) {
await this.router.navigate(["update-password"]);
return;
}
}
}
if (this.clientType === ClientType.Browser) {
await this.router.navigate(["/tabs/vault"]);
@@ -310,54 +327,51 @@ export class LoginComponent implements OnInit, OnDestroy {
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
}
protected async evaluatePassword(): Promise<void> {
/**
* Checks if the master password meets the enforced policy requirements
* and if the user is required to change their password.
*/
private async isPasswordChangeRequiredByOrgPolicy(
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions,
): Promise<boolean> {
try {
// If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) {
this.enforcedMasterPasswordOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$(),
);
if (enforcedPasswordPolicyOptions == undefined) {
return false;
}
if (this.requirePasswordChange()) {
await this.router.navigate(["update-password"]);
return;
// Note: we deliberately do not check enforcedPasswordPolicyOptions.enforceOnLogin
// as existing users who are logging in after getting an org invite should
// always be forced to set a password that meets the org's policy.
// Org Invite -> Registration also works this way for new BW users as well.
const masterPassword = this.formGroup.controls.masterPassword.value;
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
if (!masterPassword) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.formGroup.value.email ?? undefined,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
enforcedPasswordPolicyOptions,
);
} catch (e) {
// Do not prevent unlock if there is an error evaluating policies
this.logService.error(e);
return false;
}
}
/**
* Checks if the master password meets the enforced policy requirements
* If not, returns false
*/
private requirePasswordChange(): boolean {
if (
this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin
) {
return false;
}
const masterPassword = this.formGroup.controls.masterPassword.value;
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
if (!masterPassword) {
return false;
}
const passwordStrength = this.passwordStrengthService.getPasswordStrength(
masterPassword,
this.formGroup.value.email ?? undefined,
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
masterPassword,
this.enforcedMasterPasswordOptions,
);
private async setPoliciesIntoState(userId: UserId, policies: Policy[]): Promise<void> {
const policiesData: { [id: string]: PolicyData } = {};
policies.map((p) => (policiesData[p.id] = PolicyData.fromPolicy(p)));
await this.policyService.replace(policiesData, userId);
}
protected async startAuthRequestLogin(): Promise<void> {
@@ -528,12 +542,6 @@ export class LoginComponent implements OnInit, OnDestroy {
}
private async defaultOnInit(): Promise<void> {
// If there's an existing org invite, use it to get the password policies
const orgPolicies = await this.loginComponentService.getOrgPolicies();
this.policies = orgPolicies?.policies;
this.showResetPasswordAutoEnrollWarning = orgPolicies?.isPolicyAndAutoEnrollEnabled ?? false;
let paramEmailIsSet = false;
const params = await firstValueFrom(this.activatedRoute.queryParams);