mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 06:13:38 +00:00
* 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
196 lines
6.0 KiB
TypeScript
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,
|
|
});
|
|
};
|
|
}
|