mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +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-decryption-options";
|
||||
export * from "./webauthn-login";
|
||||
|
||||
@@ -185,11 +185,11 @@ describe("WebRegistrationFinishService", () => {
|
||||
emailVerificationToken = "emailVerificationToken";
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
@@ -231,8 +231,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
@@ -267,8 +267,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
@@ -308,8 +308,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
@@ -351,8 +351,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
@@ -396,8 +396,8 @@ describe("WebRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: undefined,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
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";
|
||||
|
||||
/**
|
||||
* @deprecated use the auth `PasswordSettingsComponent` instead
|
||||
*/
|
||||
@Component({
|
||||
selector: "app-change-password",
|
||||
templateUrl: "change-password.component.html",
|
||||
@@ -132,7 +135,7 @@ export class ChangePasswordComponent
|
||||
content:
|
||||
this.i18nService.t("updateEncryptionKeyWarning") +
|
||||
" " +
|
||||
this.i18nService.t("updateEncryptionKeyExportWarning") +
|
||||
this.i18nService.t("updateEncryptionKeyAccountExportWarning") +
|
||||
" " +
|
||||
this.i18nService.t("rotateEncKeyConfirmation"),
|
||||
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 { 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 { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component";
|
||||
|
||||
import { DeviceManagementComponent } from "./device-management.component";
|
||||
import { PasswordSettingsComponent } from "./password-settings/password-settings.component";
|
||||
import { SecurityKeysComponent } from "./security-keys.component";
|
||||
import { SecurityComponent } from "./security.component";
|
||||
|
||||
@@ -14,10 +18,31 @@ const routes: Routes = [
|
||||
component: SecurityComponent,
|
||||
data: { titleId: "security" },
|
||||
children: [
|
||||
{ path: "", pathMatch: "full", redirectTo: "change-password" },
|
||||
{ path: "", pathMatch: "full", redirectTo: "password" },
|
||||
{
|
||||
path: "change-password",
|
||||
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" },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<app-header>
|
||||
<bit-tab-nav-bar slot="tabs">
|
||||
<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>
|
||||
<bit-tab-link route="two-factor">{{ "twoStepLogin" | 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 { 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";
|
||||
|
||||
@Component({
|
||||
@@ -10,6 +11,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
})
|
||||
export class SecurityComponent implements OnInit {
|
||||
showChangePassword = true;
|
||||
changePasswordRoute = "change-password";
|
||||
|
||||
constructor(
|
||||
private userVerificationService: UserVerificationService,
|
||||
@@ -18,5 +20,12 @@ export class SecurityComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
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">
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</div>
|
||||
<div *ngIf="useTrialStepper">
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[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
|
||||
? 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
|
||||
@if (initializing) {
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin bwi-3x tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div *ngIf="!useTrialStepper">
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</div>
|
||||
<div *ngIf="useTrialStepper">
|
||||
<app-vertical-stepper #stepper linear (selectionChange)="verticalStepChange($event)">
|
||||
<app-vertical-step label="Create Account" [editable]="false" [subLabel]="email">
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[masterPasswordPolicyOptions]="enforcedPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordSubmit($event)"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
></auth-input-password>
|
||||
</app-vertical-step>
|
||||
<app-vertical-step label="Organization Information" [subLabel]="orgInfoSubLabel">
|
||||
<app-org-info [nameOnly]="true" [formGroup]="orgInfoFormGroup"></app-org-info>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
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
|
||||
? ['/sm', orgId]
|
||||
: ['/organizations', orgId, 'vault']
|
||||
? SubscriptionProduct.SecretsManager
|
||||
: SubscriptionProduct.PasswordManager
|
||||
"
|
||||
[trialLength]="trialLength"
|
||||
(steppedBack)="previousStep()"
|
||||
(organizationCreated)="createdOrganization($event)"
|
||||
>
|
||||
{{ "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>
|
||||
</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"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[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 {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
InputPasswordFlow = InputPasswordFlow;
|
||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||
initializing = true;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
product: ProductType;
|
||||
@@ -203,6 +204,8 @@ export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
.subscribe(() => {
|
||||
this.orgInfoFormGroup.controls.name.markAsTouched();
|
||||
});
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
LoginDecryptionOptionsService,
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
ChangePasswordService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
@@ -110,6 +111,7 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde
|
||||
import { flagEnabled } from "../../utils/flags";
|
||||
import { PolicyListService } from "../admin-console/core/policy-list.service";
|
||||
import {
|
||||
WebChangePasswordService,
|
||||
WebSetPasswordJitService,
|
||||
WebRegistrationFinishService,
|
||||
WebLoginComponentService,
|
||||
@@ -123,6 +125,7 @@ import { AcceptOrganizationInviteService } from "../auth/organization-invite/acc
|
||||
import { HtmlStorageService } from "../core/html-storage.service";
|
||||
import { I18nService } from "../core/i18n.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 { WebProcessReloadService } from "../key-management/services/web-process-reload.service";
|
||||
import { WebBiometricsService } from "../key-management/web-biometric.service";
|
||||
@@ -373,6 +376,16 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsService, I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangePasswordService,
|
||||
useClass: WebChangePasswordService,
|
||||
deps: [
|
||||
KeyServiceAbstraction,
|
||||
MasterPasswordApiService,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
UserKeyRotationService,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -4549,8 +4549,8 @@
|
||||
"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."
|
||||
},
|
||||
"updateEncryptionKeyExportWarning": {
|
||||
"message": "Any encrypted exports that you have saved will also become invalid."
|
||||
"updateEncryptionKeyAccountExportWarning": {
|
||||
"message": "Any account restricted exports you have saved will become invalid."
|
||||
},
|
||||
"subscription": {
|
||||
"message": "Subscription"
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
TwoFactorAuthComponentService,
|
||||
TwoFactorAuthEmailComponentService,
|
||||
TwoFactorAuthWebAuthnComponentService,
|
||||
ChangePasswordService,
|
||||
DefaultChangePasswordService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
AuthRequestApiService,
|
||||
@@ -1538,6 +1540,15 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultCipherEncryptionService,
|
||||
deps: [SdkService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ChangePasswordService,
|
||||
useClass: DefaultChangePasswordService,
|
||||
deps: [
|
||||
KeyService,
|
||||
MasterPasswordApiServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@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/default-anon-layout-wrapper-data.service";
|
||||
|
||||
// change password
|
||||
export * from "./change-password/change-password.component";
|
||||
export * from "./change-password/change-password.service.abstraction";
|
||||
export * from "./change-password/default-change-password.service";
|
||||
|
||||
// fingerprint dialog
|
||||
export * from "./fingerprint-dialog/fingerprint-dialog.component";
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
<bit-form-field
|
||||
*ngIf="
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
flow === InputPasswordFlow.ChangePassword ||
|
||||
flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
"
|
||||
>
|
||||
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
||||
@@ -58,12 +58,12 @@
|
||||
</div>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
||||
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_confirm-new-password"
|
||||
id="input-password-form_new-password-confirm"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmNewPassword"
|
||||
formControlName="newPasswordConfirm"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -76,21 +76,33 @@
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="hint" />
|
||||
<input id="input-password-form_new-password-hint" bitInput formControlName="newPasswordHint" />
|
||||
<bit-hint>
|
||||
{{ "masterPassHintText" | i18n: formGroup.value.hint.length : maxHintLength.toString() }}
|
||||
{{
|
||||
"masterPassHintText"
|
||||
| i18n: formGroup.value.newPasswordHint.length : maxHintLength.toString()
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox formControlName="checkForBreaches" />
|
||||
<input
|
||||
id="input-password-form_check-for-breaches"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<bit-form-control
|
||||
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
>
|
||||
<input type="checkbox" bitCheckbox formControlName="rotateUserKey" />
|
||||
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
||||
<input
|
||||
id="input-password-form_rotate-user-key"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="rotateUserKey"
|
||||
(change)="rotateUserKeyClicked()"
|
||||
/>
|
||||
<bit-label>
|
||||
{{ "rotateAccountEncKey" | i18n }}
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from "@angular/forms";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormControl } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -9,9 +10,13 @@ import {
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
@@ -23,7 +28,12 @@ import {
|
||||
ToastService,
|
||||
Translation,
|
||||
} from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
KdfConfig,
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -34,30 +44,41 @@ import { compareInputs, ValidationGoal } from "../validators/compare-inputs.vali
|
||||
import { PasswordInputResult } from "./password-input-result";
|
||||
|
||||
/**
|
||||
* Determines which form input elements will be displayed in the UI.
|
||||
* Determines which form elements will be displayed in the UI
|
||||
* and which cryptographic keys will be created and emitted.
|
||||
*/
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* - Input: New password
|
||||
* - Input: Confirm new password
|
||||
* - Input: Hint
|
||||
* - Checkbox: Check for breaches
|
||||
* Form elements displayed:
|
||||
* - [Input] New password
|
||||
* - [Input] New password confirm
|
||||
* - [Input] New password hint
|
||||
* - [Checkbox] Check for breaches
|
||||
*/
|
||||
SetInitialPassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Input: Current password (as the first element in the UI)
|
||||
AccountRegistration, // important: this flow does not involve an activeAccount/userId
|
||||
SetInitialPasswordAuthedUser,
|
||||
/*
|
||||
* All form elements above, plus: [Input] Current password (as the first element in the UI)
|
||||
*/
|
||||
ChangePassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Checkbox: Rotate account encryption key (as the last element in the UI)
|
||||
* All form elements above, plus: [Checkbox] Rotate account encryption key (as the last element in the UI)
|
||||
*/
|
||||
ChangePasswordWithOptionalUserKeyRotation,
|
||||
}
|
||||
|
||||
interface InputPasswordForm {
|
||||
newPassword: FormControl<string>;
|
||||
newPasswordConfirm: FormControl<string>;
|
||||
newPasswordHint: FormControl<string>;
|
||||
checkForBreaches: FormControl<boolean>;
|
||||
|
||||
currentPassword?: FormControl<string>;
|
||||
rotateUserKey?: FormControl<boolean>;
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-input-password",
|
||||
@@ -80,9 +101,10 @@ export class InputPasswordComponent implements OnInit {
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||
|
||||
@Input({ required: true }) inputPasswordFlow!: InputPasswordFlow;
|
||||
@Input({ required: true }) email!: string;
|
||||
@Input({ required: true }) flow!: InputPasswordFlow;
|
||||
@Input({ required: true, transform: (val: string) => val.trim().toLowerCase() }) email!: string;
|
||||
|
||||
@Input() userId?: UserId;
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
|
||||
@@ -93,6 +115,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
protected secondaryButtonTextStr: string = "";
|
||||
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
private kdfConfig: KdfConfig | null = null;
|
||||
private minHintLength = 0;
|
||||
protected maxHintLength = 50;
|
||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||
@@ -101,64 +124,93 @@ export class InputPasswordComponent implements OnInit {
|
||||
protected showErrorSummary = false;
|
||||
protected showPassword = false;
|
||||
|
||||
protected formGroup = this.formBuilder.nonNullable.group(
|
||||
protected formGroup = this.formBuilder.nonNullable.group<InputPasswordForm>(
|
||||
{
|
||||
newPassword: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmNewPassword: ["", Validators.required],
|
||||
hint: [
|
||||
"", // must be string (not null) because we check length in validation
|
||||
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
||||
],
|
||||
checkForBreaches: [true],
|
||||
newPassword: this.formBuilder.nonNullable.control("", [
|
||||
Validators.required,
|
||||
Validators.minLength(this.minPasswordLength),
|
||||
]),
|
||||
newPasswordConfirm: this.formBuilder.nonNullable.control("", Validators.required),
|
||||
newPasswordHint: this.formBuilder.nonNullable.control("", [
|
||||
Validators.minLength(this.minHintLength),
|
||||
Validators.maxLength(this.maxHintLength),
|
||||
]),
|
||||
checkForBreaches: this.formBuilder.nonNullable.control(true),
|
||||
},
|
||||
{
|
||||
validators: [
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldMatch,
|
||||
"newPassword",
|
||||
"confirmNewPassword",
|
||||
"newPasswordConfirm",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldNotMatch,
|
||||
"newPassword",
|
||||
"hint",
|
||||
"newPasswordHint",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
protected get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
this.masterPasswordPolicyOptions.minLength > 0
|
||||
) {
|
||||
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
|
||||
} else {
|
||||
return this.i18nService.t("characterMinimum", this.minPasswordLength);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private auditService: AuditService,
|
||||
private keyService: KeyService,
|
||||
private cipherService: CipherService,
|
||||
private dialogService: DialogService,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.addFormFieldsIfNecessary();
|
||||
this.setButtonText();
|
||||
}
|
||||
|
||||
private addFormFieldsIfNecessary() {
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
this.formGroup.addControl(
|
||||
"currentPassword",
|
||||
this.formBuilder.control("", Validators.required),
|
||||
this.formBuilder.nonNullable.control("", Validators.required),
|
||||
);
|
||||
|
||||
this.formGroup.addValidators([
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldNotMatch,
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
this.i18nService.t("yourNewPasswordCannotBeTheSameAsYourCurrentPassword"),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"rotateUserKey",
|
||||
this.formBuilder.control(false),
|
||||
);
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
this.formGroup.addControl("rotateUserKey", this.formBuilder.nonNullable.control(false));
|
||||
}
|
||||
}
|
||||
|
||||
private setButtonText() {
|
||||
if (this.primaryButtonText) {
|
||||
this.primaryButtonTextStr = this.i18nService.t(
|
||||
this.primaryButtonText.key,
|
||||
@@ -174,22 +226,9 @@ export class InputPasswordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
this.masterPasswordPolicyOptions.minLength > 0
|
||||
) {
|
||||
return this.i18nService.t("characterMinimum", this.masterPasswordPolicyOptions.minLength);
|
||||
} else {
|
||||
return this.i18nService.t("characterMinimum", this.minPasswordLength);
|
||||
}
|
||||
}
|
||||
|
||||
getPasswordStrengthScore(score: PasswordStrengthScore) {
|
||||
this.passwordStrengthScore = score;
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
this.verifyFlowAndUserId();
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
@@ -197,79 +236,204 @@ export class InputPasswordComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
|
||||
const passwordEvaluatedSuccessfully = await this.evaluateNewPassword(
|
||||
newPassword,
|
||||
this.passwordStrengthScore,
|
||||
this.formGroup.controls.checkForBreaches.value,
|
||||
);
|
||||
|
||||
if (!passwordEvaluatedSuccessfully) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and hash new master key
|
||||
const kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
|
||||
if (this.email == null) {
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to create master key.");
|
||||
}
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
const currentPassword = this.formGroup.controls.currentPassword?.value ?? "";
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
const newPasswordHint = this.formGroup.controls.newPasswordHint.value;
|
||||
const checkForBreaches = this.formGroup.controls.checkForBreaches.value;
|
||||
|
||||
// 1. Determine kdfConfig
|
||||
if (this.flow === InputPasswordFlow.AccountRegistration) {
|
||||
this.kdfConfig = DEFAULT_KDF_CONFIG;
|
||||
} else {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
this.kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(this.userId));
|
||||
}
|
||||
|
||||
if (this.kdfConfig == null) {
|
||||
throw new Error("KdfConfig is required to create master key.");
|
||||
}
|
||||
|
||||
// 2. Verify current password is correct (if necessary)
|
||||
if (
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
const currentPasswordVerified = await this.verifyCurrentPassword(
|
||||
currentPassword,
|
||||
this.kdfConfig,
|
||||
);
|
||||
if (!currentPasswordVerified) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Verify new password
|
||||
const newPasswordVerified = await this.verifyNewPassword(
|
||||
newPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdfConfig,
|
||||
this.passwordStrengthScore,
|
||||
checkForBreaches,
|
||||
);
|
||||
if (!newPasswordVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Create cryptographic keys and build a PasswordInputResult object
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
newPassword,
|
||||
this.email,
|
||||
this.kdfConfig,
|
||||
);
|
||||
|
||||
const serverMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
const newServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
const newLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
newMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
kdfConfig,
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig: this.kdfConfig,
|
||||
};
|
||||
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
passwordInputResult.currentPassword = this.formGroup.get("currentPassword")?.value;
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentPassword,
|
||||
this.email,
|
||||
this.kdfConfig,
|
||||
);
|
||||
|
||||
const currentServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
currentPassword,
|
||||
currentMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const currentLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
currentPassword,
|
||||
currentMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
passwordInputResult.currentPassword = currentPassword;
|
||||
passwordInputResult.currentMasterKey = currentMasterKey;
|
||||
passwordInputResult.currentServerMasterKeyHash = currentServerMasterKeyHash;
|
||||
passwordInputResult.currentLocalMasterKeyHash = currentLocalMasterKeyHash;
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||
}
|
||||
|
||||
// 5. Emit cryptographic keys and other password related properties
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
};
|
||||
|
||||
// Returns true if the password passes all checks, false otherwise
|
||||
private async evaluateNewPassword(
|
||||
/**
|
||||
* This method prevents a dev from passing down the wrong `InputPasswordFlow`
|
||||
* from the parent component or from failing to pass down a `userId` for flows
|
||||
* that require it.
|
||||
*
|
||||
* We cannot mark the `userId` `@Input` as required because in an account registration
|
||||
* flow we will not have an active account `userId` to pass down.
|
||||
*/
|
||||
private verifyFlowAndUserId() {
|
||||
/**
|
||||
* There can be no active account (and thus no userId) in an account registration
|
||||
* flow. If there is a userId, it means the dev passed down the wrong InputPasswordFlow
|
||||
* from the parent component.
|
||||
*/
|
||||
if (this.flow === InputPasswordFlow.AccountRegistration) {
|
||||
if (this.userId) {
|
||||
throw new Error(
|
||||
"There can be no userId in an account registration flow. Please pass down the appropriate InputPasswordFlow from the parent component.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* There MUST be an active account (and thus a userId) in all other flows.
|
||||
* If no userId is passed down, it means the dev either:
|
||||
* (a) passed down the wrong InputPasswordFlow, or
|
||||
* (b) passed down the correct InputPasswordFlow but failed to pass down a userId
|
||||
*/
|
||||
if (this.flow !== InputPasswordFlow.AccountRegistration) {
|
||||
if (!this.userId) {
|
||||
throw new Error("The selected InputPasswordFlow requires that a userId be passed down");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the current password is correct (it can be used to successfully decrypt
|
||||
* the masterKeyEncrypedUserKey), `false` otherwise
|
||||
*/
|
||||
private async verifyCurrentPassword(
|
||||
currentPassword: string,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<boolean> {
|
||||
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||
currentPassword,
|
||||
this.email,
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
currentMasterKey,
|
||||
this.userId,
|
||||
);
|
||||
|
||||
if (decryptedUserKey == null) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: "",
|
||||
message: this.i18nService.t("invalidMasterPassword"),
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the new password is not weak or breached and it passes
|
||||
* any enforced org policy options, `false` otherwise
|
||||
*/
|
||||
private async verifyNewPassword(
|
||||
newPassword: string,
|
||||
passwordStrengthScore: PasswordStrengthScore,
|
||||
checkForBreaches: boolean,
|
||||
) {
|
||||
): Promise<boolean> {
|
||||
// Check if the password is breached, weak, or both
|
||||
const passwordIsBreached =
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword));
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword)) > 0;
|
||||
|
||||
const passwordWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
const passwordIsWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
|
||||
if (passwordIsBreached && passwordWeak) {
|
||||
if (passwordIsBreached && passwordIsWeak) {
|
||||
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakAndExposedMasterPassword" },
|
||||
content: { key: "weakAndBreachedMasterPasswordDesc" },
|
||||
@@ -279,7 +443,7 @@ export class InputPasswordComponent implements OnInit {
|
||||
if (!userAcceptedDialog) {
|
||||
return false;
|
||||
}
|
||||
} else if (passwordWeak) {
|
||||
} else if (passwordIsWeak) {
|
||||
const userAcceptedDialog = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPasswordDesc" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
@@ -321,4 +485,67 @@ export class InputPasswordComponent implements OnInit {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async rotateUserKeyClicked() {
|
||||
const rotateUserKeyCtrl = this.formGroup.controls.rotateUserKey;
|
||||
|
||||
const rotateUserKey = rotateUserKeyCtrl?.value;
|
||||
|
||||
if (rotateUserKey) {
|
||||
if (!this.userId) {
|
||||
throw new Error("userId not passed down");
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted(this.userId);
|
||||
|
||||
let hasOldAttachments = false;
|
||||
|
||||
if (ciphers != null) {
|
||||
for (let i = 0; i < ciphers.length; i++) {
|
||||
if (ciphers[i].organizationId == null && ciphers[i].hasOldAttachments) {
|
||||
hasOldAttachments = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasOldAttachments) {
|
||||
const learnMore = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "warning" },
|
||||
content: { key: "oldAttachmentsNeedFixDesc" },
|
||||
acceptButtonText: { key: "learnMore" },
|
||||
cancelButtonText: { key: "close" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (learnMore) {
|
||||
this.platformUtilsService.launchUri(
|
||||
"https://bitwarden.com/help/attachments/#add-storage-space",
|
||||
);
|
||||
}
|
||||
|
||||
rotateUserKeyCtrl.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "rotateEncKeyTitle" },
|
||||
content:
|
||||
this.i18nService.t("updateEncryptionKeyWarning") +
|
||||
" " +
|
||||
this.i18nService.t("updateEncryptionKeyAccountExportWarning") +
|
||||
" " +
|
||||
this.i18nService.t("rotateEncKeyConfirmation"),
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
rotateUserKeyCtrl.setValue(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
|
||||
this.passwordStrengthScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On
|
||||
submission it creates a master key, master key hash, and emits those values to the parent (along
|
||||
with the other values found in `PasswordInputResult`).
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On form
|
||||
submission, the component creates cryptographic properties (`newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.) and emits those properties to the parent (along with the other
|
||||
values defined in `PasswordInputResult`).
|
||||
|
||||
The component is intended for re-use in different scenarios throughout the application. Therefore it
|
||||
is mostly presentational and simply emits values rather than acting on them itself. It is the job of
|
||||
@@ -16,12 +17,27 @@ the parent component to act on those values as needed.
|
||||
|
||||
<br />
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [@Inputs](#inputs)
|
||||
- [@Outputs](#outputs)
|
||||
- [The InputPasswordFlow](#the-inputpasswordflow)
|
||||
- [HTML - Form Fields](#html---form-fields)
|
||||
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
||||
- [Difference between AccountRegistration and SetInitialPasswordAuthedUser](#difference-between-accountregistration-and-setinitialpasswordautheduser)
|
||||
- [Validation](#validation)
|
||||
- [Submit Logic](#submit-logic)
|
||||
- [Example](#example)
|
||||
|
||||
<br />
|
||||
|
||||
## `@Input()`'s
|
||||
|
||||
**Required**
|
||||
|
||||
- `inputPasswordFlow` - the parent component must provide the correct flow, which is used to
|
||||
determine which form input elements will be displayed in the UI.
|
||||
- `flow` - the parent component must provide an `InputPasswordFlow`, which is used to determine
|
||||
which form input elements will be displayed in the UI and which cryptographic keys will be created
|
||||
and emitted.
|
||||
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
||||
create a master key.
|
||||
|
||||
@@ -29,13 +45,15 @@ the parent component to act on those values as needed.
|
||||
|
||||
- `loading` - a boolean used to indicate that the parent component is performing some
|
||||
long-running/async operation and that the form should be disabled until the operation is complete.
|
||||
The primary button will also show a spinner if `loading` true.
|
||||
The primary button will also show a spinner if `loading` is true.
|
||||
- `masterPasswordPolicyOptions` - used to display and enforce master password policy requirements.
|
||||
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
||||
opposed to full-width)
|
||||
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
- `secondaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
|
||||
<br />
|
||||
|
||||
## `@Output()`'s
|
||||
|
||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||
@@ -45,25 +63,31 @@ the parent component to act on those values as needed.
|
||||
|
||||
<br />
|
||||
|
||||
## Form Input Fields
|
||||
## The `InputPasswordFlow`
|
||||
|
||||
The `InputPasswordComponent` can handle up to 6 different form input fields, depending on the
|
||||
`InputPasswordFlow` provided by the parent component.
|
||||
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
||||
credential generation logic of the component.
|
||||
|
||||
**InputPasswordFlow.SetInitialPassword**
|
||||
<br />
|
||||
|
||||
### HTML - Form Fields
|
||||
|
||||
The `InputPasswordFlow` determines which form fields get displayed in the UI.
|
||||
|
||||
**`InputPasswordFlow.AccountRegistration`** and **`InputPasswordFlow.SetInitialPasswordAuthedUser`**
|
||||
|
||||
- Input: New password
|
||||
- Input: Confirm new password
|
||||
- Input: Hint
|
||||
- Checkbox: Check for breaches
|
||||
|
||||
**InputPasswordFlow.ChangePassword**
|
||||
**`InputPasswordFlow.ChangePassword`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Input: Current password (as the first element in the UI)
|
||||
|
||||
**InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation**
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
@@ -71,49 +95,122 @@ Includes everything above, plus:
|
||||
|
||||
<br />
|
||||
|
||||
### TypeScript - Credential Generation
|
||||
|
||||
- The `AccountRegistration` and `SetInitialPasswordAuthedUser` flows involve a user setting their
|
||||
password for the first time. Therefore on submit the component will only generate new credentials
|
||||
(`newMasterKey`) and not current credentials (`currentMasterKey`).
|
||||
- The `ChangePassword` and `ChangePasswordWithOptionalUserKeyRotation` flows both require the user
|
||||
to enter a current password along with a new password. Therefore on submit the component will
|
||||
generate current credentials (`currentMasterKey`) along with new credentials (`newMasterKey`).
|
||||
|
||||
<br />
|
||||
|
||||
### Difference between `AccountRegistration` and `SetInitialPasswordAuthedUser`
|
||||
|
||||
These two flows are similar in that they display the same form fields and only generate new
|
||||
credentials, but we need to keep them separate for the following reasons:
|
||||
|
||||
- `AccountRegistration` involves scenarios where we have no existing user, and **thus NO active
|
||||
account `userId`**:
|
||||
|
||||
- Standard Account Registration
|
||||
- Email Invite Account Registration
|
||||
- Trial Initiation Account Registration
|
||||
|
||||
<br />
|
||||
|
||||
- `SetInitialPasswordAuthedUser` involves scenarios where we do have an existing and authed user,
|
||||
and **thus an active account `userId`**:
|
||||
- A "just-in-time" (JIT) provisioned user joins a master password (MP) encryption org and must set
|
||||
their initial password
|
||||
- A "just-in-time" (JIT) provisioned user joins a trusted device encryption (TDE) org with a
|
||||
starting role that requires them to have/set their initial password
|
||||
- A note on JIT provisioned user flows:
|
||||
- Even though a JIT provisioned user is a brand-new user who was “just” created, we consider
|
||||
them to be an “existing authed user” _from the perspective of the set-password flow_. This
|
||||
is because at the time they set their initial password, their account already exists in the
|
||||
database (before setting their password) and they have already authenticated via SSO.
|
||||
- The same is not true in the account registration flows above—that is, during account
|
||||
registration when a user reaches the `/finish-signup` or `/trial-initiation` page to set
|
||||
their initial password, their account does not yet exist in the database, and will only be
|
||||
created once they set an initial password.
|
||||
- An existing user in a TDE org logs in after the org admin upgraded the user to a role that now
|
||||
requires them to have/set their initial password
|
||||
- An existing user logs in after their org admin offboarded the org from TDE, and the user must
|
||||
now have/set their initial password
|
||||
|
||||
The presence or absence of an active account `userId` is important because it determines how we get
|
||||
the correct `kdfConfig` prior to key generation:
|
||||
|
||||
- If there is no `userId` passed down from the parent, we default to `DEFAULT_KDF_CONFIG`
|
||||
- If there is a `userId` passed down from the parent, we get the `kdfConfig` from state using the
|
||||
`userId`
|
||||
|
||||
That said, we cannot mark the `userId` as a required via `@Input({ required: true })` because
|
||||
`AccountRegistration` flows will not have a `userId`. But we still want to require a `userId` in a
|
||||
`SetInitialPasswordAuthedUser` flow. Therefore the `InputPasswordComponent` has init logic that
|
||||
ensures the following:
|
||||
|
||||
- If the passed down flow is `AccountRegistration`, require that the parent **MUST NOT** have passed
|
||||
down a `userId`
|
||||
- If the passed down flow is `SetInitialPasswordAuthedUser` require that the parent must also have
|
||||
passed down a `userId`
|
||||
|
||||
If either of these checks is not met, the component throws to alert the dev of a mistake.
|
||||
|
||||
<br />
|
||||
|
||||
## Validation
|
||||
|
||||
Validation ensures that:
|
||||
Form validators ensure that:
|
||||
|
||||
- The current password and new password are NOT the same
|
||||
- The new password and confirmed new password are the same
|
||||
- The new password and password hint are NOT the same
|
||||
|
||||
Additional submit logic validation ensures that:
|
||||
|
||||
- The new password adheres to any enforced master password policy options (that were passed down
|
||||
from the parent)
|
||||
|
||||
<br />
|
||||
|
||||
## On Submit
|
||||
## Submit Logic
|
||||
|
||||
When the form is submitted, the `InputPasswordComponent` does the following in order:
|
||||
|
||||
1. If the user selected the checkbox to check for password breaches, they will recieve a popup
|
||||
dialog if their entered password is found in a breach. The popup will give them the option to
|
||||
continue with the password or to back out and choose a different password.
|
||||
2. If there is a master password policy being enforced by an org, it will check to make sure the
|
||||
entered master password meets the policy requirements.
|
||||
3. The component will use the password, email, and default kdfConfig to create a master key and
|
||||
master key hash.
|
||||
4. The component will emit the following values (defined in the `PasswordInputResult` interface) to
|
||||
be used by the parent component as needed:
|
||||
1. Verifies inputs:
|
||||
- Checks that the current password is correct (if it was required in the flow)
|
||||
- Checks if the new password is found in a breach and warns the user if so (if the user selected
|
||||
the checkbox)
|
||||
- Checks that the new password meets any master password policy requirements enforced by an org
|
||||
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.)
|
||||
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
||||
`PasswordInputResult`) to be used by the parent as needed.
|
||||
|
||||
```typescript
|
||||
export interface PasswordInputResult {
|
||||
// Properties starting with "current..." are included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
currentPassword?: string;
|
||||
currentMasterKey?: MasterKey;
|
||||
currentServerMasterKeyHash?: string;
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string; // included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||
}
|
||||
```
|
||||
|
||||
# Example - InputPasswordFlow.SetInitialPassword
|
||||
# Example
|
||||
|
||||
<Story of={stories.SetInitialPassword} />
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
<br />
|
||||
|
||||
# Example - With Policy Requrements
|
||||
|
||||
<Story of={stories.WithPolicies} />
|
||||
<Story of={stories.ChangePasswordWithOptionalUserKeyRotation} />
|
||||
|
||||
@@ -2,14 +2,20 @@ import { importProvidersFrom } from "@angular/core";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { Meta, StoryObj, applicationConfig } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
import { ZXCVBNResult } from "zxcvbn";
|
||||
|
||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
// FIXME: remove `src` and fix import
|
||||
@@ -26,12 +32,47 @@ export default {
|
||||
providers: [
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
importProvidersFrom(BrowserAnimationsModule),
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "1" as UserId,
|
||||
name: "User",
|
||||
email: "user@email.com",
|
||||
emailVerified: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: AuditService,
|
||||
useValue: {
|
||||
passwordLeaked: () => Promise.resolve(1),
|
||||
} as Partial<AuditService>,
|
||||
},
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: {
|
||||
getAllDecrypted: () => Promise.resolve([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: {
|
||||
getKdfConfig$: () => of(DEFAULT_KDF_CONFIG),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MasterPasswordServiceAbstraction,
|
||||
useValue: {
|
||||
decryptUserKeyWithMasterKey: () => Promise.resolve("example-decrypted-user-key"),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
launchUri: () => Promise.resolve(true),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: KeyService,
|
||||
useValue: {
|
||||
@@ -87,11 +128,14 @@ export default {
|
||||
],
|
||||
args: {
|
||||
InputPasswordFlow: {
|
||||
SetInitialPassword: InputPasswordFlow.SetInitialPassword,
|
||||
AccountRegistration: InputPasswordFlow.AccountRegistration,
|
||||
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||
ChangePasswordWithOptionalUserKeyRotation:
|
||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||
},
|
||||
userId: "1" as UserId,
|
||||
email: "user@email.com",
|
||||
masterPasswordPolicyOptions: {
|
||||
minComplexity: 4,
|
||||
minLength: 14,
|
||||
@@ -108,11 +152,27 @@ export default {
|
||||
|
||||
type Story = StoryObj<InputPasswordComponent>;
|
||||
|
||||
export const SetInitialPassword: Story = {
|
||||
export const AccountRegistration: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"></auth-input-password>
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SetInitialPasswordAuthedUser: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -121,7 +181,11 @@ export const ChangePassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.ChangePassword"></auth-input-password>
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.ChangePassword"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -131,7 +195,9 @@ export const ChangePasswordWithOptionalUserKeyRotation: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
[flow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
@@ -142,7 +208,9 @@ export const WithPolicies: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -154,7 +222,8 @@ export const SecondaryButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
@@ -167,7 +236,8 @@ export const SecondaryButtonWithPlaceHolderText: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
@@ -180,7 +250,8 @@ export const InlineButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[inlineButtons]="true"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -192,7 +263,8 @@ export const InlineButtons: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
[inlineButtons]="true"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface PasswordInputResult {
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string;
|
||||
currentMasterKey?: MasterKey;
|
||||
currentServerMasterKeyHash?: string;
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
|
||||
@@ -58,12 +58,12 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
emailVerificationToken = "emailVerificationToken";
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
newPassword: "password",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -93,8 +93,8 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
masterPasswordHash: passwordInputResult.newServerMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.newPasswordHint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
publicKey: userKeyPair[0],
|
||||
|
||||
@@ -36,7 +36,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
providerUserId?: string,
|
||||
): Promise<void> {
|
||||
const [newUserKey, newEncUserKey] = await this.keyService.makeUserKey(
|
||||
passwordInputResult.masterKey,
|
||||
passwordInputResult.newMasterKey,
|
||||
);
|
||||
|
||||
if (!newUserKey || !newEncUserKey) {
|
||||
@@ -79,8 +79,8 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
|
||||
const registerFinishRequest = new RegisterFinishRequest(
|
||||
email,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.hint,
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
passwordInputResult.newPasswordHint,
|
||||
encryptedUserKey,
|
||||
userAsymmetricKeysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<auth-input-password
|
||||
*ngIf="!loading"
|
||||
[email]="email"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[loading]="submitting"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
|
||||
@@ -39,8 +39,7 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
InputPasswordFlow = InputPasswordFlow;
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||
loading = true;
|
||||
submitting = false;
|
||||
email: string;
|
||||
|
||||
@@ -111,12 +111,12 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userId = "userId" as UserId;
|
||||
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
newMasterKey: masterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
newPassword: "password",
|
||||
newPassword: "newPassword",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
@@ -131,9 +131,9 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.hint,
|
||||
passwordInputResult.newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
passwordInputResult.kdfConfig.kdfType,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { KdfConfigService, KeyService, KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import {
|
||||
SetPasswordCredentials,
|
||||
@@ -43,10 +43,10 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
hint,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
@@ -60,7 +60,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
}
|
||||
}
|
||||
|
||||
const protectedUserKey = await this.makeProtectedUserKey(masterKey, userId);
|
||||
const protectedUserKey = await this.makeProtectedUserKey(newMasterKey, userId);
|
||||
if (protectedUserKey == null) {
|
||||
throw new Error("protectedUserKey not found. Could not set password.");
|
||||
}
|
||||
@@ -70,12 +70,12 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
serverMasterKeyHash,
|
||||
newServerMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
hint,
|
||||
newPasswordHint,
|
||||
orgSsoIdentifier,
|
||||
keysRequest,
|
||||
kdfConfig.kdfType, // kdfConfig is always DEFAULT_KDF_CONFIG (see InputPasswordComponent)
|
||||
kdfConfig.kdfType,
|
||||
kdfConfig.iterations,
|
||||
);
|
||||
|
||||
@@ -85,14 +85,14 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
await this.masterPasswordService.setForceSetPasswordReason(ForceSetPasswordReason.None, userId);
|
||||
|
||||
// User now has a password so update account decryption options in state
|
||||
await this.updateAccountDecryptionProperties(masterKey, kdfConfig, protectedUserKey, userId);
|
||||
await this.updateAccountDecryptionProperties(newMasterKey, kdfConfig, protectedUserKey, userId);
|
||||
|
||||
await this.keyService.setPrivateKey(keyPair[1].encryptedString, userId);
|
||||
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(newLocalMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(serverMasterKeyHash, orgId, userId);
|
||||
await this.handleResetPasswordAutoEnroll(newServerMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
|
||||
private async updateAccountDecryptionProperties(
|
||||
masterKey: MasterKey,
|
||||
kdfConfig: PBKDF2KdfConfig,
|
||||
kdfConfig: KdfConfig,
|
||||
protectedUserKey: [UserKey, EncString],
|
||||
userId: UserId,
|
||||
) {
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
[flow]="inputPasswordFlow"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
protected inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAuthedUser;
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
@@ -60,9 +60,9 @@ export class SetPasswordJitComponent implements OnInit {
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
);
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
this.userId = activeAccount?.id;
|
||||
this.email = activeAccount?.email;
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
this.syncLoading = false;
|
||||
@@ -97,14 +97,12 @@ export class SetPasswordJitComponent implements OnInit {
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.submitting = true;
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
...passwordInputResult,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
userId,
|
||||
userId: this.userId,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
// @ts-strict-ignore
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
import { KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint: string;
|
||||
kdfConfig: KdfConfig;
|
||||
orgSsoIdentifier: string;
|
||||
orgId: string;
|
||||
resetPasswordAutoEnroll: boolean;
|
||||
|
||||
@@ -18,6 +18,7 @@ export enum FeatureFlag {
|
||||
SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions",
|
||||
|
||||
/* Auth */
|
||||
PM16117_ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor",
|
||||
PM9115_TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence",
|
||||
|
||||
/* Autofill */
|
||||
@@ -115,6 +116,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
|
||||
/* Auth */
|
||||
[FeatureFlag.PM16117_ChangeExistingPasswordRefactor]: FALSE,
|
||||
[FeatureFlag.PM9115_TwoFactorExtensionDataPersistence]: FALSE,
|
||||
|
||||
/* Billing */
|
||||
|
||||
Reference in New Issue
Block a user