1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02: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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 1276 additions and 47 deletions

View File

@ -2006,6 +2006,10 @@
"message": "Your organization requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired" : {
"message": "Verification required",
"description": "Default title for the user verification dialog."
},
"hours": {
"message": "Hours"
},
@ -2643,6 +2647,42 @@
}
}
},
"tryAgain": {
"message": "Try again"
},
"verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue."
},
"setPin": {
"message": "Set PIN"
},
"verifyWithBiometrics": {
"message": "Verify with biometrics"
},
"awaitingConfirmation": {
"message": "Awaiting confirmation"
},
"couldNotCompleteBiometrics": {
"message": "Could not complete biometrics."
},
"needADifferentMethod": {
"message": "Need a different method?"
},
"useMasterPassword": {
"message": "Use master password"
},
"usePin": {
"message": "Use PIN"
},
"useBiometrics": {
"message": "Use biometrics"
},
"enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email."
},
"resendCode": {
"message": "Resend code"
},
"total": {
"message": "Total"
},
@ -2786,6 +2826,15 @@
"incorrectUsernameOrPassword": {
"message": "Incorrect username or password"
},
"incorrectPassword": {
"message": "Incorrect password"
},
"incorrectCode": {
"message": "Incorrect code"
},
"incorrectPin": {
"message": "Incorrect PIN"
},
"multifactorAuthenticationFailed": {
"message": "Multifactor authentication failed"
},

View File

@ -1,6 +1,10 @@
import { UserVerificationService as AbstractUserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import {
VaultTimeoutSettingsServiceInitOptions,
vaultTimeoutSettingsServiceFactory,
} from "../../../background/service-factories/vault-timeout-settings-service.factory";
import {
CryptoServiceInitOptions,
cryptoServiceFactory,
@ -18,6 +22,10 @@ import {
LogServiceInitOptions,
logServiceFactory,
} from "../../../platform/background/service-factories/log-service.factory";
import {
platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions,
} from "../../../platform/background/service-factories/platform-utils-service.factory";
import {
StateServiceInitOptions,
stateServiceFactory,
@ -37,7 +45,9 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
I18nServiceInitOptions &
UserVerificationApiServiceInitOptions &
PinCryptoServiceInitOptions &
LogServiceInitOptions;
LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions;
export function userVerificationServiceFactory(
cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices,
@ -55,6 +65,8 @@ export function userVerificationServiceFactory(
await userVerificationApiServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts),
await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts),
),
);
}

View File

