mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
* PM-3275 - Policy.service - Refactor existing mapPoliciesFromToken internal logic to provide public mapPolicyFromResponse method * PM-3275 - Add new PolicyApiService.getMasterPasswordPolicyOptsForOrgUser method for use in the set password comp * PM-3275 - Update set-password.comp to use new policyApiService.getMasterPasswordPoliciesForInvitedUsers method * PM-3275 - (1) Remove post TDE AuthN set password routing logic from SSO/2FA comps as we cannot set an initial user password until after decryption in order to avoid losing the ability to decrypt existing vault items (a new user key would be created if one didn't exist in memory) (2) Add set password routing logic post TDE decryption in LoginWithDevice/Lock components (3) Add new ForceResetPasswordReason to capture this case so that we can guard against users manually navigating away from the set password screen * PM-3275 - SyncSvc - Add logic for setting forcePasswordReset reason if TDE user w/out MP went from not having MP reset permission to having it. * PM-3275 - Rename ForceResetPasswordReason enum to ForceSetPasswordReason + update all references. * PM-3275 - Removing client deprecated calls to getPoliciesByInvitedUser and helper call getMasterPasswordPoliciesForInvitedUsers * PM-3275 - PolicyAPI service - remove no longer necessary getPoliciesByInvitedUser method * PM-3275 - LockComp - TODO cleanup * PM-3275 - SSO & 2FA comp - cleanup of incorrect routing path * PM-3275 - (1) State service refactor - change getForcePasswordResetReason / setForcePasswordResetReason to be getForceSetPasswordReason / setForceSetPasswordReason (2) Sync Service - encapsulate setForceSetPasswordReasonIfNeeded logic into own method * PM-3275 - SetPassword Comp - Rename "identifier" to be "orgSsoIdentifier" for clarity * PM-3275 - SetPasswordComp - Moving routing from SSO / 2FA comps to Lock / LoginWithDevice comps results in a loss of the the OrgSsoId. However, as part of the TDE work, we added the OrgSsoId to state so use that as a fallback so we can accurately evaluate if the user needs to be auto enrolled in admin account recovery. * PM-3275 - SetPasswordComp - add a bit more context to why/when we are reading the user org sso id out of state * PM-3275 - SetPassword Comp - (1) Add forceSetPasswordReason and ForceSetPasswordReason enum as public props on the class so we can change copy text based on which is set + set forceSetPasswordReason on ngOnInit (2) Refactor ngOnInit to use a single RxJs observable chain for primary logic as the auto enroll check was occurring before the async getUserSsoOrganizationIdentifier could finish. * PM-3275 - Desktop - App comp - missed replacing getForcePasswordResetReason with getForceSetPasswordReason * PM-3275 - TDE Decryption Option Comps - must set ForceSetPasswordReason so that we can properly enforce keeping the user on the component + display the correct copy explaining the scenario to the user. * PM-3275 - All Clients - SetPasswordComp html - Update page description per product + remove no longer used ssoCompleteRegistration translation. * PM-3275 - SetPasswordComp - hopefully the final puzzle piece - must clear ForceSetPasswordReason in order to let user navigate back to vault. * PM-3275 - SyncService - Remove check for previous value of account decryption options hasManageResetPasswordPermission as when a user logged in on a trusted device after having their permissions updated, the initial setting would be true and it would cause the flag to NOT be set when it should have. * PM-3275 - TDE User Context - (1) Remove explicit navigation to set password screen from post decryption success scenarios on lock & login w/ device comps (2) Move TdeUserWithoutPasswordHasPasswordResetPermission flag setting to SSO / 2FA components to support both trusted and untrusted device scenarios (both of which are now caught by the auth guard). * PM-3275 - (1) SetPassword comp - adjust set password logic for TDE users to avoid creating a new user asymmetric key pair and setting a new private key in memory. (2) Adjust SetPasswordRequest to allow null keys * PM-3275 - Remove unused route from login with device comp * PM-3275 - Sso & 2FA comp tests - Update tests to reflect new routing logic when TDE user needs to set a password * PM-3275 - Lock comp - per PR feedback, remove unused setPasswordRoute property. * PM-3275 - SetPasswordComp - Per PR feedback, use explicit null check * PM-3275 - Per PR Feedback, rename missed forcePasswordResetReason to be forceSetPasswordReason on account model * PM-3275 - Auth guard - rename forcePasswordResetReason to forceSetPasswordReason * PM-3275 - SSO / 2FA comps - Per PR feedback, refactor Admin Force Password reset handling to be in one place above the TDE user flows and standard user flows as it applies to both. * PM-3275 - Per PR feedback, clarify 2FA routing comment * PM-3275 - Per PR feedback, update set-password comp ngOnInit switchMaps to just return promises as switchMap converts promises to observables internally. * PM-3275 - Per PR feedback, refactor set password ngOnInit observable chain to avoid using async subscribe and instead simply sequence the calls via switchMap and tap for side effects. * PM-3275 - Per PR feedback, move tap after filter so we can remove if check * PM-3275 - Per PR feedback, update policy service mapping methods to use shorthand null checking. * PM-3275 - SetPassword comp - (1) Move force set password reason logic into onSetPasswordSuccess(...) (2) On onSetPasswordSuccess, must set hasMasterPassword to true for user verification scenarios. * PM-3275 - Per PR feedback, remove new hasManageResetPasswordPermission flag from profile response and instead simply read the information off the existing profile.organizations data as the information I needed was already present. * PM-4633 - PolicyService - mapPolicyFromResponse(...) - remove incorrect null check for data. Policies with internal null data property should still be evaluated and turned into Policy objects or the policy array ends up having null values in it and it causes errors down the line on login after acct creation.
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
import { Directive, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
|
import { FormBuilder, Validators } from "@angular/forms";
|
|
import { ActivatedRoute, Router } from "@angular/router";
|
|
import { Subject } from "rxjs";
|
|
import { take, takeUntil } from "rxjs/operators";
|
|
|
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
|
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
|
import { PasswordLoginCredentials } from "@bitwarden/common/auth/models/domain/login-credentials";
|
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
|
|
|
import {
|
|
AllValidationErrors,
|
|
FormValidationErrorsService,
|
|
} from "../../platform/abstractions/form-validation-errors.service";
|
|
|
|
import { CaptchaProtectedComponent } from "./captcha-protected.component";
|
|
|
|
@Directive()
|
|
export class LoginComponent extends CaptchaProtectedComponent implements OnInit, OnDestroy {
|
|
@ViewChild("masterPasswordInput", { static: true }) masterPasswordInput: ElementRef;
|
|
|
|
showPassword = false;
|
|
formPromise: Promise<AuthResult>;
|
|
onSuccessfulLogin: () => Promise<any>;
|
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
|
showLoginWithDevice: boolean;
|
|
validatedEmail = false;
|
|
paramEmailSet = false;
|
|
|
|
formGroup = this.formBuilder.group({
|
|
email: ["", [Validators.required, Validators.email]],
|
|
masterPassword: [
|
|
"",
|
|
[Validators.required, Validators.minLength(Utils.originalMinimumPasswordLength)],
|
|
],
|
|
rememberEmail: [false],
|
|
});
|
|
|
|
protected twoFactorRoute = "2fa";
|
|
protected successRoute = "vault";
|
|
protected forcePasswordResetRoute = "update-temp-password";
|
|
|
|
protected destroy$ = new Subject<void>();
|
|
|
|
get loggedEmail() {
|
|
return this.formGroup.value.email;
|
|
}
|
|
|
|
constructor(
|
|
protected devicesApiService: DevicesApiServiceAbstraction,
|
|
protected appIdService: AppIdService,
|
|
protected authService: AuthService,
|
|
protected router: Router,
|
|
platformUtilsService: PlatformUtilsService,
|
|
i18nService: I18nService,
|
|
protected stateService: StateService,
|
|
environmentService: EnvironmentService,
|
|
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
|
|
protected cryptoFunctionService: CryptoFunctionService,
|
|
protected logService: LogService,
|
|
protected ngZone: NgZone,
|
|
protected formBuilder: FormBuilder,
|
|
protected formValidationErrorService: FormValidationErrorsService,
|
|
protected route: ActivatedRoute,
|
|
protected loginService: LoginService
|
|
) {
|
|
super(environmentService, i18nService, platformUtilsService);
|
|
}
|
|
|
|
get selfHostedDomain() {
|
|
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
|
|
}
|
|
|
|
async ngOnInit() {
|
|
this.route?.queryParams.pipe(takeUntil(this.destroy$)).subscribe((params) => {
|
|
if (!params) {
|
|
return;
|
|
}
|
|
|
|
const queryParamsEmail = params.email;
|
|
|
|
if (queryParamsEmail != null && queryParamsEmail.indexOf("@") > -1) {
|
|
this.formGroup.get("email").setValue(queryParamsEmail);
|
|
this.loginService.setEmail(queryParamsEmail);
|
|
this.paramEmailSet = true;
|
|
}
|
|
});
|
|
let email = this.loginService.getEmail();
|
|
|
|
if (email == null || email === "") {
|
|
email = await this.stateService.getRememberedEmail();
|
|
}
|
|
|
|
if (!this.paramEmailSet) {
|
|
this.formGroup.get("email")?.setValue(email ?? "");
|
|
}
|
|
let rememberEmail = this.loginService.getRememberEmail();
|
|
if (rememberEmail == null) {
|
|
rememberEmail = (await this.stateService.getRememberedEmail()) != null;
|
|
}
|
|
this.formGroup.get("rememberEmail")?.setValue(rememberEmail);
|
|
}
|
|
|
|
ngOnDestroy() {
|
|
this.destroy$.next();
|
|
this.destroy$.complete();
|
|
}
|
|
|
|
async submit(showToast = true) {
|
|
const data = this.formGroup.value;
|
|
|
|
await this.setupCaptcha();
|
|
|
|
this.formGroup.markAllAsTouched();
|
|
|
|
//web
|
|
if (this.formGroup.invalid && !showToast) {
|
|
return;
|
|
}
|
|
|
|
//desktop, browser; This should be removed once all clients use reactive forms
|
|
if (this.formGroup.invalid && showToast) {
|
|
const errorText = this.getErrorToastMessage();
|
|
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const credentials = new PasswordLoginCredentials(
|
|
data.email,
|
|
data.masterPassword,
|
|
this.captchaToken,
|
|
null
|
|
);
|
|
this.formPromise = this.authService.logIn(credentials);
|
|
const response = await this.formPromise;
|
|
this.setFormValues();
|
|
await this.loginService.saveEmailSettings();
|
|
if (this.handleCaptchaRequired(response)) {
|
|
return;
|
|
} else if (this.handleMigrateEncryptionKey(response)) {
|
|
return;
|
|
} else if (response.requiresTwoFactor) {
|
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
|
this.onSuccessfulLoginTwoFactorNavigate();
|
|
} else {
|
|
this.router.navigate([this.twoFactorRoute]);
|
|
}
|
|
} else if (response.forcePasswordReset != ForceSetPasswordReason.None) {
|
|
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
|
this.onSuccessfulLoginForceResetNavigate();
|
|
} else {
|
|
this.router.navigate([this.forcePasswordResetRoute]);
|
|
}
|
|
} else {
|
|
if (this.onSuccessfulLogin != null) {
|
|
this.onSuccessfulLogin();
|
|
}
|
|
if (this.onSuccessfulLoginNavigate != null) {
|
|
this.onSuccessfulLoginNavigate();
|
|
} else {
|
|
this.router.navigate([this.successRoute]);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.logService.error(e);
|
|
}
|
|
}
|
|
|
|
togglePassword() {
|
|
this.showPassword = !this.showPassword;
|
|
if (this.ngZone.isStable) {
|
|
document.getElementById("masterPassword").focus();
|
|
} else {
|
|
this.ngZone.onStable
|
|
.pipe(take(1))
|
|
.subscribe(() => document.getElementById("masterPassword").focus());
|
|
}
|
|
}
|
|
|
|
async startAuthRequestLogin() {
|
|
this.formGroup.get("masterPassword")?.clearValidators();
|
|
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
|
|
|
if (!this.formGroup.valid) {
|
|
return;
|
|
}
|
|
|
|
this.setFormValues();
|
|
this.router.navigate(["/login-with-device"]);
|
|
}
|
|
|
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
|
await this.saveEmailSettings();
|
|
// Generate necessary sso params
|
|
const passwordOptions: any = {
|
|
type: "password",
|
|
length: 64,
|
|
uppercase: true,
|
|
lowercase: true,
|
|
numbers: true,
|
|
special: false,
|
|
};
|
|
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
const ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
|
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
|
|
|
// Save sso params
|
|
await this.stateService.setSsoState(state);
|
|
await this.stateService.setSsoCodeVerifier(ssoCodeVerifier);
|
|
|
|
// Build URI
|
|
const webUrl = this.environmentService.getWebVaultUrl();
|
|
|
|
// Launch browser
|
|
this.platformUtilsService.launchUri(
|
|
webUrl +
|
|
"/#/sso?clientId=" +
|
|
clientId +
|
|
"&redirectUri=" +
|
|
encodeURIComponent(ssoRedirectUri) +
|
|
"&state=" +
|
|
state +
|
|
"&codeChallenge=" +
|
|
codeChallenge +
|
|
"&email=" +
|
|
encodeURIComponent(this.formGroup.controls.email.value)
|
|
);
|
|
}
|
|
|
|
async validateEmail() {
|
|
this.formGroup.controls.email.markAsTouched();
|
|
const emailInvalid = this.formGroup.get("email").invalid;
|
|
if (!emailInvalid) {
|
|
this.toggleValidateEmail(true);
|
|
await this.getLoginWithDevice(this.loggedEmail);
|
|
}
|
|
}
|
|
|
|
toggleValidateEmail(value: boolean) {
|
|
this.validatedEmail = value;
|
|
if (!this.validatedEmail) {
|
|
// Reset master password only when going from validated to not validated
|
|
// so that autofill can work properly
|
|
this.formGroup.controls.masterPassword.reset();
|
|
} else {
|
|
// Mark MP as untouched so that, when users enter email and hit enter,
|
|
// the MP field doesn't load with validation errors
|
|
this.formGroup.controls.masterPassword.markAsUntouched();
|
|
|
|
// When email is validated, focus on master password after
|
|
// waiting for input to be rendered
|
|
if (this.ngZone.isStable) {
|
|
this.masterPasswordInput?.nativeElement?.focus();
|
|
} else {
|
|
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
|
|
this.masterPasswordInput?.nativeElement?.focus();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
setFormValues() {
|
|
this.loginService.setEmail(this.formGroup.value.email);
|
|
this.loginService.setRememberEmail(this.formGroup.value.rememberEmail);
|
|
}
|
|
|
|
async saveEmailSettings() {
|
|
this.setFormValues();
|
|
await this.loginService.saveEmailSettings();
|
|
}
|
|
|
|
// Legacy accounts used the master key to encrypt data. Migration is required
|
|
// but only performed on web
|
|
protected handleMigrateEncryptionKey(result: AuthResult): boolean {
|
|
if (!result.requiresEncryptionKeyMigration) {
|
|
return false;
|
|
}
|
|
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("errorOccured"),
|
|
this.i18nService.t("encryptionKeyMigrationRequired")
|
|
);
|
|
return true;
|
|
}
|
|
|
|
private getErrorToastMessage() {
|
|
const error: AllValidationErrors = this.formValidationErrorService
|
|
.getFormValidationErrors(this.formGroup.controls)
|
|
.shift();
|
|
|
|
if (error) {
|
|
switch (error.errorName) {
|
|
case "email":
|
|
return this.i18nService.t("invalidEmail");
|
|
case "minlength":
|
|
return this.i18nService.t("masterPasswordMinlength", Utils.originalMinimumPasswordLength);
|
|
default:
|
|
return this.i18nService.t(this.errorTag(error));
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
private errorTag(error: AllValidationErrors): string {
|
|
const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
|
|
return `${error.controlName}${name}`;
|
|
}
|
|
|
|
async getLoginWithDevice(email: string) {
|
|
try {
|
|
const deviceIdentifier = await this.appIdService.getAppId();
|
|
this.showLoginWithDevice = await this.devicesApiService.getKnownDevice(
|
|
email,
|
|
deviceIdentifier
|
|
);
|
|
} catch (e) {
|
|
this.showLoginWithDevice = false;
|
|
}
|
|
}
|
|
}
|