1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[PM-5085] Create InputPasswordComponent (#9630)

* setup for InputPasswordComponent and basic story

* add all input fields

* add translated error messages

* update validation

* add password-callout

* update hint text

* use PolicyService in component

* setup SetPasswordComponent

* remove div

* add default button text

* add mocks for InputPassword storybook

* simplify ngOnInit

* change param and use PolicyApiService

* check for breaches and validate against policy

* user toastService

* use useValue for mocks

* hash before emitting

* validation cleanup and use PreloadedEnglishI18nModule

* add ngOnDestroy

* create validateFormInputsDoNotMatch fn

* update validateFormInputsComparison and add deprecation jsdocs

* rename validator fn

* fix bugs in validation fn

* cleanup and re-introduce services/logic

* toggle password inputs together

* update hint help text

* remove SetPassword test

* remove master key creation / hashing

* add translations to browser/desktop

* mock basic password-strength functionality

* add check for controls

* hash before emitting

* type the EventEmitter

* use DEFAULT_KDF_CONFIG

* emit master key

* clarify comment

* update password mininum help text to match org policy requirement
This commit is contained in:
rr-bw
2024-06-17 14:56:24 -07:00
committed by GitHub
parent 75615902a3
commit 2a0e21b4bb
8 changed files with 534 additions and 3 deletions

View File

@@ -0,0 +1,192 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
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 { PBKDF2KdfConfig } 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 { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { MasterKey } from "@bitwarden/common/types/key";
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";
export interface PasswordInputResult {
masterKey: MasterKey;
masterKeyHash: string;
kdfConfig: PBKDF2KdfConfig;
hint: string;
}
@Component({
standalone: true,
selector: "auth-input-password",
templateUrl: "./input-password.component.html",
imports: [
AsyncActionsModule,
ButtonModule,
CheckboxModule,
FormFieldModule,
IconButtonModule,
InputModule,
ReactiveFormsModule,
SharedModule,
PasswordCalloutComponent,
JslibModule,
],
})
export class InputPasswordComponent implements OnInit {
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
@Input({ required: true }) email: string;
@Input() protected buttonText: string;
@Input() private orgId: string;
private minHintLength = 0;
protected maxHintLength = 50;
protected minPasswordLength = Utils.minimumPasswordLength;
protected minPasswordMsg = "";
protected masterPasswordPolicy: MasterPasswordPolicyOptions;
protected passwordStrengthResult: any;
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,
private policyApiService: PolicyApiServiceAbstraction,
) {}
async ngOnInit() {
this.masterPasswordPolicy = await this.policyApiService.getMasterPasswordPolicyOptsForOrgUser(
this.orgId,
);
if (this.masterPasswordPolicy != null && this.masterPasswordPolicy.minLength > 0) {
this.minPasswordMsg = this.i18nService.t(
"characterMinimum",
this.masterPasswordPolicy.minLength,
);
} else {
this.minPasswordMsg = this.i18nService.t("characterMinimum", this.minPasswordLength);
}
}
getPasswordStrengthResult(result: any) {
this.passwordStrengthResult = result;
}
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.masterPasswordPolicy != null &&
!this.policyService.evaluateMasterPassword(
this.passwordStrengthResult.score,
password,
this.masterPasswordPolicy,
)
) {
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;
const masterKey = await this.cryptoService.makeMasterKey(
password,
this.email.trim().toLowerCase(),
kdfConfig,
);
const masterKeyHash = await this.cryptoService.hashMasterKey(password, masterKey);
this.onPasswordFormSubmit.emit({
masterKey,
masterKeyHash,
kdfConfig,
hint: this.formGroup.controls.hint.value,
});
};
}