1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00
Files
browser/libs/auth/src/angular/input-password/input-password.component.ts
rr-bw 9355a9bb43 [PM-7330] Create SetPasswordComponent (email verification) (#9810)
* setup SetPassword component

* accept query params

* add InputPasswordComponent to template

* add route

* add dynamic translation with org name

* feature flag route

* setup onInit

* add set password logic

* move to libs

* remove comments

* update AuthGuard routing

* use ToastService

* replace deprecated methods

* replace orgId input with policy input

* use getter for msg instead of ngOnInit

* cleanup

* refactor to use services

* more refactoring of service

* address browser routing and translations

* add desktop service

* simplify queryParam handler

* remove ngOnDestroy

* small edits

* use inject()

* add jsdocs

* create basic tests

* add success toasts on successfuly set password

* add tests

* update feature-flag

* move model to service

* refactor client services to override setPassword()

* add error handling to setPassword()

* move auto enroll logic to service

* update tests

* fix test

* adjust padding on password-callout list

* revert refactor of auto enroll logic

* refactor keyPair generation to own method

* update page title and button text

* update pageSubtitle and translations

* fix test
2024-07-25 07:42:32 -07:00

196 lines
6.0 KiB
TypeScript

import { Component, EventEmitter, Input, Output } from "@angular/core";
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import {
PasswordStrengthScore,
PasswordStrengthV2Component,
} from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
CheckboxModule,
DialogService,
FormFieldModule,
IconButtonModule,
InputModule,
ToastService,
} from "@bitwarden/components";
import { InputsFieldMatch } from "../../../../angular/src/auth/validators/inputs-field-match.validator";
import { SharedModule } from "../../../../components/src/shared";
import { PasswordCalloutComponent } from "../password-callout/password-callout.component";
import { PasswordInputResult } from "./password-input-result";
@Component({
standalone: true,
selector: "auth-input-password",
templateUrl: "./input-password.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CheckboxModule,
FormFieldModule,
IconButtonModule,
InputModule,
ReactiveFormsModule,
SharedModule,
PasswordCalloutComponent,
PasswordStrengthV2Component,
JslibModule,
],
})
export class InputPasswordComponent {
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
@Input({ required: true }) email: string;
@Input() buttonText: string;
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
@Input() loading: boolean = false;
private minHintLength = 0;
protected maxHintLength = 50;
protected minPasswordLength = Utils.minimumPasswordLength;
protected minPasswordMsg = "";
protected passwordStrengthScore: PasswordStrengthScore;
protected showErrorSummary = false;
protected showPassword = false;
protected formGroup = this.formBuilder.group(
{
password: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
confirmedPassword: ["", Validators.required],
hint: [
"", // must be string (not null) because we check length in validation
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
],
checkForBreaches: true,
},
{
validators: [
InputsFieldMatch.compareInputs(
"match",
"password",
"confirmedPassword",
this.i18nService.t("masterPassDoesntMatch"),
),
InputsFieldMatch.compareInputs(
"doNotMatch",
"password",
"hint",
this.i18nService.t("hintEqualsPassword"),
),
],
},
);
constructor(
private auditService: AuditService,
private cryptoService: CryptoService,
private dialogService: DialogService,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private policyService: PolicyService,
private toastService: ToastService,
) {}
get minPasswordLengthMsg() {
if (
this.masterPasswordPolicyOptions != null &&
this.masterPasswordPolicyOptions.minLength > 0
) {
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
} else {
return this.i18nService.t("characterMinimum", this.minPasswordLength);
}
}
getPasswordStrengthScore(score: PasswordStrengthScore) {
this.passwordStrengthScore = score;
}
protected submit = async () => {
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
this.showErrorSummary = true;
return;
}
const password = this.formGroup.controls.password.value;
// Check if password is breached (if breached, user chooses to accept and continue or not)
const passwordIsBreached =
this.formGroup.controls.checkForBreaches.value &&
(await this.auditService.passwordLeaked(password));
if (passwordIsBreached) {
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
title: { key: "exposedMasterPassword" },
content: { key: "exposedMasterPasswordDesc" },
type: "warning",
});
if (!userAcceptedDialog) {
return;
}
}
// Check if password meets org policy requirements
if (
this.masterPasswordPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.passwordStrengthScore,
password,
this.masterPasswordPolicyOptions,
)
) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
});
return;
}
// Create and hash new master key
const kdfConfig = DEFAULT_KDF_CONFIG;
if (this.email == null) {
throw new Error("Email is required to create master key.");
}
const masterKey = await this.cryptoService.makeMasterKey(
password,
this.email.trim().toLowerCase(),
kdfConfig,
);
const masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
const localMasterKeyHash = await this.cryptoService.hashMasterKey(
password,
masterKey,
HashPurpose.LocalAuthorization,
);
this.onPasswordFormSubmit.emit({
masterKey,
masterKeyHash,
localMasterKeyHash,
kdfConfig,
hint: this.formGroup.controls.hint.value,
});
};
}