bitwarden-estensione-browser/libs/angular/src/auth/components/register.component.ts

339 lines
12 KiB
TypeScript

import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { AbstractControl, UntypedFormBuilder, ValidatorFn, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { LoginStrategyServiceAbstraction, PasswordLoginCredentials } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { RegisterResponse } from "@bitwarden/common/auth/models/response/register.response";
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
import { ReferenceEventRequest } from "@bitwarden/common/models/request/reference-event.request";
import { RegisterRequest } from "@bitwarden/common/models/request/register.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { DialogService } from "@bitwarden/components";
import {
AllValidationErrors,
FormValidationErrorsService,
} from "../../platform/abstractions/form-validation-errors.service";
import { PasswordColorText } from "../../tools/password-strength/password-strength.component";
import { InputsFieldMatch } from "../validators/inputs-field-match.validator";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@Directive()
export class RegisterComponent extends CaptchaProtectedComponent implements OnInit {
@Input() isInTrialFlow = false;
@Output() createdAccount = new EventEmitter<string>();
showPassword = false;
formPromise: Promise<RegisterResponse>;
referenceData: ReferenceEventRequest;
showTerms = true;
showErrorSummary = false;
passwordStrengthResult: any;
characterMinimumMessage: string;
minimumLength = Utils.minimumPasswordLength;
color: string;
text: string;
formGroup = this.formBuilder.group(
{
email: ["", [Validators.required, Validators.email]],
name: [""],
masterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
confirmMasterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
hint: [
null,
[
InputsFieldMatch.validateInputsDoesntMatch(
"masterPassword",
this.i18nService.t("hintEqualsPassword"),
),
],
],
checkForBreaches: [true],
acceptPolicies: [false, [this.acceptPoliciesValidation()]],
},
{
validator: InputsFieldMatch.validateFormInputsMatch(
"masterPassword",
"confirmMasterPassword",
this.i18nService.t("masterPassDoesntMatch"),
),
},
);
protected successRoute = "login";
protected accountCreated = false;
protected captchaBypassToken: string = null;
constructor(
protected formValidationErrorService: FormValidationErrorsService,
protected formBuilder: UntypedFormBuilder,
protected loginStrategyService: LoginStrategyServiceAbstraction,
protected router: Router,
i18nService: I18nService,
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected stateService: StateService,
platformUtilsService: PlatformUtilsService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
environmentService: EnvironmentService,
protected logService: LogService,
protected auditService: AuditService,
protected dialogService: DialogService,
) {
super(environmentService, i18nService, platformUtilsService);
this.showTerms = !platformUtilsService.isSelfHost();
this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
}
async ngOnInit() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.setupCaptcha();
}
async submit(showToast = true) {
let email = this.formGroup.value.email;
email = email.trim().toLowerCase();
let name = this.formGroup.value.name;
name = name === "" ? null : name; // Why do we do this?
const masterPassword = this.formGroup.value.masterPassword;
try {
if (!this.accountCreated) {
const registerResponse = await this.registerAccount(
await this.buildRegisterRequest(email, masterPassword, name),
showToast,
);
if (!registerResponse.successful) {
return;
}
this.captchaBypassToken = registerResponse.captchaBypassToken;
this.accountCreated = true;
}
if (this.isInTrialFlow) {
if (!this.accountCreated) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("trialAccountCreated"),
);
}
const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken);
if (loginResponse.captchaRequired) {
return;
}
this.createdAccount.emit(this.formGroup.value.email);
} else {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("newAccountCreated"),
);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate([this.successRoute], { queryParams: { email: email } });
}
} catch (e) {
this.logService.error(e);
}
}
togglePassword() {
this.showPassword = !this.showPassword;
}
getStrengthResult(result: any) {
this.passwordStrengthResult = result;
}
getPasswordScoreText(event: PasswordColorText) {
this.color = event.color;
this.text = event.text;
}
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 "inputsDoesntMatchError":
return this.i18nService.t("masterPassDoesntMatch");
case "inputsMatchError":
return this.i18nService.t("hintEqualsPassword");
case "minlength":
return this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength);
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}`;
}
//validation would be ignored on selfhosted
private acceptPoliciesValidation(): ValidatorFn {
return (control: AbstractControl) => {
const ctrlValue = control.value;
return !ctrlValue && this.showTerms ? { required: true } : null;
};
}
private async validateRegistration(showToast: boolean): Promise<{ isValid: boolean }> {
this.formGroup.markAllAsTouched();
this.showErrorSummary = true;
if (this.formGroup.get("acceptPolicies").hasError("required")) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("acceptPoliciesRequired"),
);
return { isValid: false };
}
//web
if (this.formGroup.invalid && !showToast) {
return { isValid: false };
}
//desktop, browser
if (this.formGroup.invalid && showToast) {
const errorText = this.getErrorToastMessage();
this.platformUtilsService.showToast("error", this.i18nService.t("errorOccurred"), errorText);
return { isValid: false };
}
const passwordWeak =
this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3;
const passwordLeak =
this.formGroup.controls.checkForBreaches.value &&
(await this.auditService.passwordLeaked(this.formGroup.controls.masterPassword.value)) > 0;
if (passwordWeak && passwordLeak) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakAndExposedMasterPassword" },
content: { key: "weakAndBreachedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return { isValid: false };
}
} else if (passwordWeak) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "weakMasterPassword" },
content: { key: "weakMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return { isValid: false };
}
} else if (passwordLeak) {
const result = await this.dialogService.openSimpleDialog({
title: { key: "exposedMasterPassword" },
content: { key: "exposedMasterPasswordDesc" },
type: "warning",
});
if (!result) {
return { isValid: false };
}
}
return { isValid: true };
}
private async buildRegisterRequest(
email: string,
masterPassword: string,
name: string,
): Promise<RegisterRequest> {
const hint = this.formGroup.value.hint;
const kdfConfig = DEFAULT_KDF_CONFIG;
const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig);
const newUserKey = await this.cryptoService.makeUserKey(key);
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key);
const keys = await this.cryptoService.makeKeyPair(newUserKey[0]);
const request = new RegisterRequest(
email,
name,
masterKeyHash,
hint,
newUserKey[1].encryptedString,
this.referenceData,
this.captchaToken,
kdfConfig.kdfType,
kdfConfig.iterations,
);
request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
const orgInvite = await this.stateService.getOrganizationInvitation();
if (orgInvite != null && orgInvite.token != null && orgInvite.organizationUserId != null) {
request.token = orgInvite.token;
request.organizationUserId = orgInvite.organizationUserId;
}
return request;
}
private async registerAccount(
request: RegisterRequest,
showToast: boolean,
): Promise<{ successful: boolean; captchaBypassToken?: string }> {
if (!(await this.validateRegistration(showToast)).isValid) {
return { successful: false };
}
this.formPromise = this.apiService.postRegister(request);
try {
const response = await this.formPromise;
return { successful: true, captchaBypassToken: response.captchaBypassToken };
} catch (e) {
if (this.handleCaptchaRequired(e)) {
return { successful: false };
} else {
throw e;
}
}
}
private async logIn(
email: string,
masterPassword: string,
captchaBypassToken: string,
): Promise<{ captchaRequired: boolean }> {
const credentials = new PasswordLoginCredentials(
email,
masterPassword,
captchaBypassToken,
null,
);
const loginResponse = await this.loginStrategyService.logIn(credentials);
if (this.handleCaptchaRequired(loginResponse)) {
return { captchaRequired: true };
}
return { captchaRequired: false };
}
}