mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +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 @@
|
|||||||
|
export * from "./web-change-password.service";
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { 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 { 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";
|
||||||
|
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
|
||||||
|
|
||||||
|
import { WebChangePasswordService } from "./web-change-password.service";
|
||||||
|
|
||||||
|
describe("WebChangePasswordService", () => {
|
||||||
|
let keyService: MockProxy<KeyService>;
|
||||||
|
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||||
|
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||||
|
let userKeyRotationService: MockProxy<UserKeyRotationService>;
|
||||||
|
|
||||||
|
let sut: ChangePasswordService;
|
||||||
|
|
||||||
|
const userId = "userId" as UserId;
|
||||||
|
const user: Account = {
|
||||||
|
id: userId,
|
||||||
|
email: "email",
|
||||||
|
emailVerified: false,
|
||||||
|
name: "name",
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentPassword = "currentPassword";
|
||||||
|
const newPassword = "newPassword";
|
||||||
|
const newPasswordHint = "newPasswordHint";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
keyService = mock<KeyService>();
|
||||||
|
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||||
|
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||||
|
userKeyRotationService = mock<UserKeyRotationService>();
|
||||||
|
|
||||||
|
sut = new WebChangePasswordService(
|
||||||
|
keyService,
|
||||||
|
masterPasswordApiService,
|
||||||
|
masterPasswordService,
|
||||||
|
userKeyRotationService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rotateUserKeyMasterPasswordAndEncryptedData()", () => {
|
||||||
|
it("should call the method with the same name on the UserKeyRotationService with the correct arguments", async () => {
|
||||||
|
// Arrange & Act
|
||||||
|
await sut.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||||
|
currentPassword,
|
||||||
|
newPassword,
|
||||||
|
user,
|
||||||
|
newPasswordHint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(
|
||||||
|
userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData,
|
||||||
|
).toHaveBeenCalledWith(currentPassword, newPassword, user, newPasswordHint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ChangePasswordService, DefaultChangePasswordService } 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||||
|
import { KeyService } from "@bitwarden/key-management";
|
||||||
|
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
|
||||||
|
|
||||||
|
export class WebChangePasswordService
|
||||||
|
extends DefaultChangePasswordService
|
||||||
|
implements ChangePasswordService
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
protected keyService: KeyService,
|
||||||
|
protected masterPasswordApiService: MasterPasswordApiService,
|
||||||
|
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
|
private userKeyRotationService: UserKeyRotationService,
|
||||||
|
) {
|
||||||
|
super(keyService, masterPasswordApiService, masterPasswordService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async rotateUserKeyMasterPasswordAndEncryptedData(
|
||||||
|
currentPassword: string,
|
||||||
|
newPassword: string,
|
||||||
|
user: Account,
|
||||||
|
newPasswordHint: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.userKeyRotationService.rotateUserKeyMasterPasswordAndEncryptedData(
|
||||||
|
currentPassword,
|
||||||
|
newPassword,
|
||||||
|
user,
|
||||||
|
newPasswordHint,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from "./change-password";
|
||||||
export * from "./login";
|
export * from "./login";
|
||||||
export * from "./login-decryption-options";
|
export * from "./login-decryption-options";
|
||||||
export * from "./webauthn-login";
|
export * from "./webauthn-login";
|
||||||
|
|||||||
@@ -185,11 +185,11 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
emailVerificationToken = "emailVerificationToken";
|
emailVerificationToken = "emailVerificationToken";
|
||||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||||
passwordInputResult = {
|
passwordInputResult = {
|
||||||
masterKey: masterKey,
|
newMasterKey: masterKey,
|
||||||
serverMasterKeyHash: "serverMasterKeyHash",
|
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||||
localMasterKeyHash: "localMasterKeyHash",
|
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||||
hint: "hint",
|
newPasswordHint: "newPasswordHint",
|
||||||
newPassword: "newPassword",
|
newPassword: "newPassword",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -231,8 +231,8 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: emailVerificationToken,
|
emailVerificationToken: emailVerificationToken,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
@@ -267,8 +267,8 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: undefined,
|
emailVerificationToken: undefined,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
@@ -308,8 +308,8 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: undefined,
|
emailVerificationToken: undefined,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
@@ -351,8 +351,8 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: undefined,
|
emailVerificationToken: undefined,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
@@ -396,8 +396,8 @@ describe("WebRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: undefined,
|
emailVerificationToken: undefined,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
|||||||
|
|
||||||
import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service";
|
import { UserKeyRotationService } from "../../key-management/key-rotation/user-key-rotation.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use the auth `PasswordSettingsComponent` instead
|
||||||
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-change-password",
|
selector: "app-change-password",
|
||||||
templateUrl: "change-password.component.html",
|
templateUrl: "change-password.component.html",
|
||||||
@@ -132,7 +135,7 @@ export class ChangePasswordComponent
|
|||||||
content:
|
content:
|
||||||
this.i18nService.t("updateEncryptionKeyWarning") +
|
this.i18nService.t("updateEncryptionKeyWarning") +
|
||||||
" " +
|
" " +
|
||||||
this.i18nService.t("updateEncryptionKeyExportWarning") +
|
this.i18nService.t("updateEncryptionKeyAccountExportWarning") +
|
||||||
" " +
|
" " +
|
||||||
this.i18nService.t("rotateEncKeyConfirmation"),
|
this.i18nService.t("rotateEncKeyConfirmation"),
|
||||||
type: "warning",
|
type: "warning",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="tabbed-header">
|
||||||
|
<h1>{{ "changeMasterPassword" | i18n }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tw-max-w-lg tw-mb-12">
|
||||||
|
<bit-callout type="warning">{{ "loggedOutWarning" | i18n }}</bit-callout>
|
||||||
|
<auth-change-password [inputPasswordFlow]="inputPasswordFlow"></auth-change-password>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-webauthn-login-settings></app-webauthn-login-settings>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { ChangePasswordComponent, InputPasswordFlow } from "@bitwarden/auth/angular";
|
||||||
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { CalloutModule } from "@bitwarden/components";
|
||||||
|
import { I18nPipe } from "@bitwarden/ui-common";
|
||||||
|
|
||||||
|
import { WebauthnLoginSettingsModule } from "../../webauthn-login-settings";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
standalone: true,
|
||||||
|
selector: "app-password-settings",
|
||||||
|
templateUrl: "password-settings.component.html",
|
||||||
|
imports: [CalloutModule, ChangePasswordComponent, I18nPipe, WebauthnLoginSettingsModule],
|
||||||
|
})
|
||||||
|
export class PasswordSettingsComponent implements OnInit {
|
||||||
|
inputPasswordFlow = InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
const userHasMasterPassword = await firstValueFrom(
|
||||||
|
this.userDecryptionOptionsService.hasMasterPassword$,
|
||||||
|
);
|
||||||
|
if (!userHasMasterPassword) {
|
||||||
|
await this.router.navigate(["/settings/security/two-factor"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
import { RouterModule, Routes } from "@angular/router";
|
import { RouterModule, Routes } from "@angular/router";
|
||||||
|
|
||||||
|
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
|
||||||
import { ChangePasswordComponent } from "../change-password.component";
|
import { ChangePasswordComponent } from "../change-password.component";
|
||||||
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
|
import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
|
||||||
|
|
||||||
import { DeviceManagementComponent } from "./device-management.component";
|
import { DeviceManagementComponent } from "./device-management.component";
|
||||||
|
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
|
||||||
import { SecurityKeysComponent } from "./security-keys.component";
|
import { SecurityKeysComponent } from "./security-keys.component";
|
||||||
import { SecurityComponent } from "./security.component";
|
import { SecurityComponent } from "./security.component";
|
||||||
|
|
||||||
@@ -14,10 +18,31 @@ const routes: Routes = [
|
|||||||
component: SecurityComponent,
|
component: SecurityComponent,
|
||||||
data: { titleId: "security" },
|
data: { titleId: "security" },
|
||||||
children: [
|
children: [
|
||||||
{ path: "", pathMatch: "full", redirectTo: "change-password" },
|
{ path: "", pathMatch: "full", redirectTo: "password" },
|
||||||
{
|
{
|
||||||
path: "change-password",
|
path: "change-password",
|
||||||
component: ChangePasswordComponent,
|
component: ChangePasswordComponent,
|
||||||
|
canActivate: [
|
||||||
|
canAccessFeature(
|
||||||
|
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||||
|
false,
|
||||||
|
"/settings/security/password",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
data: { titleId: "masterPassword" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "password",
|
||||||
|
component: PasswordSettingsComponent,
|
||||||
|
canActivate: [
|
||||||
|
canAccessFeature(
|
||||||
|
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||||
|
true,
|
||||||
|
"/settings/security/change-password",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
],
|
||||||
data: { titleId: "masterPassword" },
|
data: { titleId: "masterPassword" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<app-header>
|
<app-header>
|
||||||
<bit-tab-nav-bar slot="tabs">
|
<bit-tab-nav-bar slot="tabs">
|
||||||
<ng-container *ngIf="showChangePassword">
|
<ng-container *ngIf="showChangePassword">
|
||||||
<bit-tab-link route="change-password">{{ "masterPassword" | i18n }}</bit-tab-link>
|
<bit-tab-link [route]="changePasswordRoute">{{ "masterPassword" | i18n }}</bit-tab-link>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<bit-tab-link route="two-factor">{{ "twoStepLogin" | i18n }}</bit-tab-link>
|
<bit-tab-link route="two-factor">{{ "twoStepLogin" | i18n }}</bit-tab-link>
|
||||||
<bit-tab-link route="device-management">{{ "devices" | i18n }}</bit-tab-link>
|
<bit-tab-link route="device-management">{{ "devices" | i18n }}</bit-tab-link>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -10,6 +11,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
|||||||
})
|
})
|
||||||
export class SecurityComponent implements OnInit {
|
export class SecurityComponent implements OnInit {
|
||||||
showChangePassword = true;
|
showChangePassword = true;
|
||||||
|
changePasswordRoute = "change-password";
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
@@ -18,5 +20,12 @@ export class SecurityComponent implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
|
this.showChangePassword = await this.userVerificationService.hasMasterPassword();
|
||||||
|
|
||||||
|
const changePasswordRefreshFlag = await this.configService.getFeatureFlag(
|
||||||
|
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||||
|
);
|
||||||
|
if (changePasswordRefreshFlag) {
|
||||||
|
this.changePasswordRoute = "password";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,100 @@
|
|||||||
<div *ngIf="!useTrialStepper">
|
@if (initializing) {
|
||||||
<auth-input-password
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
<i
|
||||||
[email]="email"
|
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
|
||||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
title="{{ 'loading' | i18n }}"
|
||||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
aria-hidden="true"
|
||||||
[primaryButtonText]="{ key: 'createAccount' }"
|
></i>
|
||||||
></auth-input-password>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="useTrialStepper">
|
} @else {
|
||||||
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
<div *ngIf="!useTrialStepper">
|
||||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
<auth-input-password
|
||||||
<auth-input-password
|
[flow]="inputPasswordFlow"
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[email]="email"
|
||||||
[email]="email"
|
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
[primaryButtonText]="{ key: 'createAccount' }"
|
||||||
[primaryButtonText]="{ key: 'createAccount' }"
|
></auth-input-password>
|
||||||
></auth-input-password>
|
</div>
|
||||||
</app-vertical-step>
|
<div *ngIf="useTrialStepper">
|
||||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
||||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||||
<button
|
<auth-input-password
|
||||||
type="button"
|
[flow]="inputPasswordFlow"
|
||||||
bitButton
|
[email]="email"
|
||||||
buttonType="primary"
|
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||||
[disabled]="orgInfoFormGroup.controls.name.invalid"
|
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||||
[loading]="loading && (trialPaymentOptional$ | async)"
|
[primaryButtonText]="{ key: 'createAccount' }"
|
||||||
(click)="orgNameEntrySubmit()"
|
></auth-input-password>
|
||||||
>
|
</app-vertical-step>
|
||||||
{{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||||
</button>
|
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||||
</app-vertical-step>
|
<button
|
||||||
<app-vertical-step
|
|
||||||
label="Billing"
|
|
||||||
[subLabel]="billingSubLabel"
|
|
||||||
*ngIf="!(trialPaymentOptional$ | async) && !isSecretsManagerFree"
|
|
||||||
>
|
|
||||||
<app-trial-billing-step
|
|
||||||
*ngIf="stepper.selectedIndex === 2"
|
|
||||||
[organizationInfo]="{
|
|
||||||
name: orgInfoFormGroup.value.name,
|
|
||||||
email: orgInfoFormGroup.value.billingEmail,
|
|
||||||
type: trialOrganizationType,
|
|
||||||
}"
|
|
||||||
[subscriptionProduct]="
|
|
||||||
product === ProductType.SecretsManager
|
|
||||||
? SubscriptionProduct.SecretsManager
|
|
||||||
: SubscriptionProduct.PasswordManager
|
|
||||||
"
|
|
||||||
[trialLength]="trialLength"
|
|
||||||
(steppedBack)="previousStep()"
|
|
||||||
(organizationCreated)="createdOrganization($event)"
|
|
||||||
>
|
|
||||||
</app-trial-billing-step>
|
|
||||||
</app-vertical-step>
|
|
||||||
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
|
||||||
<app-trial-confirmation-details
|
|
||||||
[email]="email"
|
|
||||||
[orgLabel]="orgLabel"
|
|
||||||
[product]="this.product"
|
|
||||||
[trialLength]="trialLength"
|
|
||||||
></app-trial-confirmation-details>
|
|
||||||
<div class="tw-mb-3 tw-flex">
|
|
||||||
<a
|
|
||||||
type="button"
|
type="button"
|
||||||
bitButton
|
bitButton
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
[routerLink]="
|
[disabled]="orgInfoFormGroup.controls.name.invalid"
|
||||||
|
[loading]="loading && (trialPaymentOptional$ | async)"
|
||||||
|
(click)="orgNameEntrySubmit()"
|
||||||
|
>
|
||||||
|
{{ (trialPaymentOptional$ | async) ? ("startTrial" | i18n) : ("next" | i18n) }}
|
||||||
|
</button>
|
||||||
|
</app-vertical-step>
|
||||||
|
<app-vertical-step
|
||||||
|
label="Billing"
|
||||||
|
[subLabel]="billingSubLabel"
|
||||||
|
*ngIf="!(trialPaymentOptional$ | async) && !isSecretsManagerFree"
|
||||||
|
>
|
||||||
|
<app-trial-billing-step
|
||||||
|
*ngIf="stepper.selectedIndex === 2"
|
||||||
|
[organizationInfo]="{
|
||||||
|
name: orgInfoFormGroup.value.name,
|
||||||
|
email: orgInfoFormGroup.value.billingEmail,
|
||||||
|
type: trialOrganizationType,
|
||||||
|
}"
|
||||||
|
[subscriptionProduct]="
|
||||||
product === ProductType.SecretsManager
|
product === ProductType.SecretsManager
|
||||||
? ['/sm', orgId]
|
? SubscriptionProduct.SecretsManager
|
||||||
: ['/organizations', orgId, 'vault']
|
: SubscriptionProduct.PasswordManager
|
||||||
"
|
"
|
||||||
|
[trialLength]="trialLength"
|
||||||
|
(steppedBack)="previousStep()"
|
||||||
|
(organizationCreated)="createdOrganization($event)"
|
||||||
>
|
>
|
||||||
{{ "getStarted" | i18n | titlecase }}
|
</app-trial-billing-step>
|
||||||
</a>
|
</app-vertical-step>
|
||||||
<a
|
<app-vertical-step label="Confirmation Details" [applyBorder]="false">
|
||||||
type="button"
|
<app-trial-confirmation-details
|
||||||
bitButton
|
[email]="email"
|
||||||
buttonType="secondary"
|
[orgLabel]="orgLabel"
|
||||||
[routerLink]="['/organizations', orgId, 'members']"
|
[product]="this.product"
|
||||||
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
[trialLength]="trialLength"
|
||||||
>
|
></app-trial-confirmation-details>
|
||||||
{{ "inviteUsers" | i18n }}
|
<div class="tw-mb-3 tw-flex">
|
||||||
</a>
|
<a
|
||||||
</div>
|
type="button"
|
||||||
</app-vertical-step>
|
bitButton
|
||||||
</app-vertical-stepper>
|
buttonType="primary"
|
||||||
</div>
|
[routerLink]="
|
||||||
|
product === ProductType.SecretsManager
|
||||||
|
? ['/sm', orgId]
|
||||||
|
: ['/organizations', orgId, 'vault']
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ "getStarted" | i18n | titlecase }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
bitButton
|
||||||
|
buttonType="secondary"
|
||||||
|
[routerLink]="['/organizations', orgId, 'members']"
|
||||||
|
class="tw-ml-3 tw-inline-flex tw-items-center tw-px-3"
|
||||||
|
>
|
||||||
|
{{ "inviteUsers" | i18n }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</app-vertical-step>
|
||||||
|
</app-vertical-stepper>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export type InitiationPath =
|
|||||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||||
|
|
||||||
InputPasswordFlow = InputPasswordFlow;
|
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||||
|
initializing = true;
|
||||||
|
|
||||||
/** Password Manager or Secrets Manager */
|
/** Password Manager or Secrets Manager */
|
||||||
product: ProductType;
|
product: ProductType;
|
||||||
@@ -203,6 +204,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
this.orgInfoFormGroup.controls.name.markAsTouched();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.initializing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
LoginDecryptionOptionsService,
|
LoginDecryptionOptionsService,
|
||||||
TwoFactorAuthComponentService,
|
TwoFactorAuthComponentService,
|
||||||
TwoFactorAuthDuoComponentService,
|
TwoFactorAuthDuoComponentService,
|
||||||
|
ChangePasswordService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
@@ -110,6 +111,7 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde
|
|||||||
import { flagEnabled } from "../../utils/flags";
|
import { flagEnabled } from "../../utils/flags";
|
||||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||||
import {
|
import {
|
||||||
|
WebChangePasswordService,
|
||||||
WebSetPasswordJitService,
|
WebSetPasswordJitService,
|
||||||
WebRegistrationFinishService,
|
WebRegistrationFinishService,
|
||||||
WebLoginComponentService,
|
WebLoginComponentService,
|
||||||
@@ -123,6 +125,7 @@ import { AcceptOrganizationInviteService } from "../auth/organization-invite/acc
|
|||||||
import { HtmlStorageService } from "../core/html-storage.service";
|
import { HtmlStorageService } from "../core/html-storage.service";
|
||||||
import { I18nService } from "../core/i18n.service";
|
import { I18nService } from "../core/i18n.service";
|
||||||
import { WebFileDownloadService } from "../core/web-file-download.service";
|
import { WebFileDownloadService } from "../core/web-file-download.service";
|
||||||
|
import { UserKeyRotationService } from "../key-management/key-rotation/user-key-rotation.service";
|
||||||
import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service";
|
import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service";
|
||||||
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
import { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
||||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||||
@@ -373,6 +376,16 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultSshImportPromptService,
|
useClass: DefaultSshImportPromptService,
|
||||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ChangePasswordService,
|
||||||
|
useClass: WebChangePasswordService,
|
||||||
|
deps: [
|
||||||
|
KeyServiceAbstraction,
|
||||||
|
MasterPasswordApiService,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
UserKeyRotationService,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -4549,8 +4549,8 @@
|
|||||||
"updateEncryptionKeyWarning": {
|
"updateEncryptionKeyWarning": {
|
||||||
"message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed."
|
"message": "After updating your encryption key, you are required to log out and back in to all Bitwarden applications that you are currently using (such as the mobile app or browser extensions). Failure to log out and back in (which downloads your new encryption key) may result in data corruption. We will attempt to log you out automatically, however, it may be delayed."
|
||||||
},
|
},
|
||||||
"updateEncryptionKeyExportWarning": {
|
"updateEncryptionKeyAccountExportWarning": {
|
||||||
"message": "Any encrypted exports that you have saved will also become invalid."
|
"message": "Any account restricted exports you have saved will become invalid."
|
||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
"message": "Subscription"
|
"message": "Subscription"
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import {
|
|||||||
TwoFactorAuthComponentService,
|
TwoFactorAuthComponentService,
|
||||||
TwoFactorAuthEmailComponentService,
|
TwoFactorAuthEmailComponentService,
|
||||||
TwoFactorAuthWebAuthnComponentService,
|
TwoFactorAuthWebAuthnComponentService,
|
||||||
|
ChangePasswordService,
|
||||||
|
DefaultChangePasswordService,
|
||||||
} from "@bitwarden/auth/angular";
|
} from "@bitwarden/auth/angular";
|
||||||
import {
|
import {
|
||||||
AuthRequestApiService,
|
AuthRequestApiService,
|
||||||
@@ -1538,6 +1540,15 @@ const safeProviders: SafeProvider[] = [
|
|||||||
useClass: DefaultCipherEncryptionService,
|
useClass: DefaultCipherEncryptionService,
|
||||||
deps: [SdkService, LogService],
|
deps: [SdkService, LogService],
|
||||||
}),
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: ChangePasswordService,
|
||||||
|
useClass: DefaultChangePasswordService,
|
||||||
|
deps: [
|
||||||
|
KeyService,
|
||||||
|
MasterPasswordApiServiceAbstraction,
|
||||||
|
InternalMasterPasswordServiceAbstraction,
|
||||||
|
],
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|||||||
@@ -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/anon-layout-wrapper-data.service";
|
||||||
export * from "./anon-layout/default-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
|
// fingerprint dialog
|
||||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
<bit-form-field
|
<bit-form-field
|
||||||
*ngIf="
|
*ngIf="
|
||||||
inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
flow === InputPasswordFlow.ChangePassword ||
|
||||||
inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
||||||
@@ -58,12 +58,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="input-password-form_confirm-new-password"
|
id="input-password-form_new-password-confirm"
|
||||||
bitInput
|
bitInput
|
||||||
type="password"
|
type="password"
|
||||||
formControlName="confirmNewPassword"
|
formControlName="newPasswordConfirm"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -76,21 +76,33 @@
|
|||||||
|
|
||||||
<bit-form-field>
|
<bit-form-field>
|
||||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||||
<input bitInput formControlName="hint" />
|
<input id="input-password-form_new-password-hint" bitInput formControlName="newPasswordHint" />
|
||||||
<bit-hint>
|
<bit-hint>
|
||||||
{{ "masterPassHintText" | i18n: formGroup.value.hint.length : maxHintLength.toString() }}
|
{{
|
||||||
|
"masterPassHintText"
|
||||||
|
| i18n: formGroup.value.newPasswordHint.length : maxHintLength.toString()
|
||||||
|
}}
|
||||||
</bit-hint>
|
</bit-hint>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-control>
|
<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-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
|
|
||||||
<bit-form-control
|
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
||||||
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
<input
|
||||||
>
|
id="input-password-form_rotate-user-key"
|
||||||
<input type="checkbox" bitCheckbox formControlName="rotateUserKey" />
|
type="checkbox"
|
||||||
|
bitCheckbox
|
||||||
|
formControlName="rotateUserKey"
|
||||||
|
(change)="rotateUserKeyClicked()"
|
||||||
|
/>
|
||||||
<bit-label>
|
<bit-label>
|
||||||
{{ "rotateAccountEncKey" | i18n }}
|
{{ "rotateAccountEncKey" | i18n }}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import {
|
import {
|
||||||
@@ -9,9 +10,13 @@ import {
|
|||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
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 { 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 { 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 { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 {
|
import {
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
@@ -23,7 +28,12 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
Translation,
|
Translation,
|
||||||
} from "@bitwarden/components";
|
} 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
|
// FIXME: remove `src` and fix import
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// 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";
|
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
|
// FIXME: update to use a const object instead of a typescript enum
|
||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||||
export enum InputPasswordFlow {
|
export enum InputPasswordFlow {
|
||||||
/**
|
/**
|
||||||
* - Input: New password
|
* Form elements displayed:
|
||||||
* - Input: Confirm new password
|
* - [Input] New password
|
||||||
* - Input: Hint
|
* - [Input] New password confirm
|
||||||
* - Checkbox: Check for breaches
|
* - [Input] New password hint
|
||||||
|
* - [Checkbox] Check for breaches
|
||||||
*/
|
*/
|
||||||
SetInitialPassword,
|
AccountRegistration, // important: this flow does not involve an activeAccount/userId
|
||||||
/**
|
SetInitialPasswordAuthedUser,
|
||||||
* Everything above, plus:
|
/*
|
||||||
* - Input: Current password (as the first element in the UI)
|
* All form elements above, plus: [Input] Current password (as the first element in the UI)
|
||||||
*/
|
*/
|
||||||
ChangePassword,
|
ChangePassword,
|
||||||
/**
|
/**
|
||||||
* Everything above, plus:
|
* All form elements above, plus: [Checkbox] Rotate account encryption key (as the last element in the UI)
|
||||||
* - Checkbox: Rotate account encryption key (as the last element in the UI)
|
|
||||||
*/
|
*/
|
||||||
ChangePasswordWithOptionalUserKeyRotation,
|
ChangePasswordWithOptionalUserKeyRotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InputPasswordForm {
|
||||||
|
newPassword: FormControl<string>;
|
||||||
|
newPasswordConfirm: FormControl<string>;
|
||||||
|
newPasswordHint: FormControl<string>;
|
||||||
|
checkForBreaches: FormControl<boolean>;
|
||||||
|
|
||||||
|
currentPassword?: FormControl<string>;
|
||||||
|
rotateUserKey?: FormControl<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
selector: "auth-input-password",
|
selector: "auth-input-password",
|
||||||
@@ -80,9 +101,10 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||||
|
|
||||||
@Input({ required: true }) inputPasswordFlow!: InputPasswordFlow;
|
@Input({ required: true }) flow!: InputPasswordFlow;
|
||||||
@Input({ required: true }) email!: string;
|
@Input({ required: true, transform: (val: string) => val.trim().toLowerCase() }) email!: string;
|
||||||
|
|
||||||
|
@Input() userId?: UserId;
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||||
|
|
||||||
@@ -93,6 +115,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
protected secondaryButtonTextStr: string = "";
|
protected secondaryButtonTextStr: string = "";
|
||||||
|
|
||||||
protected InputPasswordFlow = InputPasswordFlow;
|
protected InputPasswordFlow = InputPasswordFlow;
|
||||||
|
private kdfConfig: KdfConfig | null = null;
|
||||||
private minHintLength = 0;
|
private minHintLength = 0;
|
||||||
protected maxHintLength = 50;
|
protected maxHintLength = 50;
|
||||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||||
@@ -101,64 +124,93 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
protected showErrorSummary = false;
|
protected showErrorSummary = false;
|
||||||
protected showPassword = false;
|
protected showPassword = false;
|
||||||
|
|
||||||
protected formGroup = this.formBuilder.nonNullable.group(
|
protected formGroup = this.formBuilder.nonNullable.group<InputPasswordForm>(
|
||||||
{
|
{
|
||||||
newPassword: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
newPassword: this.formBuilder.nonNullable.control("", [
|
||||||
confirmNewPassword: ["", Validators.required],
|
Validators.required,
|
||||||
hint: [
|
Validators.minLength(this.minPasswordLength),
|
||||||
"", // must be string (not null) because we check length in validation
|
]),
|
||||||
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
newPasswordConfirm: this.formBuilder.nonNullable.control("", Validators.required),
|
||||||
],
|
newPasswordHint: this.formBuilder.nonNullable.control("", [
|
||||||
checkForBreaches: [true],
|
Validators.minLength(this.minHintLength),
|
||||||
|
Validators.maxLength(this.maxHintLength),
|
||||||
|
]),
|
||||||
|
checkForBreaches: this.formBuilder.nonNullable.control(true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
validators: [
|
validators: [
|
||||||
compareInputs(
|
compareInputs(
|
||||||
ValidationGoal.InputsShouldMatch,
|
ValidationGoal.InputsShouldMatch,
|
||||||
"newPassword",
|
"newPassword",
|
||||||
"confirmNewPassword",
|
"newPasswordConfirm",
|
||||||
this.i18nService.t("masterPassDoesntMatch"),
|
this.i18nService.t("masterPassDoesntMatch"),
|
||||||
),
|
),
|
||||||
compareInputs(
|
compareInputs(
|
||||||
ValidationGoal.InputsShouldNotMatch,
|
ValidationGoal.InputsShouldNotMatch,
|
||||||
"newPassword",
|
"newPassword",
|
||||||
"hint",
|
"newPasswordHint",
|
||||||
this.i18nService.t("hintEqualsPassword"),
|
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(
|
constructor(
|
||||||
private auditService: AuditService,
|
private auditService: AuditService,
|
||||||
private keyService: KeyService,
|
private cipherService: CipherService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
private kdfConfigService: KdfConfigService,
|
||||||
|
private keyService: KeyService,
|
||||||
|
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.addFormFieldsIfNecessary();
|
||||||
|
this.setButtonText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private addFormFieldsIfNecessary() {
|
||||||
if (
|
if (
|
||||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
this.flow === InputPasswordFlow.ChangePassword ||
|
||||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||||
) {
|
) {
|
||||||
// https://github.com/angular/angular/issues/48794
|
this.formGroup.addControl(
|
||||||
(this.formGroup as FormGroup<any>).addControl(
|
|
||||||
"currentPassword",
|
"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) {
|
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||||
// https://github.com/angular/angular/issues/48794
|
this.formGroup.addControl("rotateUserKey", this.formBuilder.nonNullable.control(false));
|
||||||
(this.formGroup as FormGroup<any>).addControl(
|
|
||||||
"rotateUserKey",
|
|
||||||
this.formBuilder.control(false),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setButtonText() {
|
||||||
if (this.primaryButtonText) {
|
if (this.primaryButtonText) {
|
||||||
this.primaryButtonTextStr = this.i18nService.t(
|
this.primaryButtonTextStr = this.i18nService.t(
|
||||||
this.primaryButtonText.key,
|
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 () => {
|
protected submit = async () => {
|
||||||
|
this.verifyFlowAndUserId();
|
||||||
|
|
||||||
this.formGroup.markAllAsTouched();
|
this.formGroup.markAllAsTouched();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
if (this.formGroup.invalid) {
|
||||||
@@ -197,79 +236,204 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPassword = this.formGroup.controls.newPassword.value;
|
if (!this.email) {
|
||||||
|
|
||||||
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) {
|
|
||||||
throw new Error("Email is required to create master key.");
|
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,
|
newPassword,
|
||||||
this.email.trim().toLowerCase(),
|
this.passwordStrengthScore,
|
||||||
kdfConfig,
|
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,
|
newPassword,
|
||||||
masterKey,
|
newMasterKey,
|
||||||
HashPurpose.ServerAuthorization,
|
HashPurpose.ServerAuthorization,
|
||||||
);
|
);
|
||||||
|
|
||||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
const newLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||||
newPassword,
|
newPassword,
|
||||||
masterKey,
|
newMasterKey,
|
||||||
HashPurpose.LocalAuthorization,
|
HashPurpose.LocalAuthorization,
|
||||||
);
|
);
|
||||||
|
|
||||||
const passwordInputResult: PasswordInputResult = {
|
const passwordInputResult: PasswordInputResult = {
|
||||||
newPassword,
|
newPassword,
|
||||||
hint: this.formGroup.controls.hint.value,
|
newMasterKey,
|
||||||
kdfConfig,
|
newServerMasterKeyHash,
|
||||||
masterKey,
|
newLocalMasterKeyHash,
|
||||||
serverMasterKeyHash,
|
newPasswordHint,
|
||||||
localMasterKeyHash,
|
kdfConfig: this.kdfConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
this.flow === InputPasswordFlow.ChangePassword ||
|
||||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
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) {
|
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||||
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
|
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Emit cryptographic keys and other password related properties
|
||||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
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,
|
newPassword: string,
|
||||||
passwordStrengthScore: PasswordStrengthScore,
|
passwordStrengthScore: PasswordStrengthScore,
|
||||||
checkForBreaches: boolean,
|
checkForBreaches: boolean,
|
||||||
) {
|
): Promise<boolean> {
|
||||||
// Check if the password is breached, weak, or both
|
// Check if the password is breached, weak, or both
|
||||||
const passwordIsBreached =
|
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({
|
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "weakAndExposedMasterPassword" },
|
title: { key: "weakAndExposedMasterPassword" },
|
||||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||||
@@ -279,7 +443,7 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
if (!userAcceptedDialog) {
|
if (!userAcceptedDialog) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (passwordWeak) {
|
} else if (passwordIsWeak) {
|
||||||
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||||
title: { key: "weakMasterPasswordDesc" },
|
title: { key: "weakMasterPasswordDesc" },
|
||||||
content: { key: "weakMasterPasswordDesc" },
|
content: { key: "weakMasterPasswordDesc" },
|
||||||
@@ -321,4 +485,67 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
|
|
||||||
return true;
|
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
|
# InputPassword Component
|
||||||
|
|
||||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On
|
The `InputPasswordComponent` allows a user to enter master password related credentials. On form
|
||||||
submission it creates a master key, master key hash, and emits those values to the parent (along
|
submission, the component creates cryptographic properties (`newMasterKey`,
|
||||||
with the other values found in `PasswordInputResult`).
|
`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
|
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
|
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 />
|
<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
|
## `@Input()`'s
|
||||||
|
|
||||||
**Required**
|
**Required**
|
||||||
|
|
||||||
- `inputPasswordFlow` - the parent component must provide the correct flow, which is used to
|
- `flow` - the parent component must provide an `InputPasswordFlow`, which is used to determine
|
||||||
determine which form input elements will be displayed in the UI.
|
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
|
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
||||||
create a master key.
|
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
|
- `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.
|
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.
|
- `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
|
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
||||||
opposed to full-width)
|
opposed to full-width)
|
||||||
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
||||||
- `secondaryButtonText` - 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
|
## `@Output()`'s
|
||||||
|
|
||||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||||
@@ -45,25 +63,31 @@ the parent component to act on those values as needed.
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Form Input Fields
|
## The `InputPasswordFlow`
|
||||||
|
|
||||||
The `InputPasswordComponent` can handle up to 6 different form input fields, depending on the
|
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
||||||
`InputPasswordFlow` provided by the parent component.
|
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: New password
|
||||||
- Input: Confirm new password
|
- Input: Confirm new password
|
||||||
- Input: Hint
|
- Input: Hint
|
||||||
- Checkbox: Check for breaches
|
- Checkbox: Check for breaches
|
||||||
|
|
||||||
**InputPasswordFlow.ChangePassword**
|
**`InputPasswordFlow.ChangePassword`**
|
||||||
|
|
||||||
Includes everything above, plus:
|
Includes everything above, plus:
|
||||||
|
|
||||||
- Input: Current password (as the first element in the UI)
|
- Input: Current password (as the first element in the UI)
|
||||||
|
|
||||||
**InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation**
|
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||||
|
|
||||||
Includes everything above, plus:
|
Includes everything above, plus:
|
||||||
|
|
||||||
@@ -71,49 +95,122 @@ Includes everything above, plus:
|
|||||||
|
|
||||||
<br />
|
<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
|
||||||
|
|
||||||
Validation ensures that:
|
Form validators ensure that:
|
||||||
|
|
||||||
- The current password and new password are NOT the same
|
- The current password and new password are NOT the same
|
||||||
- The new password and confirmed new password are the same
|
- The new password and confirmed new password are the same
|
||||||
- The new password and password hint are NOT 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 />
|
<br />
|
||||||
|
|
||||||
## On Submit
|
## Submit Logic
|
||||||
|
|
||||||
When the form is submitted, the `InputPasswordComponent` does the following in order:
|
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
|
1. Verifies inputs:
|
||||||
dialog if their entered password is found in a breach. The popup will give them the option to
|
- Checks that the current password is correct (if it was required in the flow)
|
||||||
continue with the password or to back out and choose a different password.
|
- Checks if the new password is found in a breach and warns the user if so (if the user selected
|
||||||
2. If there is a master password policy being enforced by an org, it will check to make sure the
|
the checkbox)
|
||||||
entered master password meets the policy requirements.
|
- Checks that the new password meets any master password policy requirements enforced by an org
|
||||||
3. The component will use the password, email, and default kdfConfig to create a master key and
|
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
||||||
master key hash.
|
`newServerMasterKeyHash`, etc.)
|
||||||
4. The component will emit the following values (defined in the `PasswordInputResult` interface) to
|
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
||||||
be used by the parent component as needed:
|
`PasswordInputResult`) to be used by the parent as needed.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export interface PasswordInputResult {
|
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;
|
newPassword: string;
|
||||||
hint: string;
|
newPasswordHint: string;
|
||||||
kdfConfig: PBKDF2KdfConfig;
|
newMasterKey: MasterKey;
|
||||||
masterKey: MasterKey;
|
newServerMasterKeyHash: string;
|
||||||
serverMasterKeyHash: string;
|
newLocalMasterKeyHash: string;
|
||||||
localMasterKeyHash: string;
|
|
||||||
currentPassword?: string; // included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
kdfConfig: KdfConfig;
|
||||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Example - InputPasswordFlow.SetInitialPassword
|
# Example
|
||||||
|
|
||||||
<Story of={stories.SetInitialPassword} />
|
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||||
|
|
||||||
<br />
|
<Story of={stories.ChangePasswordWithOptionalUserKeyRotation} />
|
||||||
|
|
||||||
# Example - With Policy Requrements
|
|
||||||
|
|
||||||
<Story of={stories.WithPolicies} />
|
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ import { importProvidersFrom } from "@angular/core";
|
|||||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { action } from "@storybook/addon-actions";
|
import { action } from "@storybook/addon-actions";
|
||||||
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
|
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
|
||||||
|
import { of } from "rxjs";
|
||||||
import { ZXCVBNResult } from "zxcvbn";
|
import { ZXCVBNResult } from "zxcvbn";
|
||||||
|
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
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 { 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 { 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 { 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 `/apps` import from `/libs`
|
||||||
// FIXME: remove `src` and fix import
|
// FIXME: remove `src` and fix import
|
||||||
@@ -26,12 +32,47 @@ export default {
|
|||||||
providers: [
|
providers: [
|
||||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||||
importProvidersFrom(BrowserAnimationsModule),
|
importProvidersFrom(BrowserAnimationsModule),
|
||||||
|
{
|
||||||
|
provide: AccountService,
|
||||||
|
useValue: {
|
||||||
|
activeAccount$: of({
|
||||||
|
id: "1" as UserId,
|
||||||
|
name: "User",
|
||||||
|
email: "user@email.com",
|
||||||
|
emailVerified: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: AuditService,
|
provide: AuditService,
|
||||||
useValue: {
|
useValue: {
|
||||||
passwordLeaked: () => Promise.resolve(1),
|
passwordLeaked: () => Promise.resolve(1),
|
||||||
} as Partial<AuditService>,
|
} 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,
|
provide: KeyService,
|
||||||
useValue: {
|
useValue: {
|
||||||
@@ -87,11 +128,14 @@ export default {
|
|||||||
],
|
],
|
||||||
args: {
|
args: {
|
||||||
InputPasswordFlow: {
|
InputPasswordFlow: {
|
||||||
SetInitialPassword: InputPasswordFlow.SetInitialPassword,
|
AccountRegistration: InputPasswordFlow.AccountRegistration,
|
||||||
|
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
||||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||||
ChangePasswordWithOptionalUserKeyRotation:
|
ChangePasswordWithOptionalUserKeyRotation:
|
||||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||||
},
|
},
|
||||||
|
userId: "1" as UserId,
|
||||||
|
email: "user@email.com",
|
||||||
masterPasswordPolicyOptions: {
|
masterPasswordPolicyOptions: {
|
||||||
minComplexity: 4,
|
minComplexity: 4,
|
||||||
minLength: 14,
|
minLength: 14,
|
||||||
@@ -108,11 +152,27 @@ export default {
|
|||||||
|
|
||||||
type Story = StoryObj<InputPasswordComponent>;
|
type Story = StoryObj<InputPasswordComponent>;
|
||||||
|
|
||||||
export const SetInitialPassword: Story = {
|
export const AccountRegistration: Story = {
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
template: `
|
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) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
template: `
|
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,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
[flow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||||
|
[email]="email"
|
||||||
|
[userId]="userId"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
`,
|
`,
|
||||||
}),
|
}),
|
||||||
@@ -142,7 +208,9 @@ export const WithPolicies: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||||
|
[email]="email"
|
||||||
|
[userId]="userId"
|
||||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
`,
|
`,
|
||||||
@@ -154,7 +222,8 @@ export const SecondaryButton: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="InputPasswordFlow.AccountRegistration"
|
||||||
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'cancel' }"
|
[secondaryButtonText]="{ key: 'cancel' }"
|
||||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
@@ -167,7 +236,8 @@ export const SecondaryButtonWithPlaceHolderText: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="InputPasswordFlow.AccountRegistration"
|
||||||
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
@@ -180,7 +250,8 @@ export const InlineButton: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="InputPasswordFlow.AccountRegistration"
|
||||||
|
[email]="email"
|
||||||
[inlineButtons]="true"
|
[inlineButtons]="true"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
`,
|
`,
|
||||||
@@ -192,7 +263,8 @@ export const InlineButtons: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="InputPasswordFlow.AccountRegistration"
|
||||||
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'cancel' }"
|
[secondaryButtonText]="{ key: 'cancel' }"
|
||||||
[inlineButtons]="true"
|
[inlineButtons]="true"
|
||||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
import { KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
export interface PasswordInputResult {
|
export interface PasswordInputResult {
|
||||||
newPassword: string;
|
|
||||||
hint: string;
|
|
||||||
kdfConfig: PBKDF2KdfConfig;
|
|
||||||
masterKey: MasterKey;
|
|
||||||
serverMasterKeyHash: string;
|
|
||||||
localMasterKeyHash: string;
|
|
||||||
currentPassword?: string;
|
currentPassword?: string;
|
||||||
|
currentMasterKey?: MasterKey;
|
||||||
|
currentServerMasterKeyHash?: string;
|
||||||
|
currentLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
|
newPassword: string;
|
||||||
|
newPasswordHint: string;
|
||||||
|
newMasterKey: MasterKey;
|
||||||
|
newServerMasterKeyHash: string;
|
||||||
|
newLocalMasterKeyHash: string;
|
||||||
|
|
||||||
|
kdfConfig: KdfConfig;
|
||||||
rotateUserKey?: boolean;
|
rotateUserKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ describe("DefaultRegistrationFinishService", () => {
|
|||||||
emailVerificationToken = "emailVerificationToken";
|
emailVerificationToken = "emailVerificationToken";
|
||||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||||
passwordInputResult = {
|
passwordInputResult = {
|
||||||
masterKey: masterKey,
|
newMasterKey: masterKey,
|
||||||
serverMasterKeyHash: "serverMasterKeyHash",
|
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||||
localMasterKeyHash: "localMasterKeyHash",
|
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||||
hint: "hint",
|
newPasswordHint: "newPasswordHint",
|
||||||
newPassword: "password",
|
newPassword: "newPassword",
|
||||||
};
|
};
|
||||||
|
|
||||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||||
@@ -93,8 +93,8 @@ describe("DefaultRegistrationFinishService", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email,
|
email,
|
||||||
emailVerificationToken: emailVerificationToken,
|
emailVerificationToken: emailVerificationToken,
|
||||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
masterPasswordHint: passwordInputResult.hint,
|
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
userSymmetricKey: userKeyEncString.encryptedString,
|
userSymmetricKey: userKeyEncString.encryptedString,
|
||||||
userAsymmetricKeys: {
|
userAsymmetricKeys: {
|
||||||
publicKey: userKeyPair[0],
|
publicKey: userKeyPair[0],
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
|||||||
providerUserId?: string,
|
providerUserId?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(
|
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(
|
||||||
passwordInputResult.masterKey,
|
passwordInputResult.newMasterKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newUserKey || !newEncUserKey) {
|
if (!newUserKey || !newEncUserKey) {
|
||||||
@@ -79,8 +79,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
|||||||
|
|
||||||
const registerFinishRequest = new RegisterFinishRequest(
|
const registerFinishRequest = new RegisterFinishRequest(
|
||||||
email,
|
email,
|
||||||
passwordInputResult.serverMasterKeyHash,
|
passwordInputResult.newServerMasterKeyHash,
|
||||||
passwordInputResult.hint,
|
passwordInputResult.newPasswordHint,
|
||||||
encryptedUserKey,
|
encryptedUserKey,
|
||||||
userAsymmetricKeysRequest,
|
userAsymmetricKeysRequest,
|
||||||
passwordInputResult.kdfConfig.kdfType,
|
passwordInputResult.kdfConfig.kdfType,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<auth-input-password
|
<auth-input-password
|
||||||
*ngIf="!loading"
|
*ngIf="!loading"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="inputPasswordFlow"
|
||||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||||
[loading]="submitting"
|
[loading]="submitting"
|
||||||
[primaryButtonText]="{ key: 'createAccount' }"
|
[primaryButtonText]="{ key: 'createAccount' }"
|
||||||
|
|||||||
@@ -39,8 +39,7 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
|||||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
InputPasswordFlow = InputPasswordFlow;
|
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
submitting = false;
|
submitting = false;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -111,12 +111,12 @@ describe("DefaultSetPasswordJitService", () => {
|
|||||||
userId = "userId" as UserId;
|
userId = "userId" as UserId;
|
||||||
|
|
||||||
passwordInputResult = {
|
passwordInputResult = {
|
||||||
masterKey: masterKey,
|
newMasterKey: masterKey,
|
||||||
serverMasterKeyHash: "serverMasterKeyHash",
|
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||||
localMasterKeyHash: "localMasterKeyHash",
|
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||||
hint: "hint",
|
newPasswordHint: "newPasswordHint",
|
||||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||||
newPassword: "password",
|
newPassword: "newPassword",
|
||||||
};
|
};
|
||||||
|
|
||||||
credentials = {
|
credentials = {
|
||||||
@@ -131,9 +131,9 @@ describe("DefaultSetPasswordJitService", () => {
|
|||||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||||
|
|
||||||
setPasswordRequest = new SetPasswordRequest(
|
setPasswordRequest = new SetPasswordRequest(
|
||||||
passwordInputResult.serverMasterKeyHash,
|
passwordInputResult.newServerMasterKeyHash,
|
||||||
protectedUserKey[1].encryptedString,
|
protectedUserKey[1].encryptedString,
|
||||||
passwordInputResult.hint,
|
passwordInputResult.newPasswordHint,
|
||||||
orgSsoIdentifier,
|
orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
passwordInputResult.kdfConfig.kdfType,
|
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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
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 {
|
import {
|
||||||
SetPasswordCredentials,
|
SetPasswordCredentials,
|
||||||
@@ -43,10 +43,10 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
|||||||
|
|
||||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||||
const {
|
const {
|
||||||
masterKey,
|
newMasterKey,
|
||||||
serverMasterKeyHash,
|
newServerMasterKeyHash,
|
||||||
localMasterKeyHash,
|
newLocalMasterKeyHash,
|
||||||
hint,
|
newPasswordHint,
|
||||||
kdfConfig,
|
kdfConfig,
|
||||||
orgSsoIdentifier,
|
orgSsoIdentifier,
|
||||||
orgId,
|
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) {
|
if (protectedUserKey == null) {
|
||||||
throw new Error("protectedUserKey not found. Could not set password.");
|
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 [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||||
|
|
||||||
const request = new SetPasswordRequest(
|
const request = new SetPasswordRequest(
|
||||||
serverMasterKeyHash,
|
newServerMasterKeyHash,
|
||||||
protectedUserKey[1].encryptedString,
|
protectedUserKey[1].encryptedString,
|
||||||
hint,
|
newPasswordHint,
|
||||||
orgSsoIdentifier,
|
orgSsoIdentifier,
|
||||||
keysRequest,
|
keysRequest,
|
||||||
kdfConfig.kdfType, // kdfConfig is always DEFAULT_KDF_CONFIG (see InputPasswordComponent)
|
kdfConfig.kdfType,
|
||||||
kdfConfig.iterations,
|
kdfConfig.iterations,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -85,14 +85,14 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
|||||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||||
|
|
||||||
// User now has a password so update account decryption options in state
|
// 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.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||||
|
|
||||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||||
|
|
||||||
if (resetPasswordAutoEnroll) {
|
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(
|
private async updateAccountDecryptionProperties(
|
||||||
masterKey: MasterKey,
|
masterKey: MasterKey,
|
||||||
kdfConfig: PBKDF2KdfConfig,
|
kdfConfig: KdfConfig,
|
||||||
protectedUserKey: [UserKey, EncString],
|
protectedUserKey: [UserKey, EncString],
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
</app-callout>
|
</app-callout>
|
||||||
|
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
[flow]="inputPasswordFlow"
|
||||||
[primaryButtonText]="{ key: 'createAccount' }"
|
|
||||||
[email]="email"
|
[email]="email"
|
||||||
|
[userId]="userId"
|
||||||
[loading]="submitting"
|
[loading]="submitting"
|
||||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||||
|
[primaryButtonText]="{ key: 'createAccount' }"
|
||||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom, map } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||||
})
|
})
|
||||||
export class SetPasswordJitComponent implements OnInit {
|
export class SetPasswordJitComponent implements OnInit {
|
||||||
protected InputPasswordFlow = InputPasswordFlow;
|
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
|
||||||
protected email: string;
|
protected email: string;
|
||||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||||
protected orgId: string;
|
protected orgId: string;
|
||||||
@@ -60,9 +60,9 @@ export class SetPasswordJitComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.email = await firstValueFrom(
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
this.userId = activeAccount?.id;
|
||||||
);
|
this.email = activeAccount?.email;
|
||||||
|
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
this.syncLoading = false;
|
this.syncLoading = false;
|
||||||
@@ -97,14 +97,12 @@ export class SetPasswordJitComponent implements OnInit {
|
|||||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
|
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
|
||||||
|
|
||||||
const credentials: SetPasswordCredentials = {
|
const credentials: SetPasswordCredentials = {
|
||||||
...passwordInputResult,
|
...passwordInputResult,
|
||||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||||
orgId: this.orgId,
|
orgId: this.orgId,
|
||||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||||
userId,
|
userId: this.userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
import { KdfConfig } from "@bitwarden/key-management";
|
||||||
|
|
||||||
export interface SetPasswordCredentials {
|
export interface SetPasswordCredentials {
|
||||||
masterKey: MasterKey;
|
newMasterKey: MasterKey;
|
||||||
serverMasterKeyHash: string;
|
newServerMasterKeyHash: string;
|
||||||
localMasterKeyHash: string;
|
newLocalMasterKeyHash: string;
|
||||||
kdfConfig: PBKDF2KdfConfig;
|
newPasswordHint: string;
|
||||||
hint: string;
|
kdfConfig: KdfConfig;
|
||||||
orgSsoIdentifier: string;
|
orgSsoIdentifier: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resetPasswordAutoEnroll: boolean;
|
resetPasswordAutoEnroll: boolean;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export enum FeatureFlag {
|
|||||||
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
|
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||||
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
|
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
|
||||||
|
|
||||||
/* Autofill */
|
/* Autofill */
|
||||||
@@ -115,6 +116,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||||
|
|
||||||
/* Auth */
|
/* Auth */
|
||||||
|
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
|
||||||
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
||||||
|
|
||||||
/* Billing */
|
/* Billing */
|
||||||
|
|||||||
Reference in New Issue
Block a user