1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00

Auth/PM-5242 - Create new User Verification dialog and form input components which support PIN and biometrics verification (#7536)

* PM-5242 - First working draft of copying out web CL implementation of user verification form and dialog components into standalone libs/auth components which could be used in any client.

* PM-5242 - Rename UserVerificationFormComponent to UserVerificationFormInputComponent b/c it doesn't actually have a form and is meant to slot into a form as an input.

* PM-5242 - Update libs/auth angular index to take renamed component into account

* PM-5242 - Clean up UserVerificationDialogComponent as have much cleaner design approach now (maintain existing func while simply adding new requirements for client side validation for passkeys)

* PM-5242 - UserVerificationFormInput component - WIP draft of new client and server split of user verification logic

* PM-5242 - UserVerificationFormInput - WIP - Lots of progress on client side verification layout - more to do

* PM-5242 - UserVerificationFormInputComponent - Add hasMultipleVerificationMethods property so we can only show alternate methods if user has them.

* PM-5242 - UserVerificationFormInputComponent - rename hasMultipleVerificationMethods to hasMultipleClientVerificationOptions

* PM-5242 - Add new user verification biometrics fingerprint icon with proper secondary fill so it displays properly on all themes.

* PM-5242 - Create enum for tracking client user verification states

* PM-5242 - UserVerificationFormInputComponent - WIP - (1) Got biometrics layout working except for error state (2) Emitting active client verification option and biometrics result to dialog (3) Properly identifying if biometrics is enabled in a platform agnostic way (4) Translations TODO

* PM-5242 - UserVerificationDialogComponent - (1) Wire up new inputs and outputs for UserVerificationFormInput (2) Don't show submit button when clientside biometrics verification active

* PM-5242 - UserVerificationFormInputComponent - wired up biometrics failure and retry handling + re-arranged comp properties to put inputs & outputs at the top

* PM-5242 - UserVerificationFormInput component - Add logic to prevent currently active client verification method from being shown an option

* PM-5242 - UserVerificationFormInput - adjust margins

* PM-5242 - User verification dialog and form input comps - replace Verification with VerificationWithSecret type where applicable

* PM-5242 - UserVerificationFormComp - Default to server for backwards compatibility and to avoid requiring the input at all

* PM-5242 - UserVerificationFormInputComp - (1) Rename processChanges to processSecretChanges (2) Short circuit processSecretChanges when biometrics is active (3) Add new function for determining type of verification that has a secret.

* PM-5242 - UserVerificationDialog - Support custom, optional callout in dialog body.

* PM-5242 - UserVerificationDialogComp - support custom confirm button text and type.

* PM-5242 - UserVerificationDialog - Add user verification dialog result type to allow for handling all possible verification scenarios

* PM-5242 - UserVerificationFormInputComp - tweak comment

* PM-5242 - UserVerificationFormInput comp html - add placeholder text for no client verifications found scenario

* PM-5242 - UserVerificationDialogComponent - (1) Add confirm & cancel to dialog result (2) Add cancel method vs using bitDialogClose for specificity (3) Adjust naming of output property to properly specify that it is scoped to client verification (4) Adjust layout of dialog html to handle when no client side verification methods are found.

* PM-5242 - UserVerificationFormInput - Clean up test code

* PM-5242 - UserVerificationFormInput - For server verification, we don't need to check if the user has a local master key hash as we will generate a hash to send to the server for comparison.

* PM-5242 - UserVerificationFormInput html - Remove now unnecessary dev warning as I've provided a default

* PM-5242 - UserVerification Dialog & Form Input - add translations on all clients for all visible text.

* PM-5242 - UserVerificationFormInput html - remove no active client verification handling from form input comp as it is instead emitted upwards to parent dialog component to be handled there.

* PM-5242 - UserVerificationDialogComp - (1) Make UserVerificationDialogResult.noAvailableClientVerificationMethods optional because it isn't needed in cancel flows (thanks Will) (2) Modify static open to intercept closed observable event in order to always return a UserVerificationDialogResult as BitDialog returns empty string when the user clicks the x

* PM-5242 - UserVerificationDialogComp - Simplify dialog param names to remove redundant dialog

* PM-5242 - UserVerificationDialogParams - update comments to match new names

* PM-5242 - UserVerificationDialog Storybook - WIP first draft

* PM-5242 - UserVerificationDialogStoryComponent - WIP - try out having imports the same as the standalone component

* PM-5242 - UserVerificationDialogStoryComponent - more WIP - building now - some stuff displaying

* PM-5242 - UserVerificationDialogStoryComponent - some progress on providers setup

* PM-5242 - Not going to use storybook for user verification dialog

* PM-5242 - UserVerificationDialogComp - move types into own file + add docs

* PM-5242 - Update auth index to export user-verification-dialog.types

* PM-5242 - UserVerificationFormInput & UserVerificationService - Extract out getAvailableVerificationOptions logic into service

* PM-5242 -UserVerificationDialogComponent - Update close logic to handle escape key undefined scenario

* PM-5242 - UserVerificationFormInput - add getInvalidSecretErrorMessage for properly determining invalid secret translation

* PM-5242 - UserVerificationDialogComp - Refactor submit logic to handle different return methodologies in existing MP and OTP user verification service code vs new PIN flow (e.g., throwing an error instead of returning false)

* PM-5242 - PinCryptoService - change error logs to warnings per discussion with Justin

* PM-5242 - UserVerificationFormInput - Biometrics flow on desktop - remove accidentally added period in couldNotCompleteBiometrics translation key.

* PM-5242 - UserVerificationFormInput HTML - Re-arrange order of other client verification options to match design

* PM-5242 - UserVerificationFormInputComponent - Reset inputs as untouched on change of client verification method.

* PM-5242 - UserVerificationDialogComponent - Remove TODO as existing secret change logic turns invalidSecret false when biometrics is swapped to.

* PM-5242 - UserVerificationFormInputComponent - getInvalidSecretErrorMessage - fix PIN error message not being returned.

* PM-5242 - UserVerificationDialogComponent - Add documentation and examples to open method.

* PM-5242 - UserVerificationDialogComponent - tweak open docs

* PM-5242 - Remove accidental period from translation keys on browser & web

* PM-5242 - UserVerificationFormInputComponent - OTP flow needed button module to work

* PM-5242 - UserVerificationDialogParams - Add docs explaining that noAvailableClientVerificationMethods is only for desktop & browser.

* PM-5242 - User-verification-form-input - Adjust layout to meet new design requirements - (1) On load, send OTP without user clicking a button (2) Allow resending of the codes (3) show a code sent message for 3 seconds

* PM-5242 - Browser User Verification - Instantiate PinCryptoService and UserVerification service AFTER instantiating vaultTimeoutSettingsService so that it isn't undefined at run time.

* PM-5242 - JslibServices Module - UserVerificationService - add missing PlatformUtilsServiceAbstraction dependency.

* PM-5242 - Desktop Native Messaging Service - Wrap biometric getUserKeyFromStorage call in try catch because it throws an error if the user cancels the biometrics prompt and doesn't send a response to the browser extension when using the biometrics unlock bridge to the desktop app and OS.

* PM-5242 - Browser Extension - NativeMessagingBackground - if the desktop biometricUnlock command is executed with a canceled (not adjusting misspelling to keep side effects at a min) response, don't bother continuing.

* PM-5242 - BrowserCryptoService - When retrieving the user key via desktop biometrics, return null for user key if the user fails or cancels the biometrics prompt. Otherwise, if there is a user key in memory after unlock, biometrics user verification will always just return the user key from state regardless of if the user has successfully passed the biometrics prompt or not.

* PM-5242 - BrowserCryptoService - extra comments

* PM-5242 - Clean up translations - (1) Remove unused defaultUserVerificationDialogConfirmBtnText (2) Refactor name of defaultUserVerificationDialogTitle to just be verificationRequired which matches existing naming conventions.

* PM-5242 - CLI - fix order of service instantiations to ensure that vaultTimeoutSettingsService isn't undefined for PinCryptoService and UserVerificationService

* PM-5242 - Rename UserVerificationDialogParams to UserVerificationDialogOptions to match existing naming conventions of other CL comps.

* PM-5242 - UserVerificationDialogComponent - dialogParams renamed to dialogOptions

* PM-5242 - UserVerificationService Abstraction - Per PR feedback, use keyof for verificationType

* PM-5242 - UserVerificationBiometricsIcon - Per PR feedback, use https://jakearchibald.github.io/svgomg/ to optimize SVG by 50%.

* PM-5242 - Per PR feedback, clarify UserVerificationDialogOptions.clientSideOnlyVerification comment.

* PM-5242 - UserVerificationTypes - Add comments clarifying all text passed to the UserVerificationDialog are translation keys

* PM-5242 - UserVerificationDialogComp - fix extra new line per PR feedback

* PM-5242 - UserVerificationDialogTypes - per PR feedback and discussion with Will M., export ButtonType from CL so we (and consumers of the dialog) can properly import it via standard CL import.

* PM-5242 - BrowserCryptoService - Adjust comments per PR feedback.

* PM-5242 - UserVerificationDialogComponent - make ActiveClientVerificationOption readonly as it only for component html

* PM-5242 - UserVerificationDialogComp html - finish comment

* PM-5242 - BrowserCryptoService - add returns js doc per PR feedback.

* PM-5242 - UserVerificationDialogComponent - per PR feedback, add unexpected error toast.

* PM-5242  - UserVerificationService - getAvailableVerificationOptions - update params to use keyof like abstraction

* PM-5242 - Mark all existing client specific implemetations of user verification as deprecated.
This commit is contained in:
Jared Snider
2024-01-25 14:03:27 -05:00
committed by GitHub
parent 45c0c09b71
commit 2c1d215b71
30 changed files with 1276 additions and 47 deletions

View File

@@ -0,0 +1,340 @@
import { animate, style, transition, trigger } from "@angular/animations";
import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import {
ControlValueAccessor,
FormControl,
Validators,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from "@angular/forms";
import { BehaviorSubject, Subject, takeUntil } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { UserVerificationOptions } from "@bitwarden/common/auth/types/user-verification-options";
import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
AsyncActionsModule,
ButtonModule,
FormFieldModule,
IconButtonModule,
IconModule,
LinkModule,
} from "@bitwarden/components";
import { UserVerificationBiometricsIcon } from "../icons";
import { ActiveClientVerificationOption } from "./active-client-verification-option.enum";
/**
* Used for general-purpose user verification throughout the app.
* Collects the user's master password, or if they are not using a password, prompts for an OTP via email.
* This is exposed to the parent component via the ControlValueAccessor interface (e.g. bind it to a FormControl).
* Use UserVerificationService to verify the user's input.
*/
@Component({
selector: "app-user-verification-form-input",
templateUrl: "user-verification-form-input.component.html",
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: UserVerificationFormInputComponent,
},
],
animations: [
trigger("sent", [
transition(":enter", [style({ opacity: 0 }), animate("100ms", style({ opacity: 1 }))]),
]),
],
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
JslibModule,
FormFieldModule,
AsyncActionsModule,
IconButtonModule,
IconModule,
LinkModule,
ButtonModule,
],
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class UserVerificationFormInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
@Input() verificationType: "server" | "client" = "server"; // server represents original behavior
private _invalidSecret = false;
@Input()
get invalidSecret() {
return this._invalidSecret;
}
set invalidSecret(value: boolean) {
this._invalidSecret = value;
this.invalidSecretChange.emit(value);
// ISSUE: This is pretty hacky but unfortunately there is no way of knowing if the parent
// control has been marked as touched, see: https://github.com/angular/angular/issues/10887
// When that functionality has been added we should also look into forwarding reactive form
// controls errors so that we don't need a separate input/output `invalidSecret`.
if (value) {
this.secret.markAsTouched();
}
this.secret.updateValueAndValidity({ emitEvent: false });
}
@Output() invalidSecretChange = new EventEmitter<boolean>();
@Output() activeClientVerificationOptionChange =
new EventEmitter<ActiveClientVerificationOption>();
@Output() biometricsVerificationResultChange = new EventEmitter<boolean>();
readonly Icons = { UserVerificationBiometricsIcon };
// default to false to avoid null checks in template
userVerificationOptions: UserVerificationOptions = {
client: {
masterPassword: false,
pin: false,
biometrics: false,
},
server: {
masterPassword: false,
otp: false,
},
};
ActiveClientVerificationOption = ActiveClientVerificationOption;
private _activeClientVerificationOptionSubject =
new BehaviorSubject<ActiveClientVerificationOption>(null);
activeClientVerificationOption$ = this._activeClientVerificationOptionSubject.asObservable();
set activeClientVerificationOption(value: ActiveClientVerificationOption) {
this._activeClientVerificationOptionSubject.next(value);
}
get activeClientVerificationOption(): ActiveClientVerificationOption {
return this._activeClientVerificationOptionSubject.getValue();
}
get hasMultipleClientVerificationOptions(): boolean {
let optionsCount = 0;
if (this.userVerificationOptions.client.masterPassword) {
optionsCount++;
}
if (this.userVerificationOptions.client.pin) {
optionsCount++;
}
if (this.userVerificationOptions.client.biometrics) {
optionsCount++;
}
return optionsCount >= 2;
}
biometricsVerificationFailed = false;
disableRequestOTP = false;
sentInitialCode = false;
sentCode = false;
secret = new FormControl("", [
Validators.required,
() => {
if (this.invalidSecret) {
return {
invalidSecret: {
message: this.getInvalidSecretErrorMessage(),
},
};
}
},
]);
private getInvalidSecretErrorMessage(): string {
// must determine client or server
if (this.verificationType === "server") {
return this.userVerificationOptions.server.masterPassword
? this.i18nService.t("incorrectPassword")
: this.i18nService.t("incorrectCode");
} else {
// client
if (this.activeClientVerificationOption === ActiveClientVerificationOption.MasterPassword) {
return this.i18nService.t("incorrectPassword");
} else if (this.activeClientVerificationOption === ActiveClientVerificationOption.Pin) {
return this.i18nService.t("incorrectPin");
}
}
}
private onChange: (value: VerificationWithSecret) => void;
private destroy$ = new Subject<void>();
constructor(
private userVerificationService: UserVerificationService,
private i18nService: I18nService,
) {}
async ngOnInit() {
this.userVerificationOptions =
await this.userVerificationService.getAvailableVerificationOptions(this.verificationType);
if (this.verificationType === "client") {
this.setDefaultActiveClientVerificationOption();
this.setupClientVerificationOptionChangeHandler();
} else {
if (this.userVerificationOptions.server.otp) {
// New design requires requesting on load to prevent user from having to click send code
this.requestOTP();
}
}
// Don't bother executing secret changes if biometrics verification is active.
if (this.activeClientVerificationOption === ActiveClientVerificationOption.Biometrics) {
this.processSecretChanges(this.secret.value);
}
this.secret.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe((secret: string) => this.processSecretChanges(secret));
}
private setDefaultActiveClientVerificationOption(): void {
// Priorities should be Bio > Pin > Master Password for speed based on design
if (this.userVerificationOptions.client.biometrics) {
this.activeClientVerificationOption = ActiveClientVerificationOption.Biometrics;
} else if (this.userVerificationOptions.client.pin) {
this.activeClientVerificationOption = ActiveClientVerificationOption.Pin;
} else if (this.userVerificationOptions.client.masterPassword) {
this.activeClientVerificationOption = ActiveClientVerificationOption.MasterPassword;
} else {
this.activeClientVerificationOption = ActiveClientVerificationOption.None;
}
}
private setupClientVerificationOptionChangeHandler(): void {
this.activeClientVerificationOption$
.pipe(takeUntil(this.destroy$))
.subscribe((activeClientVerificationOption: ActiveClientVerificationOption) => {
this.handleActiveClientVerificationOptionChange(activeClientVerificationOption);
});
}
private async handleActiveClientVerificationOptionChange(
activeClientVerificationOption: ActiveClientVerificationOption,
): Promise<void> {
// Emit to parent component so it can implement behavior if needed.
this.activeClientVerificationOptionChange.emit(activeClientVerificationOption);
// clear secret value when switching verification methods
this.secret.setValue(null);
// Reset validation errors when swapping active client verification options
this.secret.markAsUntouched();
this.secret.updateValueAndValidity({ emitEvent: false });
// if changing to biometrics, we need to prompt for biometrics
if (activeClientVerificationOption === "biometrics") {
// reset biometrics failed
this.biometricsVerificationFailed = false;
await this.verifyUserViaBiometrics();
}
}
async verifyUserViaBiometrics() {
this.biometricsVerificationFailed = false;
const biometricsResult = await this.userVerificationService.verifyUser({
type: VerificationType.Biometrics,
});
this.biometricsVerificationResultChange.emit(biometricsResult);
this.biometricsVerificationFailed = !biometricsResult;
}
requestOTP = async () => {
if (!this.userVerificationOptions.server.masterPassword) {
this.disableRequestOTP = true;
try {
await this.userVerificationService.requestOTP();
this.sentCode = true;
this.sentInitialCode = true;
// after 3 seconds reset sentCode to false
setTimeout(() => {
this.sentCode = false;
}, 3000);
} finally {
this.disableRequestOTP = false;
}
}
};
writeValue(obj: any): void {
this.secret.setValue(obj);
}
/** Required for NG_VALUE_ACCESSOR */
registerOnChange(fn: any): void {
this.onChange = fn;
}
/** Required for NG_VALUE_ACCESSOR */
registerOnTouched(fn: any): void {
// Not implemented
}
setDisabledState?(isDisabled: boolean): void {
this.disableRequestOTP = isDisabled;
if (isDisabled) {
this.secret.disable();
} else {
this.secret.enable();
}
}
processSecretChanges(secret: string) {
this.invalidSecret = false;
// Short circuit secret change handling when biometrics is chosen as biometrics has no secret
if (this.activeClientVerificationOption === ActiveClientVerificationOption.Biometrics) {
return;
}
if (this.onChange == null) {
return;
}
this.onChange({
type: this.determineVerificationWithSecretType(),
secret: Utils.isNullOrWhitespace(secret) ? null : secret,
});
}
private determineVerificationWithSecretType():
| VerificationType.MasterPassword
| VerificationType.OTP
| VerificationType.PIN {
if (this.verificationType === "server") {
return this.userVerificationOptions.server.masterPassword
? VerificationType.MasterPassword
: VerificationType.OTP;
} else {
// client
return this.userVerificationOptions.client.masterPassword &&
this.activeClientVerificationOption === ActiveClientVerificationOption.MasterPassword
? VerificationType.MasterPassword
: VerificationType.PIN;
}
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}