mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +00:00
* Introduce `verificationType` * Update template to use `verificationType` * Implement a path for `verificationType = 'custom'` * Delete `clientSideOnlyVerification` * Update `EnrollMasterPasswordResetComponent` to include a server-side hash check * Better describe the custom scenerio through comments * Add an example of the custom verficiation scenerio * Move execution of verification function into try/catch * Migrate existing uses of `clientSideOnlyVerification` * Use generic type option instead of casting * Change "given" to "determined" in a comment
287 lines
9.2 KiB
TypeScript
287 lines
9.2 KiB
TypeScript
import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog";
|
|
import { CommonModule } from "@angular/common";
|
|
import { Component, Inject } from "@angular/core";
|
|
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
|
import { firstValueFrom } from "rxjs";
|
|
|
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
|
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
|
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
import {
|
|
AsyncActionsModule,
|
|
ButtonModule,
|
|
CalloutModule,
|
|
DialogModule,
|
|
DialogService,
|
|
} from "@bitwarden/components";
|
|
|
|
import { ActiveClientVerificationOption } from "./active-client-verification-option.enum";
|
|
import {
|
|
UserVerificationDialogOptions,
|
|
UserVerificationDialogResult,
|
|
} from "./user-verification-dialog.types";
|
|
import { UserVerificationFormInputComponent } from "./user-verification-form-input.component";
|
|
|
|
@Component({
|
|
templateUrl: "user-verification-dialog.component.html",
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
ReactiveFormsModule,
|
|
JslibModule,
|
|
ButtonModule,
|
|
DialogModule,
|
|
AsyncActionsModule,
|
|
UserVerificationFormInputComponent,
|
|
CalloutModule,
|
|
],
|
|
})
|
|
export class UserVerificationDialogComponent {
|
|
verificationForm = this.formBuilder.group({
|
|
secret: this.formBuilder.control<VerificationWithSecret | null>(null),
|
|
});
|
|
|
|
get secret() {
|
|
return this.verificationForm.controls.secret;
|
|
}
|
|
|
|
invalidSecret = false;
|
|
activeClientVerificationOption: ActiveClientVerificationOption;
|
|
readonly ActiveClientVerificationOption = ActiveClientVerificationOption;
|
|
|
|
constructor(
|
|
@Inject(DIALOG_DATA) public dialogOptions: UserVerificationDialogOptions,
|
|
private dialogRef: DialogRef<UserVerificationDialogResult | string>,
|
|
private formBuilder: FormBuilder,
|
|
private userVerificationService: UserVerificationService,
|
|
private platformUtilsService: PlatformUtilsService,
|
|
private i18nService: I18nService,
|
|
) {}
|
|
|
|
/**
|
|
* Opens the user verification dialog.
|
|
*
|
|
* @param {DialogService} dialogService - The service used to open the dialog.
|
|
* @param {UserVerificationDialogOptions} data - Parameters for configuring the dialog.
|
|
* @returns {Promise<UserVerificationDialogResult>} A promise that resolves to the result of the user verification process.
|
|
*
|
|
* @example
|
|
* // Example 1: Default, simple scenario
|
|
* const result = await UserVerificationDialogComponent.open(
|
|
* this.dialogService,
|
|
* {}
|
|
* );
|
|
*
|
|
* // Handle the result of the dialog based on user action and verification success
|
|
* if (result.userAction === 'cancel') {
|
|
* // User cancelled the dialog
|
|
* return;
|
|
* }
|
|
*
|
|
* // User confirmed the dialog so check verification success
|
|
* if (!result.verificationSuccess) {
|
|
* // verification failed
|
|
* return;
|
|
* }
|
|
*
|
|
* ----------------------------------------------------------
|
|
*
|
|
* @example
|
|
* // Example 2: Custom scenario
|
|
* const result = await UserVerificationDialogComponent.open(
|
|
* this.dialogService,
|
|
* {
|
|
* title: 'customTitle',
|
|
* bodyText: 'customBodyText',
|
|
* calloutOptions: {
|
|
* text: 'customCalloutText',
|
|
* type: 'warning',
|
|
* },
|
|
* confirmButtonOptions: {
|
|
* text: 'customConfirmButtonText',
|
|
* type: 'danger',
|
|
* }
|
|
* }
|
|
* );
|
|
*
|
|
* // Handle the result of the dialog based on user action and verification success
|
|
* if (result.userAction === 'cancel') {
|
|
* // User cancelled the dialog
|
|
* return;
|
|
* }
|
|
*
|
|
* // User confirmed the dialog so check verification success
|
|
* if (!result.verificationSuccess) {
|
|
* // verification failed
|
|
* return;
|
|
* }
|
|
*
|
|
* ----------------------------------------------------------
|
|
*
|
|
* @example
|
|
* // Example 3: Client side verification scenario only
|
|
* const result = await UserVerificationDialogComponent.open(
|
|
* this.dialogService,
|
|
* { clientSideOnlyVerification: true }
|
|
* );
|
|
*
|
|
* // Handle the result of the dialog based on user action and verification success
|
|
* if (result.userAction === 'cancel') {
|
|
* // User cancelled the dialog
|
|
* return;
|
|
* }
|
|
*
|
|
* // User confirmed the dialog so check verification success
|
|
* if (!result.verificationSuccess) {
|
|
* if (result.noAvailableClientVerificationMethods) {
|
|
* // No client-side verification methods are available
|
|
* // Could send user to configure a verification method like PIN or biometrics
|
|
* }
|
|
* return;
|
|
* }
|
|
*
|
|
* ----------------------------------------------------------
|
|
*
|
|
* @example
|
|
* // Example 4: Custom user verification validation
|
|
*
|
|
* const result = await UserVerificationDialogComponent.open(dialogService, {
|
|
* verificationType: {
|
|
* type: "custom",
|
|
* // Pass in a function that will be used to validate the input of the
|
|
* // verification dialog, returning true when finished.
|
|
* verificationFn: async (secret: VerificationWithSecret) => {
|
|
* const request = await userVerificationService.buildRequest<CustomRequestType>(secret);
|
|
*
|
|
* // ... Do something with the custom request type
|
|
*
|
|
* await someServicer.sendMyRequestThatVerfiesUserIdentity(
|
|
* // ... Some other data
|
|
* request,
|
|
* );
|
|
* return true;
|
|
* },
|
|
* },
|
|
* });
|
|
*
|
|
* // ... Evaluate the result as usual
|
|
*/
|
|
static async open(
|
|
dialogService: DialogService,
|
|
data: UserVerificationDialogOptions,
|
|
): Promise<UserVerificationDialogResult> {
|
|
const dialogRef = dialogService.open<UserVerificationDialogResult | string>(
|
|
UserVerificationDialogComponent,
|
|
{
|
|
data,
|
|
},
|
|
);
|
|
|
|
const dialogResult = await firstValueFrom(dialogRef.closed);
|
|
|
|
// An empty string is returned when the user hits the x to close the dialog.
|
|
// Undefined is returned when the users hits the escape key to close the dialog.
|
|
if (typeof dialogResult === "string" || dialogResult === undefined) {
|
|
// User used x to close dialog
|
|
return {
|
|
userAction: "cancel",
|
|
verificationSuccess: false,
|
|
};
|
|
} else {
|
|
return dialogResult;
|
|
}
|
|
}
|
|
|
|
handleActiveClientVerificationOptionChange(
|
|
activeClientVerificationOption: ActiveClientVerificationOption,
|
|
) {
|
|
this.activeClientVerificationOption = activeClientVerificationOption;
|
|
}
|
|
|
|
handleBiometricsVerificationResultChange(biometricsVerificationResult: boolean) {
|
|
if (biometricsVerificationResult) {
|
|
this.close({
|
|
userAction: "confirm",
|
|
verificationSuccess: true,
|
|
noAvailableClientVerificationMethods: false,
|
|
});
|
|
}
|
|
}
|
|
|
|
submit = async () => {
|
|
if (this.activeClientVerificationOption === ActiveClientVerificationOption.None) {
|
|
this.close({
|
|
userAction: "confirm",
|
|
verificationSuccess: false,
|
|
noAvailableClientVerificationMethods: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.verificationForm.markAllAsTouched();
|
|
|
|
if (this.verificationForm.invalid) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (
|
|
typeof this.dialogOptions.verificationType === "object" &&
|
|
this.dialogOptions.verificationType.type === "custom"
|
|
) {
|
|
const success = await this.dialogOptions.verificationType.verificationFn(this.secret.value);
|
|
this.close({
|
|
userAction: "confirm",
|
|
verificationSuccess: success,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// TODO: once we migrate all user verification scenarios to use this new implementation,
|
|
// we should consider refactoring the user verification service handling of the
|
|
// OTP and MP flows to not throw errors on verification failure.
|
|
const verificationResult = await this.userVerificationService.verifyUser(this.secret.value);
|
|
|
|
if (verificationResult) {
|
|
this.invalidSecret = false;
|
|
this.close({
|
|
userAction: "confirm",
|
|
verificationSuccess: true,
|
|
noAvailableClientVerificationMethods: false,
|
|
});
|
|
} else {
|
|
this.invalidSecret = true;
|
|
|
|
// Only pin should ever get here, but added this check to be safe.
|
|
if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) {
|
|
this.platformUtilsService.showToast(
|
|
"error",
|
|
this.i18nService.t("error"),
|
|
this.i18nService.t("invalidPin"),
|
|
);
|
|
} else {
|
|
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Catch handles OTP and MP verification scenarios as those throw errors on verification failure instead of returning false like PIN and biometrics.
|
|
this.invalidSecret = true;
|
|
this.platformUtilsService.showToast("error", this.i18nService.t("error"), e.message);
|
|
return;
|
|
}
|
|
};
|
|
|
|
cancel() {
|
|
this.close({
|
|
userAction: "cancel",
|
|
verificationSuccess: false,
|
|
});
|
|
}
|
|
|
|
close(dialogResult: UserVerificationDialogResult) {
|
|
this.dialogRef.close(dialogResult);
|
|
}
|
|
}
|