@ -489,22 +489,6 @@ export default class MainBackground {
this.userVerificationApiService = new UserVerificationApiService(this.apiService);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
this.i18nService,
this.userVerificationApiService,
this.pinCryptoService,
this.logService,
);
this.configApiService = new ConfigApiService(this.apiService, this.authService);
this.configService = new BrowserConfigService(
@ -542,6 +526,24 @@ export default class MainBackground {
this.stateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
);
this.userVerificationService = new UserVerificationService(
this.stateService,
this.cryptoService,
this.i18nService,
this.userVerificationApiService,
this.pinCryptoService,
this.logService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
);
this.vaultFilterService = new VaultFilterService(
this.stateService,
this.organizationService,

View File

@ -306,8 +306,11 @@ export class NativeMessagingBackground {
type: "danger",
});
break;
} else if (message.response === "canceled") {
break;
}
// Check for initial setup of biometric unlock
const enabled = await this.stateService.getBiometricUnlock();
if (enabled === null || enabled === false) {
if (message.response === "unlocked") {

View File

@ -16,13 +16,19 @@ export class BrowserCryptoService extends CryptoService {
/**
* Browser doesn't store biometric keys, so we retrieve them from the desktop and return
* if we successfully saved it into memory as the User Key
* @returns the `UserKey` if the user passes a biometrics prompt, otherwise return `null`.
*/
protected override async getKeyFromStorage(
keySuffix: KeySuffixOptions,
userId?: UserId,
): Promise<UserKey> {
if (keySuffix === KeySuffixOptions.Biometric) {
await this.platformUtilService.authenticateBiometric();
const biometricsResult = await this.platformUtilService.authenticateBiometric();
if (!biometricsResult) {
return null;
}
const userKey = await this.stateService.getUserKey({ userId: userId });
if (userKey) {
return new SymmetricCryptoKey(Utils.fromB64ToArray(userKey.keyB64)) as UserKey;

View File

@ -3,7 +3,10 @@ import { Component } from "@angular/core";
import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/auth/components/user-verification.component";
/**
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead.
* Each client specific component should eventually be converted over to use one of these new components.
*/
@Component({
selector: "app-user-verification",
templateUrl: "user-verification.component.html",

View File

@ -437,6 +437,13 @@ export class Main {
const lockedCallback = async (userId?: string) =>
await this.cryptoService.clearStoredUserKey(KeySuffixOptions.Auto);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.cryptoService,
this.tokenService,
this.policyService,
this.stateService,
);
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
@ -451,13 +458,8 @@ export class Main {
this.userVerificationApiService,
this.pinCryptoService,
this.logService,
);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.cryptoService,
this.tokenService,
this.policyService,
this.stateService,
this.vaultTimeoutSettingsService,
this.platformUtilsService,
);
this.vaultTimeoutService = new VaultTimeoutService(

View File

@ -7,6 +7,10 @@ import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/a
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FormFieldModule } from "@bitwarden/components";
/**
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead.
* Each client specific component should eventually be converted over to use one of these new components.
*/
@Component({
selector: "app-user-verification",
standalone: true,

View File

@ -1547,6 +1547,10 @@
"message": "Your organization requires you to set a master password.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"verificationRequired" : {
"message": "Verification required",
"description": "Default title for the user verification dialog."
},
"currentMasterPass": {
"message": "Current master password"
},
@ -1872,6 +1876,42 @@
"updateWeakMasterPasswordWarning": {
"message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"tryAgain": {
"message": "Try again"
},
"verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue."
},
"setPin": {
"message": "Set PIN"
},
"verifyWithBiometrics": {
"message": "Verify with biometrics"
},
"awaitingConfirmation": {
"message": "Awaiting confirmation"
},
"couldNotCompleteBiometrics": {
"message": "Could not complete biometrics."
},
"needADifferentMethod": {
"message": "Need a different method?"
},
"useMasterPassword": {
"message": "Use master password"
},
"usePin": {
"message": "Use PIN"
},
"useBiometrics": {
"message": "Use biometrics"
},
"enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email."
},
"resendCode": {
"message": "Resend code"
},
"hours": {
"message": "Hours"
},
@ -2565,6 +2605,15 @@
"incorrectUsernameOrPassword": {
"message": "Incorrect username or password"
},
"incorrectPassword": {
"message": "Incorrect password"
},
"incorrectCode": {
"message": "Incorrect code"
},
"incorrectPin": {
"message": "Incorrect PIN"
},
"multifactorAuthenticationFailed": {
"message": "Multifactor authentication failed"
},

View File

@ -146,26 +146,30 @@ export class NativeMessagingService {
);
}
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
message.userId,
);
const masterKey = await this.cryptoService.getMasterKey(message.userId);
if (userKey != null) {
// we send the master key still for backwards compatibility
// with older browser extensions
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
this.send(
{
command: "biometricUnlock",
response: "unlocked",
keyB64: masterKey?.keyB64,
userKeyB64: userKey.keyB64,
},
appId,
try {
const userKey = await this.cryptoService.getUserKeyFromStorage(
KeySuffixOptions.Biometric,
message.userId,
);
} else {
const masterKey = await this.cryptoService.getMasterKey(message.userId);
if (userKey != null) {
// we send the master key still for backwards compatibility
// with older browser extensions
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
this.send(
{
command: "biometricUnlock",
response: "unlocked",
keyB64: masterKey?.keyB64,
userKeyB64: userKey.keyB64,
},
appId,
);
} else {
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}
} catch (e) {
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
}

View File

@ -11,6 +11,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogService } from "@bitwarden/components";
/**
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead.
*/
@Component({
templateUrl: "user-verification-prompt.component.html",
})

View File

@ -4,6 +4,10 @@ import { NG_VALUE_ACCESSOR } from "@angular/forms";
import { UserVerificationComponent as BaseComponent } from "@bitwarden/angular/auth/components/user-verification.component";
/**
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead.
* Each client specific component should eventually be converted over to use one of these new components.
*/
@Component({
selector: "app-user-verification",
templateUrl: "user-verification.component.html",

View File

@ -2816,6 +2816,9 @@
"incorrectCode": {
"message": "Incorrect code"
},
"incorrectPin": {
"message": "Incorrect PIN"
},
"exportedVault": {
"message": "Vault exported"
},
@ -6604,6 +6607,39 @@
}
}
},
"verificationRequiredForActionSetPinToContinue": {
"message": "Verification required for this action. Set a PIN to continue."
},
"setPin": {
"message": "Set PIN"
},
"verifyWithBiometrics": {
"message": "Verify with biometrics"
},
"awaitingConfirmation": {
"message": "Awaiting confirmation"
},
"couldNotCompleteBiometrics": {
"message": "Could not complete biometrics."
},
"needADifferentMethod": {
"message": "Need a different method?"
},
"useMasterPassword": {
"message": "Use master password"
},
"usePin": {
"message": "Use PIN"
},
"useBiometrics": {
"message": "Use biometrics"
},
"enterVerificationCodeSentToEmail": {
"message": "Enter the verification code that was sent to your email."
},
"resendCode": {
"message": "Resend code"
},
"membersColumnHeader": {
"message": "Member/Group"
},
@ -7086,6 +7122,10 @@
}
}
},
"verificationRequired" : {
"message": "Verification required",
"description": "Default title for the user verification dialog."
},
"recoverAccount": {
"message": "Recover account"
},

View File

@ -16,6 +16,7 @@ export interface UserVerificationPromptParams {
/**
* Used to verify the user's identity (using their master password or email-based OTP for Key Connector users). You can customize all of the text in the modal.
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent instead.
*/
@Directive()
export class UserVerificationPromptComponent {

View File

@ -14,6 +14,9 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
* 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.
*
* @deprecated Jan 24, 2024: Use new libs/auth UserVerificationDialogComponent or UserVerificationFormInputComponent instead.
* Each client specific component should eventually be converted over to use one of these new components.
*/
@Directive({
selector: "app-user-verification",

View File

@ -633,6 +633,8 @@ import { ModalService } from "./modal.service";
UserVerificationApiServiceAbstraction,
PinCryptoServiceAbstraction,
LogService,
VaultTimeoutSettingsServiceAbstraction,
PlatformUtilsServiceAbstraction,
],
},
{

View File

@ -0,0 +1 @@
export * from "./user-verification-biometrics-fingerprint.icon";

View File

@ -0,0 +1,12 @@
import { svgIcon } from "@bitwarden/components";
export const UserVerificationBiometricsIcon = svgIcon`
<svg xmlns="http://www.w3.org/2000/svg" width="63" height="65" fill="none">
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M6.539 13.582C12.113 5.786 21.228.7 31.529.7c15.02 0 27.512 10.808 30.203 25.086a2 2 0 1 1-3.93.74C55.457 14.093 44.578 4.7 31.528 4.7c-8.952 0-16.879 4.416-21.736 11.21a2 2 0 0 1-3.254-2.327Zm-.955 5.384A2 2 0 0 1 6.7 21.565a26.876 26.876 0 0 0-1.91 9.988v8.833a2 2 0 1 1-4 0v-8.833c0-4.05.778-7.923 2.195-11.472a2 2 0 0 1 2.599-1.115Zm54.685 10.587a2 2 0 0 1 2 2v8.244a2 2 0 0 1-4 0v-8.244a2 2 0 0 1 2-2Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M8.476 21.293c3.898-8.848 12.751-15.032 23.053-15.032a25.08 25.08 0 0 1 14.296 4.448A2 2 0 1 1 43.552 14a21.08 21.08 0 0 0-12.023-3.739c-8.66 0-16.11 5.196-19.392 12.645a2 2 0 1 1-3.661-1.613Zm39.328-6.481a2 2 0 0 1 2.82.211 25.062 25.062 0 0 1 6.082 16.4v19.629a2 2 0 1 1-4 0V31.423c0-5.27-1.925-10.085-5.114-13.79a2 2 0 0 1 .212-2.821ZM8.728 26.786A2 2 0 0 1 10.49 29c-.09.794-.137 1.603-.137 2.423v19.629a2 2 0 1 1-4 0V31.423c0-.972.055-1.931.163-2.876a2 2 0 0 1 2.213-1.76Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M12.223 31.097c0-10.648 8.647-19.273 19.306-19.273s19.306 8.625 19.306 19.273v25.321a2 2 0 1 1-4 0v-25.32c0-8.433-6.85-15.274-15.306-15.274s-15.305 6.841-15.305 15.273v9.913a2 2 0 1 1-4 0v-9.913Zm2 13.409a2 2 0 0 1 2 2v9.912a2 2 0 1 1-4 0v-9.912a2 2 0 0 1 2-2Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M24.698 19.044a13.418 13.418 0 0 1 6.831-1.857c7.411 0 13.434 5.984 13.434 13.385v7.851a2 2 0 1 1-4 0v-7.851c0-5.175-4.216-9.385-9.434-9.385a9.419 9.419 0 0 0-4.8 1.304 2 2 0 0 1-2.031-3.447Zm-1.76 3.755a2 2 0 0 1 .613 2.762 9.296 9.296 0 0 0-1.456 5.01v29.64a2 2 0 0 1-4 0v-29.64c0-2.63.763-5.087 2.081-7.158a2 2 0 0 1 2.761-.614Zm20.025 20.298a2 2 0 0 1 2 2v15.114a2 2 0 1 1-4 0V45.097a2 2 0 0 1 2-2Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M23.967 30.18c0-4.163 3.408-7.497 7.562-7.497s7.563 3.334 7.563 7.496v12.563a2 2 0 0 1-4 0V30.179c0-1.908-1.573-3.496-3.563-3.496-1.99 0-3.562 1.588-3.562 3.496v31.603a2 2 0 0 1-4 0V30.179ZM37.092 46.04a2 2 0 0 1 2 2v13.74a2 2 0 0 1-4 0v-13.74a2 2 0 0 1 2-2Z" clip-rule="evenodd"/>
<path class="tw-fill-secondary-500" fill="#89929F" fill-rule="evenodd" d="M31.546 28.375a2 2 0 0 1 2 2v4.908a2 2 0 1 1-4 0v-4.908a2 2 0 0 1 2-2Zm-.018 10.334a2 2 0 0 1 2.001 1.999l.017 22.25a2 2 0 1 1-4 .003l-.017-22.25a2 2 0 0 1 1.999-2.002Z" clip-rule="evenodd"/>
</svg>
`;

View File

@ -1,5 +1,14 @@
/**
* This barrel file should only contain Angular exports
*/
// icons
export * from "./icons";
export * from "./fingerprint-dialog/fingerprint-dialog.component";
export * from "./password-callout/password-callout.component";
// user verification
export * from "./user-verification/user-verification-dialog.component";
export * from "./user-verification/user-verification-dialog.types";
export * from "./user-verification/user-verification-form-input.component";

View File

@ -0,0 +1,6 @@
export enum ActiveClientVerificationOption {
MasterPassword = "masterPassword",
Pin = "pin",
Biometrics = "biometrics",
None = "none",
}

View File

@ -0,0 +1,103 @@
<form [formGroup]="verificationForm" [bitSubmit]="submit">
<bit-dialog>
<span bitDialogTitle>
{{
dialogOptions.title ? (dialogOptions.title | i18n) : ("verificationRequired" | i18n)
}}</span
>
<ng-container bitDialogContent>
<!-- Show optional content when verification is server side or client side and verification methods were found. -->
<ng-container
*ngIf="
!dialogOptions.clientSideOnlyVerification ||
(dialogOptions.clientSideOnlyVerification &&
activeClientVerificationOption !== ActiveClientVerificationOption.None)
"
>
<p bitTypography="body1" *ngIf="dialogOptions.bodyText">
{{ dialogOptions.bodyText | i18n }}
</p>
<app-callout
*ngIf="dialogOptions.calloutOptions"
[type]="dialogOptions.calloutOptions.type"
>
{{ dialogOptions.calloutOptions.text | i18n }}
</app-callout>
</ng-container>
<!-- Shown when client side verification methods picked and no verification methods found -->
<ng-container
*ngIf="
dialogOptions.clientSideOnlyVerification &&
activeClientVerificationOption === ActiveClientVerificationOption.None
"
>
<p bitTypography="body1">
{{ "verificationRequiredForActionSetPinToContinue" | i18n }}
</p>
</ng-container>
<app-user-verification-form-input
[(invalidSecret)]="invalidSecret"
formControlName="secret"
[verificationType]="dialogOptions.clientSideOnlyVerification ? 'client' : 'server'"
(activeClientVerificationOptionChange)="handleActiveClientVerificationOptionChange($event)"
(biometricsVerificationResultChange)="handleBiometricsVerificationResultChange($event)"
></app-user-verification-form-input>
</ng-container>
<ng-container bitDialogFooter>
<!-- Confirm button container - shown for server side validation but hidden if client side validation + biometrics -->
<ng-container
*ngIf="
!dialogOptions.clientSideOnlyVerification ||
(dialogOptions.clientSideOnlyVerification &&
activeClientVerificationOption !== ActiveClientVerificationOption.Biometrics)
"
>
<!-- Default / custom buttons shown for server verifications or any valid, non biometric client verifications (MP or PIN) -->
<ng-container
*ngIf="activeClientVerificationOption !== ActiveClientVerificationOption.None"
>
<!-- Default confirm button -->
<button
*ngIf="!dialogOptions.confirmButtonOptions"
type="submit"
bitButton
bitFormButton
buttonType="primary"
>
{{ "submit" | i18n }}
</button>
<!-- Custom confirm button -->
<button
*ngIf="dialogOptions.confirmButtonOptions"
type="submit"
bitButton
bitFormButton
[buttonType]="dialogOptions.confirmButtonOptions.type"
>
{{ dialogOptions.confirmButtonOptions.text | i18n }}
</button>
</ng-container>
<ng-container
*ngIf="activeClientVerificationOption === ActiveClientVerificationOption.None"
>
<!-- For no client verifications found, show set a pin confirm button.
Note: this doesn't make sense for web as web doesn't support PINs, but this is how we are handling it for now
as the expectation is that only browser and desktop will use the new clientSideOnlyVerification flow.
We might genericize this in the future to just tell the user they need to configure a valid user verification option like PIN or Biometrics. -->
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "setPin" | i18n }}
</button>
</ng-container>
</ng-container>
<button type="button" bitButton bitFormButton buttonType="secondary" (click)="cancel()">
{{ "cancel" | i18n }}
</button>
</ng-container>
</bit-dialog>
</form>

View File

@ -0,0 +1,247 @@
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,
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,
],
})
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;
* }
*
*/
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 {
// 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);
}
}

