|
|
|
|
@@ -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);
|
|
|
|
|
|