mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
refactor(set-change-password): [Auth/PM-18458] Create new ChangePasswordComponent (#14226)
This PR creates a new ChangePasswordComponent. The first use-case of the ChangePasswordComponent is to place it inside a new PasswordSettingsComponent, which is accessed by going to Account Settings > Security. The ChangePasswordComponent will be updated in future PRs to handle more change password scenarios. Feature Flags: PM16117_ChangeExistingPasswordRefactor
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
@if (initializing) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
} @else {
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[inlineButtons]="true"
|
||||
[primaryButtonText]="{ key: 'changeMasterPassword' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
>
|
||||
</auth-input-password>
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
imports: [InputPasswordComponent, I18nPipe],
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
@Input() inputPasswordFlow: InputPasswordFlow = InputPasswordFlow.ChangePassword;
|
||||
|
||||
activeAccount: Account | null = null;
|
||||
email?: string;
|
||||
userId?: UserId;
|
||||
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
initializing = true;
|
||||
submitting = false;
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private changePasswordService: ChangePasswordService,
|
||||
private i18nService: I18nService,
|
||||
private messagingService: MessagingService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
private syncService: SyncService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = this.activeAccount?.id;
|
||||
this.email = this.activeAccount?.email;
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
this.masterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(this.userId),
|
||||
);
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
try {
|
||||
if (passwordInputResult.rotateUserKey) {
|
||||
if (this.activeAccount == null) {
|
||||
throw new Error("activeAccount not found");
|
||||
}
|
||||
|
||||
if (passwordInputResult.currentPassword == null) {
|
||||
throw new Error("currentPassword not found");
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
await this.changePasswordService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
passwordInputResult.currentPassword,
|
||||
passwordInputResult.newPassword,
|
||||
this.activeAccount,
|
||||
passwordInputResult.newPasswordHint,
|
||||
);
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
|
||||
await this.changePasswordService.changePassword(passwordInputResult, this.userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: this.i18nService.t("masterPasswordChanged"),
|
||||
message: this.i18nService.t("masterPasswordChangedDesc"),
|
||||
});
|
||||
|
||||
this.messagingService.send("logout");
|
||||
}
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("errorOccurred"),
|
||||
});
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PasswordInputResult } from "@bitwarden/auth/angular";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export abstract class ChangePasswordService {
|
||||
/**
|
||||
* Creates a new user key and re-encrypts all required data with it.
|
||||
* - does so by calling the underlying method on the `UserKeyRotationService`
|
||||
* - implemented in Web only
|
||||
*
|
||||
* @param currentPassword the current password
|
||||
* @param newPassword the new password
|
||||
* @param user the user account
|
||||
* @param newPasswordHint the new password hint
|
||||
* @throws if called from a non-Web client
|
||||
*/
|
||||
abstract rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
newPasswordHint: string,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Changes the user's password and re-encrypts the user key with the `newMasterKey`.
|
||||
* - Specifically, this method uses credentials from the `passwordInputResult` to:
|
||||
* 1. Decrypt the user key with the `currentMasterKey`
|
||||
* 2. Re-encrypt that user key with the `newMasterKey`, resulting in a `newMasterKeyEncryptedUserKey`
|
||||
* 3. Build a `PasswordRequest` object that gets POSTed to `"/accounts/password"`
|
||||
*
|
||||
* @param passwordInputResult credentials object received from the `InputPasswordComponent`
|
||||
* @param userId the `userId`
|
||||
* @throws if the `userId`, `currentMasterKey`, or `currentServerMasterKeyHash` is not found
|
||||
*/
|
||||
abstract changePassword(passwordInputResult: PasswordInputResult, userId: UserId): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import { ChangePasswordService } from "./change-password.service.abstraction";
|
||||
import { DefaultChangePasswordService } from "./default-change-password.service";
|
||||
|
||||
describe("DefaultChangePasswordService", () => {
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
|
||||
let sut: ChangePasswordService;
|
||||
|
||||
const userId = "userId" as UserId;
|
||||
|
||||
const user: Account = {
|
||||
id: userId,
|
||||
email: "email",
|
||||
emailVerified: false,
|
||||
name: "name",
|
||||
};
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
currentMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
currentServerMasterKeyHash: "currentServerMasterKeyHash",
|
||||
|
||||
newPassword: "newPassword",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
|
||||
kdfConfig: new PBKDF2KdfConfig(),
|
||||
};
|
||||
|
||||
const decryptedUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const newMasterKeyEncryptedUserKey: [UserKey, EncString] = [
|
||||
decryptedUserKey,
|
||||
{ encryptedString: "newMasterKeyEncryptedUserKey" } as EncString,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
|
||||
sut = new DefaultChangePasswordService(
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
);
|
||||
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(decryptedUserKey);
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(newMasterKeyEncryptedUserKey);
|
||||
});
|
||||
|
||||
describe("changePassword()", () => {
|
||||
it("should call the postPassword() API method with a the correct PasswordRequest credentials", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
masterPasswordHash: passwordInputResult.currentServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
newMasterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
key: newMasterKeyEncryptedUserKey[1].encryptedString,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call decryptUserKeyWithMasterKey and encryptUserKeyWithMasterKey", async () => {
|
||||
// Act
|
||||
await sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
expect(keyService.encryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a userId was not found", async () => {
|
||||
// Arrange
|
||||
const userId: null = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("userId not found");
|
||||
});
|
||||
|
||||
it("should throw if a currentMasterKey was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentMasterKey = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if a currentServerMasterKeyHash was not found", async () => {
|
||||
// Arrange
|
||||
const incorrectPasswordInputResult = { ...passwordInputResult };
|
||||
incorrectPasswordInputResult.currentServerMasterKeyHash = null;
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(incorrectPasswordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error if user key decryption fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordService.decryptUserKeyWithMasterKey.mockResolvedValue(null);
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not decrypt user key");
|
||||
});
|
||||
|
||||
it("should throw an error if postPassword() fails", async () => {
|
||||
// Arrange
|
||||
masterPasswordApiService.postPassword.mockRejectedValueOnce(new Error("error"));
|
||||
|
||||
// Act
|
||||
const testFn = sut.changePassword(passwordInputResult, userId);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow("Could not change password");
|
||||
expect(masterPasswordApiService.postPassword).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
|
||||
it("should throw an error (the method is only implemented in Web)", async () => {
|
||||
// Act
|
||||
const testFn = sut.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
user,
|
||||
"newPasswordHint",
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { PasswordInputResult, ChangePasswordService } from "@bitwarden/auth/angular";
|
||||
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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
export class DefaultChangePasswordService implements ChangePasswordService {
|
||||
constructor(
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
user: Account,
|
||||
hint: string,
|
||||
): Promise<void> {
|
||||
throw new Error("rotateUserKeyMasterPasswordAndEncryptedData() is only implemented in Web");
|
||||
}
|
||||
|
||||
async changePassword(passwordInputResult: PasswordInputResult, userId: UserId) {
|
||||
if (!userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
if (!passwordInputResult.currentMasterKey || !passwordInputResult.currentServerMasterKeyHash) {
|
||||
throw new Error("currentMasterKey or currentServerMasterKeyHash not found");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
passwordInputResult.currentMasterKey,
|
||||
userId,
|
||||
);
|
||||
|
||||
if (decryptedUserKey == null) {
|
||||
throw new Error("Could not decrypt user key");
|
||||
}
|
||||
|
||||
const newMasterKeyEncryptedUserKey = await this.keyService.encryptUserKeyWithMasterKey(
|
||||
passwordInputResult.newMasterKey,
|
||||
decryptedUserKey,
|
||||
);
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = passwordInputResult.currentServerMasterKeyHash;
|
||||
request.newMasterPasswordHash = passwordInputResult.newServerMasterKeyHash;
|
||||
request.masterPasswordHint = passwordInputResult.newPasswordHint;
|
||||
request.key = newMasterKeyEncryptedUserKey[1].encryptedString as string;
|
||||
|
||||
try {
|
||||
await this.masterPasswordApiService.postPassword(request);
|
||||
} catch {
|
||||
throw new Error("Could not change password");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,11 @@ export * from "./anon-layout/anon-layout-wrapper.component";
|
||||
export * from "./anon-layout/anon-layout-wrapper-data.service";
|
||||
export * from "./anon-layout/default-anon-layout-wrapper-data.service";
|
||||
|
||||
// change password
|
||||
export * from "./change-password/change-password.component";
|
||||
export * from "./change-password/change-password.service.abstraction";
|
||||
export * from "./change-password/default-change-password.service";
|
||||
|
||||
// fingerprint dialog
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -58,12 +58,12 @@
|
||||
</div>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
||||
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_confirm-new-password"
|
||||
id="input-password-form_new-password-confirm"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmNewPassword"
|
||||
formControlName="newPasswordConfirm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -76,21 +76,33 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="hint" />
|
||||
<input id="input-password-form_new-password-hint" bitInput formControlName="newPasswordHint" />
|
||||
<bit-hint>
|
||||
{{ "masterPassHintText" | i18n: formGroup.value.hint.length : maxHintLength.toString() }}
|
||||
{{
|
||||
"masterPassHintText"
|
||||
| i18n: formGroup.value.newPasswordHint.length : maxHintLength.toString()
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="checkForBreaches" />
|
||||
<input
|
||||
id="input-password-form_check-for-breaches"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-form-control
|
||||
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
>
|
||||
<input type="checkbox" bitCheckbox formControlName="rotateUserKey" />
|
||||
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
||||
<input
|
||||
id="input-password-form_rotate-user-key"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="rotateUserKey"
|
||||
(change)="rotateUserKeyClicked()"
|
||||
/>
|
||||
<bit-label>
|
||||
{{ "rotateAccountEncKey" | i18n }}
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from "@angular/forms";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormControl } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -9,9 +10,13 @@ import {
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -23,7 +28,12 @@ import {
|
||||
ToastService,
|
||||
Translation,
|
||||
} from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, 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
|
||||
@@ -34,30 +44,41 @@ 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.
|
||||
*/
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* - Input: New password
|
||||
* - Input: Confirm new password
|
||||
* - Input: 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,
|
||||
}
|
||||
|
||||
interface InputPasswordForm {
|
||||
newPassword: FormControl<string>;
|
||||
newPasswordConfirm: FormControl<string>;
|
||||
newPasswordHint: FormControl<string>;
|
||||
checkForBreaches: FormControl<boolean>;
|
||||
|
||||
currentPassword?: FormControl<string>;
|
||||
rotateUserKey?: FormControl<boolean>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-input-password",
|
||||
@@ -80,9 +101,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 }) flow!: InputPasswordFlow;
|
||||
@Input({ required: true, transform: (val: string) => val.trim().toLowerCase() }) email!: string;
|
||||
|
||||
@Input() userId?: UserId;
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
|
||||
@@ -93,6 +115,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
protected secondaryButtonTextStr: string = "";
|
||||
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
private kdfConfig: KdfConfig | null = null;
|
||||
private minHintLength = 0;
|
||||
protected maxHintLength = 50;
|
||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||
@@ -101,64 +124,93 @@ export class InputPasswordComponent implements OnInit {
|
||||
protected showErrorSummary = false;
|
||||
protected showPassword = false;
|
||||
|
||||
protected formGroup = this.formBuilder.nonNullable.group(
|
||||
protected formGroup = this.formBuilder.nonNullable.group<InputPasswordForm>(
|
||||
{
|
||||
newPassword: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmNewPassword: ["", Validators.required],
|
||||
hint: [
|
||||
"", // must be string (not null) because we check length in validation
|
||||
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
||||
],
|
||||
checkForBreaches: [true],
|
||||
newPassword: this.formBuilder.nonNullable.control("", [
|
||||
Validators.required,
|
||||
Validators.minLength(this.minPasswordLength),
|
||||
]),
|
||||
newPasswordConfirm: this.formBuilder.nonNullable.control("", Validators.required),
|
||||
newPasswordHint: this.formBuilder.nonNullable.control("", [
|
||||
Validators.minLength(this.minHintLength),
|
||||
Validators.maxLength(this.maxHintLength),
|
||||
]),
|
||||
checkForBreaches: this.formBuilder.nonNullable.control(true),
|
||||
},
|
||||
{
|
||||
validators: [
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldMatch,
|
||||
"newPassword",
|
||||
"confirmNewPassword",
|
||||
"newPasswordConfirm",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
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 keyService: KeyService,
|
||||
private cipherService: CipherService,
|
||||
private dialogService: DialogService,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
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(
|
||||
this.formGroup.addControl(
|
||||
"currentPassword",
|
||||
this.formBuilder.control("", Validators.required),
|
||||
this.formBuilder.nonNullable.control("", Validators.required),
|
||||
);
|
||||
|
||||
this.formGroup.addValidators([
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldNotMatch,
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
this.i18nService.t("yourNewPasswordCannotBeTheSameAsYourCurrentPassword"),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"rotateUserKey",
|
||||
this.formBuilder.control(false),
|
||||
);
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
this.formGroup.addControl("rotateUserKey", this.formBuilder.nonNullable.control(false));
|
||||
}
|
||||
}
|
||||
|
||||
private setButtonText() {
|
||||
if (this.primaryButtonText) {
|
||||
this.primaryButtonTextStr = this.i18nService.t(
|
||||
this.primaryButtonText.key,
|
||||
@@ -174,22 +226,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) {
|
||||
@@ -197,79 +236,204 @@ export class InputPasswordComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
|
||||
const passwordEvaluatedSuccessfully = await this.evaluateNewPassword(
|
||||
newPassword,
|
||||
this.passwordStrengthScore,
|
||||
this.formGroup.controls.checkForBreaches.value,
|
||||
);
|
||||
|
||||
if (!passwordEvaluatedSuccessfully) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and hash new master key
|
||||
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
|
||||
if (this.email == null) {
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to create master key.");
|
||||
}
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
const currentPassword = this.formGroup.controls.currentPassword?.value ?? "";
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
const newPasswordHint = this.formGroup.controls.newPasswordHint.value;
|
||||
const checkForBreaches = this.formGroup.controls.checkForBreaches.value;
|
||||
|
||||
// 1. Determine kdfConfig
|
||||
if (this.flow === InputPasswordFlow.AccountRegistration) {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
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.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
const currentPasswordVerified = await this.verifyCurrentPassword(
|
||||
currentPassword,
|
||||
this.kdfConfig,
|
||||
);
|
||||
if (!currentPasswordVerified) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Verify new password
|
||||
const newPasswordVerified = await this.verifyNewPassword(
|
||||
newPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdfConfig,
|
||||
this.passwordStrengthScore,
|
||||
checkForBreaches,
|
||||
);
|
||||
if (!newPasswordVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Create cryptographic keys and build a PasswordInputResult object
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
newPassword,
|
||||
this.email,
|
||||
this.kdfConfig,
|
||||
);
|
||||
|
||||
const serverMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
const newServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
const newLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
newMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
kdfConfig,
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig: this.kdfConfig,
|
||||
};
|
||||
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
passwordInputResult.currentPassword = this.formGroup.get("currentPassword")?.value;
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentPassword,
|
||||
this.email,
|
||||
this.kdfConfig,
|
||||
);
|
||||
|
||||
const currentServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
currentPassword,
|
||||
currentMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const currentLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
currentPassword,
|
||||
currentMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
passwordInputResult.currentPassword = currentPassword;
|
||||
passwordInputResult.currentMasterKey = currentMasterKey;
|
||||
passwordInputResult.currentServerMasterKeyHash = currentServerMasterKeyHash;
|
||||
passwordInputResult.currentLocalMasterKeyHash = currentLocalMasterKeyHash;
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||
}
|
||||
|
||||
// 5. Emit cryptographic keys and other password related properties
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
};
|
||||
|
||||
// Returns true if the password passes all checks, false otherwise
|
||||
private async evaluateNewPassword(
|
||||
/**
|
||||
* 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,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<boolean> {
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentPassword,
|
||||
this.email,
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
currentMasterKey,
|
||||
this.userId,
|
||||
);
|
||||
|
||||
if (decryptedUserKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the new password is not weak or breached and it passes
|
||||
* any enforced org policy options, `false` otherwise
|
||||
*/
|
||||
private async verifyNewPassword(
|
||||
newPassword: string,
|
||||
passwordStrengthScore: PasswordStrengthScore,
|
||||
checkForBreaches: boolean,
|
||||
) {
|
||||
): Promise<boolean> {
|
||||
// Check if the password is breached, weak, or both
|
||||
const passwordIsBreached =
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword));
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword)) > 0;
|
||||
|
||||
const passwordWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
const passwordIsWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
|
||||
if (passwordIsBreached && passwordWeak) {
|
||||
if (passwordIsBreached && passwordIsWeak) {
|
||||
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
@@ -279,7 +443,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
if (!userAcceptedDialog) {
|
||||
return false;
|
||||
}
|
||||
} else if (passwordWeak) {
|
||||
} else if (passwordIsWeak) {
|
||||
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPasswordDesc" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
@@ -321,4 +485,67 @@ export class InputPasswordComponent implements OnInit {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async rotateUserKeyClicked() {
|
||||
const rotateUserKeyCtrl = this.formGroup.controls.rotateUserKey;
|
||||
|
||||
const rotateUserKey = rotateUserKeyCtrl?.value;
|
||||
|
||||
if (rotateUserKey) {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted(this.userId);
|
||||
|
||||
let hasOldAttachments = false;
|
||||
|
||||
if (ciphers != null) {
|
||||
for (let i = 0; i < ciphers.length; i++) {
|
||||
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
|
||||
hasOldAttachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOldAttachments) {
|
||||
const learnMore = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
content: { key: "oldAttachmentsNeedFixDesc" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
cancelButtonText: { key: "close" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (learnMore) {
|
||||
this.platformUtilsService.launchUri(
|
||||
"https://bitwarden.com/help/attachments/#add-storage-space",
|
||||
);
|
||||
}
|
||||
|
||||
rotateUserKeyCtrl.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "rotateEncKeyTitle" },
|
||||
content:
|
||||
this.i18nService.t("updateEncryptionKeyWarning") +
|
||||
" " +
|
||||
this.i18nService.t("updateEncryptionKeyAccountExportWarning") +
|
||||
" " +
|
||||
this.i18nService.t("rotateEncKeyConfirmation"),
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
rotateUserKeyCtrl.setValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
|
||||
this.passwordStrengthScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On
|
||||
submission it creates a master key, master key hash, and emits those values to the parent (along
|
||||
with the other values found in `PasswordInputResult`).
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On form
|
||||
submission, the component creates cryptographic properties (`newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.) and emits those properties to the parent (along with the other
|
||||
values defined in `PasswordInputResult`).
|
||||
|
||||
The component is intended for re-use in different scenarios throughout the application. Therefore it
|
||||
is mostly presentational and simply emits values rather than acting on them itself. It is the job of
|
||||
@@ -16,12 +17,27 @@ the parent component to act on those values as needed.
|
||||
|
||||
<br />
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [@Inputs](#inputs)
|
||||
- [@Outputs](#outputs)
|
||||
- [The InputPasswordFlow](#the-inputpasswordflow)
|
||||
- [HTML - Form Fields](#html---form-fields)
|
||||
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
||||
- [Difference between AccountRegistration and SetInitialPasswordAuthedUser](#difference-between-accountregistration-and-setinitialpasswordautheduser)
|
||||
- [Validation](#validation)
|
||||
- [Submit Logic](#submit-logic)
|
||||
- [Example](#example)
|
||||
|
||||
<br />
|
||||
|
||||
## `@Input()`'s
|
||||
|
||||
**Required**
|
||||
|
||||
- `inputPasswordFlow` - the parent component must provide the correct flow, which is used to
|
||||
determine which form input elements will be displayed in the UI.
|
||||
- `flow` - the parent component must provide an `InputPasswordFlow`, which is used to determine
|
||||
which form input elements will be displayed in the UI and which cryptographic keys will be created
|
||||
and emitted.
|
||||
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
||||
create a master key.
|
||||
|
||||
@@ -29,13 +45,15 @@ the parent component to act on those values as needed.
|
||||
|
||||
- `loading` - a boolean used to indicate that the parent component is performing some
|
||||
long-running/async operation and that the form should be disabled until the operation is complete.
|
||||
The primary button will also show a spinner if `loading` true.
|
||||
The primary button will also show a spinner if `loading` is true.
|
||||
- `masterPasswordPolicyOptions` - used to display and enforce master password policy requirements.
|
||||
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
||||
opposed to full-width)
|
||||
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
- `secondaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
|
||||
<br />
|
||||
|
||||
## `@Output()`'s
|
||||
|
||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||
@@ -45,25 +63,31 @@ the parent component to act on those values as needed.
|
||||
|
||||
<br />
|
||||
|
||||
## Form Input Fields
|
||||
## The `InputPasswordFlow`
|
||||
|
||||
The `InputPasswordComponent` can handle up to 6 different form input fields, depending on the
|
||||
`InputPasswordFlow` provided by the parent component.
|
||||
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
||||
credential generation logic of the component.
|
||||
|
||||
**InputPasswordFlow.SetInitialPassword**
|
||||
<br />
|
||||
|
||||
### HTML - Form Fields
|
||||
|
||||
The `InputPasswordFlow` determines which form fields get displayed in the UI.
|
||||
|
||||
**`InputPasswordFlow.AccountRegistration`** and **`InputPasswordFlow.SetInitialPasswordAuthedUser`**
|
||||
|
||||
- Input: New password
|
||||
- Input: Confirm new password
|
||||
- Input: Hint
|
||||
- Checkbox: Check for breaches
|
||||
|
||||
**InputPasswordFlow.ChangePassword**
|
||||
**`InputPasswordFlow.ChangePassword`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Input: Current password (as the first element in the UI)
|
||||
|
||||
**InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation**
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
@@ -71,49 +95,122 @@ Includes everything above, plus:
|
||||
|
||||
<br />
|
||||
|
||||
### TypeScript - Credential Generation
|
||||
|
||||
- The `AccountRegistration` and `SetInitialPasswordAuthedUser` flows involve a user setting their
|
||||
password for the first time. Therefore on submit the component will only generate new credentials
|
||||
(`newMasterKey`) and not current credentials (`currentMasterKey`).
|
||||
- The `ChangePassword` and `ChangePasswordWithOptionalUserKeyRotation` flows both require the user
|
||||
to enter a current password along with a new password. Therefore on submit the component will
|
||||
generate current credentials (`currentMasterKey`) along with new credentials (`newMasterKey`).
|
||||
|
||||
<br />
|
||||
|
||||
### Difference between `AccountRegistration` and `SetInitialPasswordAuthedUser`
|
||||
|
||||
These two flows are similar in that they display the same form fields and only generate new
|
||||
credentials, but we need to keep them separate for the following reasons:
|
||||
|
||||
- `AccountRegistration` involves scenarios where we have no existing user, and **thus NO active
|
||||
account `userId`**:
|
||||
|
||||
- Standard Account Registration
|
||||
- Email Invite Account Registration
|
||||
- Trial Initiation Account Registration
|
||||
|
||||
<br />
|
||||
|
||||
- `SetInitialPasswordAuthedUser` involves scenarios where we do have an existing and authed user,
|
||||
and **thus an active account `userId`**:
|
||||
- A "just-in-time" (JIT) provisioned user joins a master password (MP) encryption org and must set
|
||||
their initial password
|
||||
- A "just-in-time" (JIT) provisioned user joins a trusted device encryption (TDE) org with a
|
||||
starting role that requires them to have/set their initial password
|
||||
- A note on JIT provisioned user flows:
|
||||
- Even though a JIT provisioned user is a brand-new user who was “just” created, we consider
|
||||
them to be an “existing authed user” _from the perspective of the set-password flow_. This
|
||||
is because at the time they set their initial password, their account already exists in the
|
||||
database (before setting their password) and they have already authenticated via SSO.
|
||||
- The same is not true in the account registration flows above—that is, during account
|
||||
registration when a user reaches the `/finish-signup` or `/trial-initiation` page to set
|
||||
their initial password, their account does not yet exist in the database, and will only be
|
||||
created once they set an initial password.
|
||||
- An existing user in a TDE org logs in after the org admin upgraded the user to a role that now
|
||||
requires them to have/set their initial password
|
||||
- An existing user logs in after their org admin offboarded the org from TDE, and the user must
|
||||
now have/set their initial password
|
||||
|
||||
The presence or absence of an active account `userId` is important because it determines how we get
|
||||
the correct `kdfConfig` prior to key generation:
|
||||
|
||||
- If there is no `userId` passed down from the parent, we default to `DEFAULT_KDF_CONFIG`
|
||||
- If there is a `userId` passed down from the parent, we get the `kdfConfig` from state using the
|
||||
`userId`
|
||||
|
||||
That said, we cannot mark the `userId` as a required via `@Input({ required: true })` because
|
||||
`AccountRegistration` flows will not have a `userId`. But we still want to require a `userId` in a
|
||||
`SetInitialPasswordAuthedUser` flow. Therefore the `InputPasswordComponent` has init logic that
|
||||
ensures the following:
|
||||
|
||||
- If the passed down flow is `AccountRegistration`, require that the parent **MUST NOT** have passed
|
||||
down a `userId`
|
||||
- If the passed down flow is `SetInitialPasswordAuthedUser` require that the parent must also have
|
||||
passed down a `userId`
|
||||
|
||||
If either of these checks is not met, the component throws to alert the dev of a mistake.
|
||||
|
||||
<br />
|
||||
|
||||
## Validation
|
||||
|
||||
Validation ensures that:
|
||||
Form validators ensure that:
|
||||
|
||||
- The current password and new password are NOT the same
|
||||
- The new password and confirmed new password are the same
|
||||
- The new password and password hint are NOT the same
|
||||
|
||||
Additional submit logic validation ensures that:
|
||||
|
||||
- The new password adheres to any enforced master password policy options (that were passed down
|
||||
from the parent)
|
||||
|
||||
<br />
|
||||
|
||||
## On Submit
|
||||
## Submit Logic
|
||||
|
||||
When the form is submitted, the `InputPasswordComponent` does the following in order:
|
||||
|
||||
1. If the user selected the checkbox to check for password breaches, they will recieve a popup
|
||||
dialog if their entered password is found in a breach. The popup will give them the option to
|
||||
continue with the password or to back out and choose a different password.
|
||||
2. If there is a master password policy being enforced by an org, it will check to make sure the
|
||||
entered master password meets the policy requirements.
|
||||
3. The component will use the password, email, and default kdfConfig to create a master key and
|
||||
master key hash.
|
||||
4. The component will emit the following values (defined in the `PasswordInputResult` interface) to
|
||||
be used by the parent component as needed:
|
||||
1. Verifies inputs:
|
||||
- Checks that the current password is correct (if it was required in the flow)
|
||||
- Checks if the new password is found in a breach and warns the user if so (if the user selected
|
||||
the checkbox)
|
||||
- Checks that the new password meets any master password policy requirements enforced by an org
|
||||
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.)
|
||||
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
||||
`PasswordInputResult`) to be used by the parent as needed.
|
||||
|
||||
```typescript
|
||||
export interface PasswordInputResult {
|
||||
// Properties starting with "current..." are included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
currentPassword?: string;
|
||||
currentMasterKey?: MasterKey;
|
||||
currentServerMasterKeyHash?: string;
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string; // included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||
}
|
||||
```
|
||||
|
||||
# Example - InputPasswordFlow.SetInitialPassword
|
||||
# Example
|
||||
|
||||
<Story of={stories.SetInitialPassword} />
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
<br />
|
||||
|
||||
# Example - With Policy Requrements
|
||||
|
||||
<Story of={stories.WithPolicies} />
|
||||
<Story of={stories.ChangePasswordWithOptionalUserKeyRotation} />
|
||||
|
||||
@@ -2,14 +2,20 @@ import { importProvidersFrom } from "@angular/core";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
import { ZXCVBNResult } from "zxcvbn";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
// FIXME: remove `src` and fix import
|
||||
@@ -26,12 +32,47 @@ export default {
|
||||
providers: [
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
importProvidersFrom(BrowserAnimationsModule),
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "1" as UserId,
|
||||
name: "User",
|
||||
email: "user@email.com",
|
||||
emailVerified: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuditService,
|
||||
useValue: {
|
||||
passwordLeaked: () => Promise.resolve(1),
|
||||
} as Partial<AuditService>,
|
||||
},
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: {
|
||||
getAllDecrypted: () => Promise.resolve([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: {
|
||||
getKdfConfig$: () => of(DEFAULT_KDF_CONFIG),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MasterPasswordServiceAbstraction,
|
||||
useValue: {
|
||||
decryptUserKeyWithMasterKey: () => Promise.resolve("example-decrypted-user-key"),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
launchUri: () => Promise.resolve(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: KeyService,
|
||||
useValue: {
|
||||
@@ -87,11 +128,14 @@ export default {
|
||||
],
|
||||
args: {
|
||||
InputPasswordFlow: {
|
||||
SetInitialPassword: InputPasswordFlow.SetInitialPassword,
|
||||
AccountRegistration: InputPasswordFlow.AccountRegistration,
|
||||
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||
ChangePasswordWithOptionalUserKeyRotation:
|
||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||
},
|
||||
userId: "1" as UserId,
|
||||
email: "user@email.com",
|
||||
masterPasswordPolicyOptions: {
|
||||
minComplexity: 4,
|
||||
minLength: 14,
|
||||
@@ -108,11 +152,27 @@ export default {
|
||||
|
||||
type Story = StoryObj<InputPasswordComponent>;
|
||||
|
||||
export const SetInitialPassword: Story = {
|
||||
export const AccountRegistration: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"></auth-input-password>
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SetInitialPasswordAuthedUser: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -121,7 +181,11 @@ export const ChangePassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.ChangePassword"></auth-input-password>
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.ChangePassword"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -131,7 +195,9 @@ export const ChangePasswordWithOptionalUserKeyRotation: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
[flow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
@@ -142,7 +208,9 @@ export const WithPolicies: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -154,7 +222,8 @@ export const SecondaryButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
@@ -167,7 +236,8 @@ export const SecondaryButtonWithPlaceHolderText: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
@@ -180,7 +250,8 @@ export const InlineButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[inlineButtons]="true"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -192,7 +263,8 @@ export const InlineButtons: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
[inlineButtons]="true"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface PasswordInputResult {
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string;
|
||||
currentMasterKey?: MasterKey;
|
||||
currentServerMasterKeyHash?: string;
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
|
||||
@@ -58,12 +58,12 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
emailVerificationToken = "emailVerificationToken";
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
newPassword: "password",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -93,8 +93,8 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
|
||||
@@ -36,7 +36,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
providerUserId?: string,
|
||||
): Promise<void> {
|
||||
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(
|
||||
passwordInputResult.masterKey,
|
||||
passwordInputResult.newMasterKey,
|
||||
);
|
||||
|
||||
if (!newUserKey || !newEncUserKey) {
|
||||
@@ -79,8 +79,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
|
||||
const registerFinishRequest = new RegisterFinishRequest(
|
||||
email,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.hint,
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
passwordInputResult.newPasswordHint,
|
||||
encryptedUserKey,
|
||||
userAsymmetricKeysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<auth-input-password
|
||||
*ngIf="!loading"
|
||||
[email]="email"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[loading]="submitting"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -111,12 +111,12 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userId = "userId" as UserId;
|
||||
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
newPassword: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
@@ -131,9 +131,9 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.hint,
|
||||
passwordInputResult.newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
@@ -43,10 +43,10 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
hint,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
@@ -60,7 +60,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
}
|
||||
}
|
||||
|
||||
const protectedUserKey = await this.makeProtectedUserKey(masterKey, userId);
|
||||
const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId);
|
||||
if (protectedUserKey == null) {
|
||||
throw new Error("protectedUserKey not found. Could not set password.");
|
||||
}
|
||||
@@ -70,12 +70,12 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
serverMasterKeyHash,
|
||||
newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
hint,
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType, // kdfConfig is always DEFAULT_KDF_CONFIG (see InputPasswordComponent)
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
@@ -85,14 +85,14 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(masterKey, kdfConfig, protectedUserKey, userId);
|
||||
await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId);
|
||||
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(serverMasterKeyHash, orgId, userId);
|
||||
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: PBKDF2KdfConfig,
|
||||
kdfConfig: KdfConfig,
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
userId: UserId,
|
||||
) {
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -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;
|
||||
@@ -60,9 +60,9 @@ export class SetPasswordJitComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = activeAccount?.id;
|
||||
this.email = activeAccount?.email;
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
@@ -97,14 +97,12 @@ export class SetPasswordJitComponent implements OnInit {
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
...passwordInputResult,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
userId,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint: string;
|
||||
kdfConfig: KdfConfig;
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
|
||||
Reference in New Issue
Block a user