View File

@ -0,0 +1,90 @@
import { ButtonType } from "@bitwarden/components";
/**
* @typedef {Object} UserVerificationCalloutOptions - Configuration options for the callout displayed in the dialog body.
*/
export type UserVerificationCalloutOptions = {
/**
* The translation key for the text of the callout.
*/
text: string;
/**
* The type of the callout.
* Can be "warning", "danger", "error", or "tip".
*/
type: "warning" | "danger" | "error" | "tip";
};
/**
* @typedef {Object} UserVerificationConfirmButtonOptions - Configuration options for the confirm button in the User Verification Dialog.
*/
export type UserVerificationConfirmButtonOptions = {
/**
* The translation key for the text of the confirm button.
*/
text: string;
/**
* The type of the confirm button.
* It should be a valid ButtonType.
*/
type: ButtonType;
};
/**
* @typedef {Object} UserVerificationDialogOptions - Configuration parameters for the user verification dialog.
*/
export type UserVerificationDialogOptions = {
/**
* The translation key for the title of the dialog.
* This is optional and defaults to "Verification required" if not provided.
*/
title?: string;
/**
* The translation key for the body text of the dialog.
* Optional.
*/
bodyText?: string;
/**
* Options for a callout to be displayed in the dialog body below the body text.
* Optional.
*/
calloutOptions?: UserVerificationCalloutOptions;
/**
* Options for the confirm button.
* Optional. The default text is "Submit" and the default type is "primary".
*/
confirmButtonOptions?: UserVerificationConfirmButtonOptions;
/**
* Indicates whether the verification is only performed client-side. Includes local MP verification, PIN, and Biometrics.
* Optional.
* **Important:** Only for use on desktop and browser platforms as when there are no client verification methods, the user is instructed to set a pin (which is not supported on web)
*/
clientSideOnlyVerification?: boolean;
};
/**
* @typedef {Object} UserVerificationDialogResult - The result of the user verification dialog.
*/
export type UserVerificationDialogResult = {
/**
* The user's action.
*/
userAction: "confirm" | "cancel";
/**
* Indicates whether the verification was successful.
*/
verificationSuccess: boolean;
/**
* Indicates whether there are no available client verification methods.
* Optional and only relevant when the dialog is configured to only perform client-side verification.
*/
noAvailableClientVerificationMethods?: boolean;
};

