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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user