1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 05:00:10 +00:00

add a flow to InputPasswordFlow enum, and a method to verify the flow and presence of a userId

This commit is contained in:
rr-bw
2025-04-11 15:51:59 -07:00
parent a1d53e9840
commit 2bf88c5008
14 changed files with 140 additions and 85 deletions

View File

@@ -1,6 +1,6 @@
<div *ngIf="!useTrialStepper">
<auth-input-password
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
[flow]="inputPasswordFlow"
[email]="email"
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
@@ -11,7 +11,7 @@
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
<auth-input-password
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
[flow]="inputPasswordFlow"
[email]="email"
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
(onPasswordFormSubmit)="handlePasswordSubmit($event)"

View File

@@ -51,7 +51,7 @@ export type InitiationPath =
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
InputPasswordFlow = InputPasswordFlow;
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
/** Password Manager or Secrets Manager */
product: ProductType;

View File

@@ -389,7 +389,6 @@ const safeProviders: SafeProvider[] = [
provide: ChangePasswordService,
useClass: WebChangePasswordService,
deps: [
AccountService,
KeyServiceAbstraction,
MasterPasswordApiServiceAbstraction,
InternalMasterPasswordServiceAbstraction,

View File

@@ -1511,7 +1511,6 @@ const safeProviders: SafeProvider[] = [
provide: ChangePasswordService,
useClass: DefaultChangePasswordService,
deps: [
AccountServiceAbstraction,
KeyService,
MasterPasswordApiServiceAbstraction,
InternalMasterPasswordServiceAbstraction,

View File

@@ -1,5 +1,5 @@
<auth-input-password
[inputPasswordFlow]="inputPasswordFlow"
[flow]="inputPasswordFlow"
[email]="email"
[userId]="userId"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"

View File

@@ -93,7 +93,7 @@ export class ChangePasswordComponent implements OnInit {
await this.syncService.fullSync(true);
if (this.activeAccount == null) {
throw new Error("User not found");
throw new Error("User or userId not found");
}
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
@@ -109,6 +109,7 @@ export class ChangePasswordComponent implements OnInit {
passwordInputResult.newPasswordHint,
passwordInputResult.newMasterKey,
passwordInputResult.newServerMasterKeyHash,
this.userId,
);
this.toastService.showToast({
@@ -181,6 +182,7 @@ export class ChangePasswordComponent implements OnInit {
title: this.i18nService.t("masterPasswordChanged"),
message: this.i18nService.t("logBackIn"),
});
this.messagingService.send("logout");
} catch {
this.toastService.showToast({

View File

@@ -6,8 +6,8 @@
<bit-form-field
*ngIf="
inputPasswordFlow === InputPasswordFlow.ChangePassword ||
inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
flow === InputPasswordFlow.ChangePassword ||
flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
"
>
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
@@ -90,9 +90,7 @@
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
>
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
<input
type="checkbox"
bitCheckbox

View File

@@ -34,7 +34,12 @@ import {
ToastService,
Translation,
} from "@bitwarden/components";
import { KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
import {
DEFAULT_KDF_CONFIG,
KdfConfig,
KdfConfigService,
KeyService,
} from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
@@ -45,24 +50,25 @@ import { compareInputs, ValidationGoal } from "../validators/compare-inputs.vali
import { PasswordInputResult } from "./password-input-result";
/**
* Determines which form input elements will be displayed in the UI.
* Determines which form elements will be displayed in the UI
* and which cryptographic keys will be created and emitted.
*/
export enum InputPasswordFlow {
/**
* - Input: New password
* - Input: New password confirm
* - Input: New password hint
* - Checkbox: Check for breaches
* Form elements displayed:
* - [Input] New password
* - [Input] New password confirm
* - [Input] New password hint
* - [Checkbox] Check for breaches
*/
SetInitialPassword,
/**
* Everything above, plus:
* - Input: Current password (as the first element in the UI)
AccountRegistration, // important: this flow does not involve an activeAccount/userId
SetInitialPasswordAuthedUser,
/*
* All form elements above, plus: [Input] Current password (as the first element in the UI)
*/
ChangePassword,
/**
* Everything above, plus:
* - Checkbox: Rotate account encryption key (as the last element in the UI)
* All form elements above, plus: [Checkbox] Rotate account encryption key (as the last element in the UI)
*/
ChangePasswordWithOptionalUserKeyRotation,
}
@@ -89,10 +95,10 @@ export class InputPasswordComponent implements OnInit {
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
@Output() onSecondaryButtonClick = new EventEmitter<void>();
@Input({ required: true }) inputPasswordFlow!: InputPasswordFlow;
@Input({ required: true }) email!: string;
@Input({ required: true }) userId!: UserId;
@Input({ required: true }) flow!: InputPasswordFlow;
@Input({ required: true, transform: (val: string) => val.trim().toLowerCase() }) email!: string;
@Input() userId?: UserId;
@Input() loading = false;
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
@@ -133,13 +139,24 @@ export class InputPasswordComponent implements OnInit {
compareInputs(
ValidationGoal.InputsShouldNotMatch,
"newPassword",
"hint",
"newPasswordHint",
this.i18nService.t("hintEqualsPassword"),
),
],
},
);
protected get minPasswordLengthMsg() {
if (
this.masterPasswordPolicyOptions != null &&
this.masterPasswordPolicyOptions.minLength > 0
) {
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
} else {
return this.i18nService.t("characterMinimum", this.minPasswordLength);
}
}
constructor(
private auditService: AuditService,
private cipherService: CipherService,
@@ -155,9 +172,14 @@ export class InputPasswordComponent implements OnInit {
) {}
ngOnInit(): void {
this.addFormFieldsIfNecessary();
this.setButtonText();
}
private addFormFieldsIfNecessary() {
if (
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
this.flow === InputPasswordFlow.ChangePassword ||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
) {
// https://github.com/angular/angular/issues/48794
(this.formGroup as FormGroup<any>).addControl(
@@ -166,14 +188,16 @@ export class InputPasswordComponent implements OnInit {
);
}
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
// https://github.com/angular/angular/issues/48794
(this.formGroup as FormGroup<any>).addControl(
"rotateUserKey",
this.formBuilder.control<boolean>(false),
);
}
}
private setButtonText() {
if (this.primaryButtonText) {
this.primaryButtonTextStr = this.i18nService.t(
this.primaryButtonText.key,
@@ -189,22 +213,9 @@ export class InputPasswordComponent implements OnInit {
}
}
get minPasswordLengthMsg() {
if (
this.masterPasswordPolicyOptions != null &&
this.masterPasswordPolicyOptions.minLength > 0
) {
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
} else {
return this.i18nService.t("characterMinimum", this.minPasswordLength);
}
}
getPasswordStrengthScore(score: PasswordStrengthScore) {
this.passwordStrengthScore = score;
}
protected submit = async () => {
this.verifyFlowAndUserId();
this.formGroup.markAllAsTouched();
if (this.formGroup.invalid) {
@@ -216,45 +227,50 @@ export class InputPasswordComponent implements OnInit {
throw new Error("Email is required to create master key.");
}
this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
if (this.kdfConfig == null) {
throw new Error("KdfConfig is required to create master key.");
}
const currentPassword = this.formGroup.get("currentPassword")?.value || "";
const newPassword = this.formGroup.controls.newPassword.value;
const newPasswordHint = this.formGroup.controls.newPasswordHint.value;
const checkForBreaches = this.formGroup.controls.checkForBreaches.value;
// 1. Verify current password is correct (if necessary)
// 1. Determine kdfConfig
if (this.flow === InputPasswordFlow.AccountRegistration) {
this.kdfConfig = DEFAULT_KDF_CONFIG;
} else {
this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
}
if (this.kdfConfig == null) {
throw new Error("KdfConfig is required to create master key.");
}
// 2. Verify current password is correct (if necessary)
if (
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
this.flow === InputPasswordFlow.ChangePassword ||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
) {
const currentPasswordIsCorrect = await this.verifyCurrentPassword(
const currentPasswordVerified = await this.verifyCurrentPassword(
currentPassword,
this.userId,
this.kdfConfig,
);
if (!currentPasswordIsCorrect) {
if (!currentPasswordVerified) {
return;
}
}
// 2. Evaluate new password
const newPasswordEvaluatedSuccessfully = await this.evaluateNewPassword(
// 3. Verify new password
const newPasswordVerified = await this.verifyNewPassword(
newPassword,
this.passwordStrengthScore,
checkForBreaches,
);
if (!newPasswordEvaluatedSuccessfully) {
if (!newPasswordVerified) {
return;
}
// 3. Create cryptographic keys
// 4. Create cryptographic keys and build a PasswordInputResult object
const newMasterKey = await this.keyService.makeMasterKey(
newPassword,
this.email.trim().toLowerCase(),
this.email,
this.kdfConfig,
);
@@ -280,12 +296,12 @@ export class InputPasswordComponent implements OnInit {
};
if (
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
this.flow === InputPasswordFlow.ChangePassword ||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
) {
const currentMasterKey = await this.keyService.makeMasterKey(
currentPassword,
this.email.trim().toLowerCase(),
this.email,
this.kdfConfig,
);
@@ -307,31 +323,66 @@ export class InputPasswordComponent implements OnInit {
passwordInputResult.currentLocalMasterKeyHash = currentLocalMasterKeyHash;
}
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
}
// 4. Emit cryptographic keys and other password related properties
// 5. Emit cryptographic keys and other password related properties
this.onPasswordFormSubmit.emit(passwordInputResult);
};
/**
* Returns true if the current password is correct, false otherwise
* This method prevents a dev from passing down the wrong `InputPasswordFlow`
* from the parent component or from failing to pass down a `userId` for flows
* that require it.
*
* We cannot mark the `userId` `@Input` as required because in an account registration
* flow we will not have an active account `userId` to pass down.
*/
private verifyFlowAndUserId() {
/**
* There can be no active account (and thus no userId) in an account registration
* flow. If there is a userId, it means the dev passed down the wrong InputPasswordFlow
* from the parent component.
*/
if (this.flow === InputPasswordFlow.AccountRegistration) {
if (this.userId) {
throw new Error(
"There can be no userId in an account registration flow. Please pass down the appropriate InputPasswordFlow from the parent component.",
);
}
}
/**
* There MUST be an active account (and thus a userId) in all other flows.
* If no userId is passed down, it means the dev either:
* (a) passed down the wrong InputPasswordFlow, or
* (b) passed down the correct InputPasswordFlow but failed to pass down a userId
*/
if (this.flow !== InputPasswordFlow.AccountRegistration) {
if (!this.userId) {
throw new Error("The selected InputPasswordFlow requires that a userId be passed down");
}
}
}
/**
* Returns `true` if the current password is correct (it can be used to successfully decrypt
* the masterKeyEncrypedUserKey), `false` otherwise
*/
private async verifyCurrentPassword(
currentPassword: string,
userId: UserId,
kdfConfig: KdfConfig,
): Promise<boolean> {
const currentMasterKey = await this.keyService.makeMasterKey(
currentPassword,
this.email.trim().toLowerCase(),
this.email,
kdfConfig,
);
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
currentMasterKey,
userId,
this.userId,
);
if (decryptedUserKey == null) {
@@ -348,9 +399,10 @@ export class InputPasswordComponent implements OnInit {
}
/**
* Returns true if the new password passes all checks, false otherwise
* Returns `true` if the new password is not weak or breached and it passes
* any enforced org policy options, `false` otherwise
*/
private async evaluateNewPassword(
private async verifyNewPassword(
newPassword: string,
passwordStrengthScore: PasswordStrengthScore,
checkForBreaches: boolean,
@@ -414,7 +466,7 @@ export class InputPasswordComponent implements OnInit {
return true;
}
async rotateUserKeyClicked() {
protected async rotateUserKeyClicked() {
const rotateUserKeyCtrl = this.formGroup.get(
"rotateUserKey",
) as unknown as FormControl<boolean>;
@@ -468,4 +520,8 @@ export class InputPasswordComponent implements OnInit {
}
}
}
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
this.passwordStrengthScore = score;
}
}

View File

@@ -5,7 +5,7 @@
<auth-input-password
*ngIf="!loading"
[email]="email"
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
[flow]="inputPasswordFlow"
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
[loading]="submitting"
[primaryButtonText]="{ key: 'createAccount' }"

View File

@@ -39,8 +39,7 @@ import { RegistrationFinishService } from "./registration-finish.service";
export class RegistrationFinishComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
InputPasswordFlow = InputPasswordFlow;
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
loading = true;
submitting = false;
email: string;

View File

@@ -13,7 +13,7 @@
</app-callout>
<auth-input-password
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
[flow]="inputPasswordFlow"
[primaryButtonText]="{ key: 'createAccount' }"
[email]="email"
[loading]="submitting"

View File

@@ -36,7 +36,7 @@ import {
imports: [CommonModule, InputPasswordComponent, JslibModule],
})
export class SetPasswordJitComponent implements OnInit {
protected InputPasswordFlow = InputPasswordFlow;
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
protected email: string;
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
protected orgId: string;

View File

@@ -1,4 +1,5 @@
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
export abstract class ChangePasswordService {
@@ -20,5 +21,6 @@ export abstract class ChangePasswordService {
newPasswordHint: string,
newMasterKey: MasterKey,
newServerMasterKeyHash: string,
userId: UserId,
): Promise<void>;
}

View File

@@ -1,10 +1,8 @@
import { firstValueFrom } from "rxjs";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
import { UserId } from "@bitwarden/common/types/guid";
import { MasterKey } from "@bitwarden/common/types/key";
import { KeyService } from "@bitwarden/key-management";
@@ -12,7 +10,6 @@ import { ChangePasswordService } from "../../abstractions";
export class DefaultChangePasswordService implements ChangePasswordService {
constructor(
private accountService: AccountService,
private keyService: KeyService,
private masterPasswordApiService: MasterPasswordApiService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
@@ -40,8 +37,11 @@ export class DefaultChangePasswordService implements ChangePasswordService {
newPasswordHint: string,
newMasterKey: MasterKey,
newServerMasterKeyHash: string,
userId: UserId,
) {
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
if (!userId) {
throw new Error("The change password process requires a userId");
}
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
currentMasterKey,