View File

@ -0,0 +1,165 @@
<ng-container *ngIf="verificationType === 'client'">
<div class="tw-flex tw-flex-col">
<!-- Master password -->
<ng-container
*ngIf="
userVerificationOptions.client.masterPassword &&
activeClientVerificationOption == ActiveClientVerificationOption.MasterPassword
"
>
<ng-container *ngTemplateOutlet="masterPasswordFormField"></ng-container>
</ng-container>
<!-- PIN -->
<ng-container
*ngIf="
userVerificationOptions.client.pin &&
activeClientVerificationOption == ActiveClientVerificationOption.Pin
"
>
<bit-form-field disableMargin>
<bit-label>{{ "pin" | i18n }}</bit-label>
<input
bitInput
id="pin"
type="password"
name="pin"
[formControl]="secret"
appAutofocus
appInputVerbatim
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>
<!-- Biometrics -->
<ng-container
*ngIf="
userVerificationOptions.client.biometrics &&
activeClientVerificationOption == ActiveClientVerificationOption.Biometrics
"
>
<div class="tw-flex tw-flex-col tw-items-center">
<bit-icon [icon]="Icons.UserVerificationBiometricsIcon" class="tw-mb-4"></bit-icon>
<p class="tw-font-bold tw-mb-1">{{ "verifyWithBiometrics" | i18n }}</p>
<div *ngIf="!biometricsVerificationFailed">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "awaitingConfirmation" | i18n }}
</div>
</div>
<app-callout type="error" *ngIf="biometricsVerificationFailed">
{{ "couldNotCompleteBiometrics" | i18n }}
<button bitLink type="button" linkType="primary" (click)="verifyUserViaBiometrics()">
{{ "tryAgain" | i18n }}
</button>
</app-callout>
</ng-container>
<!-- Alternate verification options if user has more than 1 -->
<div
class="tw-flex tw-flex-col tw-items-center tw-justify-center tw-mt-2"
*ngIf="hasMultipleClientVerificationOptions"
>
<p class="tw-mb-1">{{ "needADifferentMethod" | i18n }}</p>
<button
*ngIf="
userVerificationOptions.client.biometrics &&
activeClientVerificationOption !== ActiveClientVerificationOption.Biometrics
"
type="button"
class="tw-mb-1"
bitLink
linkType="primary"
(click)="activeClientVerificationOption = ActiveClientVerificationOption.Biometrics"
>
{{ "useBiometrics" | i18n }}
</button>
<button
*ngIf="
userVerificationOptions.client.pin &&
activeClientVerificationOption !== ActiveClientVerificationOption.Pin
"
type="button"
class="tw-mb-1"
bitLink
linkType="primary"
(click)="activeClientVerificationOption = ActiveClientVerificationOption.Pin"
>
{{ "usePin" | i18n }}
</button>
<button
*ngIf="
userVerificationOptions.client.masterPassword &&
activeClientVerificationOption !== ActiveClientVerificationOption.MasterPassword
"
type="button"
bitLink
linkType="primary"
(click)="activeClientVerificationOption = ActiveClientVerificationOption.MasterPassword"
>
{{ "useMasterPassword" | i18n }}
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="verificationType === 'server'">
<ng-container *ngIf="userVerificationOptions.server.masterPassword">
<ng-container *ngTemplateOutlet="masterPasswordFormField"></ng-container>
</ng-container>
<ng-container *ngIf="userVerificationOptions.server.otp">
<div class="tw-mb-6" *ngIf="!sentInitialCode">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
</div>
<div class="tw-mb-6" *ngIf="sentInitialCode">
{{ "enterVerificationCodeSentToEmail" | i18n }}
<p class="mb-0">
<button bitLink type="button" linkType="primary" (click)="requestOTP()">
{{ "resendCode" | i18n }}
</button>
<span class="tw-ml-2 tw-text-success" role="alert" @sent *ngIf="sentCode">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "codeSent" | i18n }}
</span>
</p>
</div>
<bit-form-field disableMargin>
<bit-label>{{ "verificationCode" | i18n }}</bit-label>
<input
bitInput
type="text"
id="verificationCode"
name="verificationCode"
[formControl]="secret"
appInputVerbatim
/>
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-container>
</ng-container>
<ng-template #masterPasswordFormField>
<bit-form-field disableMargin>
<bit-label>{{ "masterPass" | i18n }}</bit-label>
<input
bitInput
id="masterPassword"
type="password"
name="MasterPasswordHash"
[formControl]="secret"
appAutofocus
appInputVerbatim
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
<bit-hint>{{ "confirmIdentity" | i18n }}</bit-hint>
</bit-form-field>
</ng-template>

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();
}
}

