mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +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:
@@ -74,10 +74,10 @@ describe("WebLoginComponentService", () => {
|
|||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getOrgPolicies", () => {
|
describe("getOrgPoliciesFromOrgInvite", () => {
|
||||||
it("returns undefined if organization invite is null", async () => {
|
it("returns undefined if organization invite is null", async () => {
|
||||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||||
const result = await service.getOrgPolicies();
|
const result = await service.getOrgPoliciesFromOrgInvite();
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ describe("WebLoginComponentService", () => {
|
|||||||
organizationName: "org-name",
|
organizationName: "org-name",
|
||||||
});
|
});
|
||||||
policyApiService.getPoliciesByToken.mockRejectedValue(error);
|
policyApiService.getPoliciesByToken.mockRejectedValue(error);
|
||||||
await service.getOrgPolicies();
|
await service.getOrgPoliciesFromOrgInvite();
|
||||||
expect(logService.error).toHaveBeenCalledWith(error);
|
expect(logService.error).toHaveBeenCalledWith(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ describe("WebLoginComponentService", () => {
|
|||||||
of(masterPasswordPolicyOptions),
|
of(masterPasswordPolicyOptions),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await service.getOrgPolicies();
|
const result = await service.getOrgPoliciesFromOrgInvite();
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
policies: policies,
|
policies: policies,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class WebLoginComponentService
|
|||||||
this.clientType = this.platformUtilsService.getClientType();
|
this.clientType = this.platformUtilsService.getClientType();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrgPolicies(): Promise<PasswordPolicies | null> {
|
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | null> {
|
||||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||||
|
|
||||||
if (orgInvite != null) {
|
if (orgInvite != null) {
|
||||||
|
|||||||
@@ -56,13 +56,6 @@ describe("DefaultLoginComponentService", () => {
|
|||||||
expect(service).toBeTruthy();
|
expect(service).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getOrgPolicies", () => {
|
|
||||||
it("returns null", async () => {
|
|
||||||
const result = await service.getOrgPolicies();
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("isLoginWithPasskeySupported", () => {
|
describe("isLoginWithPasskeySupported", () => {
|
||||||
it("returns true when clientType is Web", () => {
|
it("returns true when clientType is Web", () => {
|
||||||
service["clientType"] = ClientType.Web;
|
service["clientType"] = ClientType.Web;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { firstValueFrom } from "rxjs";
|
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 { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
@@ -23,10 +23,6 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
|||||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getOrgPolicies(): Promise<PasswordPolicies | null> {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoginWithPasskeySupported(): boolean {
|
isLoginWithPasskeySupported(): boolean {
|
||||||
return this.clientType === ClientType.Web;
|
return this.clientType === ClientType.Web;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export abstract class LoginComponentService {
|
|||||||
* Gets the organization policies if there is an organization invite.
|
* Gets the organization policies if there is an organization invite.
|
||||||
* - Used by: Web
|
* - Used by: Web
|
||||||
*/
|
*/
|
||||||
getOrgPolicies: () => Promise<PasswordPolicies | null>;
|
getOrgPoliciesFromOrgInvite?: () => Promise<PasswordPolicies | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether login with passkey is supported on the given client
|
* Indicates whether login with passkey is supported on the given client
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
PasswordLoginCredentials,
|
PasswordLoginCredentials,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import {
|
import {
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
@@ -43,7 +45,7 @@ import {
|
|||||||
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service";
|
||||||
import { VaultIcon, WaveIcon } from "../icons";
|
import { VaultIcon, WaveIcon } from "../icons";
|
||||||
|
|
||||||
import { LoginComponentService } from "./login-component.service";
|
import { LoginComponentService, PasswordPolicies } from "./login-component.service";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "LoginComponent";
|
const BroadcasterSubscriptionId = "LoginComponent";
|
||||||
|
|
||||||
@@ -72,7 +74,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
|
@ViewChild("masterPasswordInputRef") masterPasswordInputRef: ElementRef | undefined;
|
||||||
|
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions | undefined = undefined;
|
|
||||||
readonly Icons = { WaveIcon, VaultIcon };
|
readonly Icons = { WaveIcon, VaultIcon };
|
||||||
|
|
||||||
clientType: ClientType;
|
clientType: ClientType;
|
||||||
@@ -97,11 +98,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
return this.formGroup.controls.email;
|
return this.formGroup.controls.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web properties
|
|
||||||
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions | undefined;
|
|
||||||
policies: Policy[] | undefined;
|
|
||||||
showResetPasswordAutoEnrollWarning = false;
|
|
||||||
|
|
||||||
// Desktop properties
|
// Desktop properties
|
||||||
deferFocus: boolean | null = null;
|
deferFocus: boolean | null = null;
|
||||||
|
|
||||||
@@ -281,18 +277,39 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User logged in successfully so execute side effects
|
||||||
await this.loginSuccessHandlerService.run(authResult.userId);
|
await this.loginSuccessHandlerService.run(authResult.userId);
|
||||||
|
|
||||||
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
|
||||||
this.loginEmailService.clearValues();
|
this.loginEmailService.clearValues();
|
||||||
|
|
||||||
|
// Determine where to send the user next
|
||||||
|
if (authResult.forcePasswordReset != ForceSetPasswordReason.None) {
|
||||||
await this.router.navigate(["update-temp-password"]);
|
await this.router.navigate(["update-temp-password"]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If none of the above cases are true, proceed with login...
|
// TODO: PM-18269 - evaluate if we can combine this with the
|
||||||
await this.evaluatePassword();
|
// 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) {
|
if (this.clientType === ClientType.Browser) {
|
||||||
await this.router.navigate(["/tabs/vault"]);
|
await this.router.navigate(["/tabs/vault"]);
|
||||||
@@ -310,37 +327,23 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
|
await this.loginComponentService.launchSsoBrowserWindow(email, clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async evaluatePassword(): Promise<void> {
|
|
||||||
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 (this.requirePasswordChange()) {
|
|
||||||
await this.router.navigate(["update-password"]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Do not prevent unlock if there is an error evaluating policies
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the master password meets the enforced policy requirements
|
* Checks if the master password meets the enforced policy requirements
|
||||||
* If not, returns false
|
* and if the user is required to change their password.
|
||||||
*/
|
*/
|
||||||
private requirePasswordChange(): boolean {
|
private async isPasswordChangeRequiredByOrgPolicy(
|
||||||
if (
|
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions,
|
||||||
this.enforcedMasterPasswordOptions == undefined ||
|
): Promise<boolean> {
|
||||||
!this.enforcedMasterPasswordOptions.enforceOnLogin
|
try {
|
||||||
) {
|
if (enforcedPasswordPolicyOptions == undefined) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
const masterPassword = this.formGroup.controls.masterPassword.value;
|
||||||
|
|
||||||
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
|
// Return false if masterPassword is null/undefined since this is only evaluated after successful login
|
||||||
@@ -356,8 +359,19 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
return !this.policyService.evaluateMasterPassword(
|
return !this.policyService.evaluateMasterPassword(
|
||||||
passwordStrength,
|
passwordStrength,
|
||||||
masterPassword,
|
masterPassword,
|
||||||
this.enforcedMasterPasswordOptions,
|
enforcedPasswordPolicyOptions,
|
||||||
);
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Do not prevent unlock if there is an error evaluating policies
|
||||||
|
this.logService.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
protected async startAuthRequestLogin(): Promise<void> {
|
||||||
@@ -528,12 +542,6 @@ export class LoginComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async defaultOnInit(): Promise<void> {
|
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;
|
let paramEmailIsSet = false;
|
||||||
|
|
||||||
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
const params = await firstValueFrom(this.activatedRoute.queryParams);
|
||||||
|
|||||||
Reference in New Issue
Block a user