View File

@ -48,12 +48,12 @@ export class PinCryptoService implements PinCryptoServiceAbstraction {
}
if (!userKey) {
this.logService.error(`User key null after pin key decryption.`);
this.logService.warning(`User key null after pin key decryption.`);
return null;
}
if (!(await this.validatePin(userKey, pin))) {
this.logService.error(`Pin key decryption successful but pin validation failed.`);
this.logService.warning(`Pin key decryption successful but pin validation failed.`);
return null;
}

View File

@ -1,4 +1,5 @@
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { UserVerificationOptions } from "../../types/user-verification-options";
import { Verification } from "../../types/verification";
export abstract class UserVerificationService {
@ -21,4 +22,8 @@ export abstract class UserVerificationService {
* @returns True if the user has a master password and has used it in the current session
*/
hasMasterPasswordAndMasterKeyHash: (userId?: string) => Promise<boolean>;
getAvailableVerificationOptions: (
verificationType: keyof UserVerificationOptions,
) => Promise<UserVerificationOptions>;
}

View File

@ -1,7 +1,9 @@
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum";
import { UserKey } from "../../../types/key";
@ -10,6 +12,7 @@ import { UserVerificationService as UserVerificationServiceAbstraction } from ".
import { VerificationType } from "../../enums/verification-type";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
import { UserVerificationOptions } from "../../types/user-verification-options";
import {
MasterPasswordVerification,
OtpVerification,
@ -32,8 +35,54 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private userVerificationApiService: UserVerificationApiServiceAbstraction,
private pinCryptoService: PinCryptoServiceAbstraction,
private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService,
) {}
async getAvailableVerificationOptions(
verificationType: keyof UserVerificationOptions,
): Promise<UserVerificationOptions> {
if (verificationType === "client") {
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(),
this.vaultTimeoutSettingsService.isPinLockSet(),
this.vaultTimeoutSettingsService.isBiometricLockSet(),
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric),
]);
// note: we do not need to check this.platformUtilsService.supportsBiometric() because
// we can just use the logic below which works for both desktop & the browser extension.
return {
client: {
masterPassword: userHasMasterPassword,
pin: pinLockType !== "DISABLED",
biometrics:
biometricsLockSet &&
(biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()),
},
server: {
masterPassword: false,
otp: false,
},
};
} else {
// server
// Don't check if have MP hash locally, because we are going to send the secret to the server to be verified.
const userHasMasterPassword = await this.hasMasterPassword();
return {
client: {
masterPassword: false,
pin: false,
biometrics: false,
},
server: { masterPassword: userHasMasterPassword, otp: !userHasMasterPassword },
};
}
}
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)

View File

@ -0,0 +1,14 @@
/**
* @typedef {Object} UserVerificationOptions - The available verification options for a user.
*/
export type UserVerificationOptions = {
server: {
otp: boolean;
masterPassword: boolean;
};
client: {
masterPassword: boolean;
pin: boolean;
biometrics: boolean;
};
};

View File

@ -5,6 +5,7 @@ export * from "./badge";
export * from "./banner";
export * from "./breadcrumbs";
export * from "./button";
export { ButtonType } from "./shared/button-like.abstraction";
export * from "./callout";
export * from "./checkbox";
export * from "./color-password";