mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
refactor(emergency-access-takeover): [PM-18721][PM-21271] Integrate InputPasswordComponent in EmergencyAccessTakeoverDialogComponent (#14636)
Integrates the `InputPasswordComponent` within the `EmergencyAccessTakeoverDialogComponent` Feature Flag: `PM16117_ChangeExistingPasswordRefactor`
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -87,13 +85,23 @@ export class EmergencyAccessService
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns policies that apply to the grantor.
|
||||
* Returns policies that apply to the grantor if the grantor is the owner of an org, otherwise returns null.
|
||||
* Intended for grantee.
|
||||
* @param id emergency access id
|
||||
*
|
||||
* @remarks
|
||||
* The ONLY time the API call will return an array of policies is when the Grantor is the OWNER
|
||||
* of an organization. In all other scenarios the server returns null. Even if the Grantor
|
||||
* is the member of an org that has enforced MP policies, the server will still return null
|
||||
* because in the Emergency Access Takeover process, the Grantor gets removed from the org upon
|
||||
* takeover, and therefore the MP policies are irrelevant.
|
||||
*
|
||||
* The only scenario where a Grantor does NOT get removed from the org is when that Grantor is the
|
||||
* OWNER of the org. In that case the server returns Grantor policies and we enforce them on the client.
|
||||
*/
|
||||
async getGrantorPolicies(id: string): Promise<Policy[]> {
|
||||
const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies(id);
|
||||
let policies: Policy[];
|
||||
let policies: Policy[] = [];
|
||||
if (response.data != null && response.data.length > 0) {
|
||||
policies = response.data.map((policyResponse) => new Policy(new PolicyData(policyResponse)));
|
||||
}
|
||||
@@ -299,6 +307,10 @@ export class EmergencyAccessService
|
||||
|
||||
const encKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey);
|
||||
|
||||
if (encKey == null || !encKey[1].encryptedString) {
|
||||
throw new Error("masterKeyEncryptedUserKey not found");
|
||||
}
|
||||
|
||||
const request = new EmergencyAccessPasswordRequest();
|
||||
request.newMasterPasswordHash = masterKeyHash;
|
||||
request.key = encKey[1].encryptedString;
|
||||
@@ -405,6 +417,15 @@ export class EmergencyAccessService
|
||||
}
|
||||
|
||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
||||
return (await this.encryptService.encapsulateKeyUnsigned(userKey, publicKey)).encryptedString;
|
||||
const publicKeyEncryptedUserKey = await this.encryptService.encapsulateKeyUnsigned(
|
||||
userKey,
|
||||
publicKey,
|
||||
);
|
||||
|
||||
if (publicKeyEncryptedUserKey == null || !publicKeyEncryptedUserKey.encryptedString) {
|
||||
throw new Error("publicKeyEncryptedUserKey not found");
|
||||
}
|
||||
|
||||
return publicKeyEncryptedUserKey.encryptedString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import { OrganizationManagementPreferencesService } from "@bitwarden/common/admi
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
@@ -34,6 +36,10 @@ import {
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAddEditDialogResult,
|
||||
} from "./emergency-access-add-edit.component";
|
||||
import {
|
||||
EmergencyAccessTakeoverDialogComponent,
|
||||
EmergencyAccessTakeoverDialogResultType,
|
||||
} from "./takeover/emergency-access-takeover-dialog.component";
|
||||
import {
|
||||
EmergencyAccessTakeoverComponent,
|
||||
EmergencyAccessTakeoverResultType,
|
||||
@@ -69,6 +75,7 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
@@ -285,6 +292,45 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
}
|
||||
|
||||
takeover = async (details: GrantorEmergencyAccess) => {
|
||||
const changePasswordRefactorFlag = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM16117_ChangeExistingPasswordRefactor,
|
||||
);
|
||||
|
||||
if (changePasswordRefactorFlag) {
|
||||
if (!details || !details.email || !details.id) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("grantorDetailsNotFound"),
|
||||
});
|
||||
this.logService.error(
|
||||
"Grantor details not found when attempting emergency access takeover",
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const grantorName = this.userNamePipe.transform(details);
|
||||
|
||||
const dialogRef = EmergencyAccessTakeoverDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
grantorName,
|
||||
grantorEmail: details.email,
|
||||
emergencyAccessId: details.id,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === EmergencyAccessTakeoverDialogResultType.Done) {
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("passwordResetFor", grantorName),
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(details),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<bit-dialog>
|
||||
<span bitDialogTitle>
|
||||
{{ "takeover" | i18n }}
|
||||
<small class="tw-text-muted" *ngIf="dialogData.grantorName">{{ dialogData.grantorName }}</small>
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
@if (initializing) {
|
||||
<div class="tw-flex tw-items-center tw-justify-center">
|
||||
<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>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- TODO: PM-22237 -->
|
||||
<!-- <bit-callout type="warning">{{
|
||||
"emergencyAccessLoggedOutWarning" | i18n: dialogData.grantorName
|
||||
}}</bit-callout> -->
|
||||
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
(isSubmitting)="handleIsSubmittingChange($event)"
|
||||
></auth-input-password>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
[disabled]="submitting$ | async"
|
||||
(click)="handlePrimaryButtonClick()"
|
||||
>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,160 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
|
||||
import { BehaviorSubject, combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
PasswordInputResult,
|
||||
} from "@bitwarden/auth/angular";
|
||||
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 { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import {
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { EmergencyAccessService } from "../../../emergency-access";
|
||||
|
||||
type EmergencyAccessTakeoverDialogData = {
|
||||
grantorName: string;
|
||||
grantorEmail: string;
|
||||
/** Traces a unique emergency request */
|
||||
emergencyAccessId: string;
|
||||
};
|
||||
|
||||
export const EmergencyAccessTakeoverDialogResultType = {
|
||||
Done: "done",
|
||||
} as const;
|
||||
|
||||
export type EmergencyAccessTakeoverDialogResultType =
|
||||
(typeof EmergencyAccessTakeoverDialogResultType)[keyof typeof EmergencyAccessTakeoverDialogResultType];
|
||||
|
||||
/**
|
||||
* This component is used by a Grantee to take over emergency access of a Grantor's account
|
||||
* by changing the Grantor's master password. It is displayed as a dialog when the Grantee
|
||||
* clicks the "Takeover" button while on the `/settings/emergency-access` page (see `EmergencyAccessComponent`).
|
||||
*
|
||||
* @link https://bitwarden.com/help/emergency-access/
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-emergency-access-takeover-dialog",
|
||||
templateUrl: "./emergency-access-takeover-dialog.component.html",
|
||||
imports: [
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
I18nPipe,
|
||||
InputPasswordComponent,
|
||||
],
|
||||
})
|
||||
export class EmergencyAccessTakeoverDialogComponent implements OnInit {
|
||||
@ViewChild(InputPasswordComponent)
|
||||
inputPasswordComponent: InputPasswordComponent | undefined = undefined;
|
||||
|
||||
private parentSubmittingBehaviorSubject = new BehaviorSubject(false);
|
||||
parentSubmitting$ = this.parentSubmittingBehaviorSubject.asObservable();
|
||||
|
||||
private childSubmittingBehaviorSubject = new BehaviorSubject(false);
|
||||
childSubmitting$ = this.childSubmittingBehaviorSubject.asObservable();
|
||||
|
||||
submitting$ = combineLatest([this.parentSubmitting$, this.childSubmitting$]).pipe(
|
||||
map(([parentIsSubmitting, childIsSubmitting]) => parentIsSubmitting || childIsSubmitting),
|
||||
);
|
||||
|
||||
initializing = true;
|
||||
inputPasswordFlow = InputPasswordFlow.ChangePasswordDelegation;
|
||||
masterPasswordPolicyOptions?: MasterPasswordPolicyOptions;
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected dialogData: EmergencyAccessTakeoverDialogData,
|
||||
private accountService: AccountService,
|
||||
private dialogRef: DialogRef<EmergencyAccessTakeoverDialogResultType>,
|
||||
private emergencyAccessService: EmergencyAccessService,
|
||||
private i18nService: I18nService,
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
const grantorPolicies = await this.emergencyAccessService.getGrantorPolicies(
|
||||
this.dialogData.emergencyAccessId,
|
||||
);
|
||||
|
||||
this.masterPasswordPolicyOptions = await firstValueFrom(
|
||||
this.policyService.masterPasswordPolicyOptions$(activeUserId, grantorPolicies),
|
||||
);
|
||||
|
||||
this.initializing = false;
|
||||
}
|
||||
|
||||
protected handlePrimaryButtonClick = async () => {
|
||||
if (!this.inputPasswordComponent) {
|
||||
throw new Error("InputPasswordComponent is not initialized");
|
||||
}
|
||||
|
||||
await this.inputPasswordComponent.submit();
|
||||
};
|
||||
|
||||
protected async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
this.parentSubmittingBehaviorSubject.next(true);
|
||||
|
||||
try {
|
||||
await this.emergencyAccessService.takeover(
|
||||
this.dialogData.emergencyAccessId,
|
||||
passwordInputResult.newPassword,
|
||||
this.dialogData.grantorEmail,
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("unexpectedError"),
|
||||
});
|
||||
} finally {
|
||||
this.parentSubmittingBehaviorSubject.next(false);
|
||||
}
|
||||
|
||||
this.dialogRef.close(EmergencyAccessTakeoverDialogResultType.Done);
|
||||
}
|
||||
|
||||
protected handleIsSubmittingChange(isSubmitting: boolean) {
|
||||
this.childSubmittingBehaviorSubject.next(isSubmitting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open an EmergencyAccessTakeoverDialogComponent
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param dialogConfig Configuration for the dialog
|
||||
*/
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<
|
||||
EmergencyAccessTakeoverDialogData,
|
||||
DialogRef<EmergencyAccessTakeoverDialogResultType, unknown>
|
||||
>,
|
||||
) => {
|
||||
return dialogService.open<
|
||||
EmergencyAccessTakeoverDialogResultType,
|
||||
EmergencyAccessTakeoverDialogData
|
||||
>(EmergencyAccessTakeoverDialogComponent, dialogConfig);
|
||||
};
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export type InitiationPath =
|
||||
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||
initializing = true;
|
||||
|
||||
/** Password Manager or Secrets Manager */
|
||||
|
||||
@@ -5370,6 +5370,9 @@
|
||||
"emergencyRejected": {
|
||||
"message": "Emergency access rejected"
|
||||
},
|
||||
"grantorDetailsNotFound": {
|
||||
"message": "Grantor details not found"
|
||||
},
|
||||
"passwordResetFor": {
|
||||
"message": "Password reset for $USER$. You can now login using the new password.",
|
||||
"placeholders": {
|
||||
@@ -5773,12 +5776,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"emergencyAccessLoggedOutWarning": {
|
||||
"message": "Proceeding will log $NAME$ out of their current session, requiring them to log back in. Active sessions on other devices may continue to remain active for up to one hour.",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "John Smith"
|
||||
}
|
||||
}
|
||||
},
|
||||
"thisUser": {
|
||||
"message": "this user"
|
||||
},
|
||||
"resetPasswordMasterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require the master password to meet the following requirements:"
|
||||
},
|
||||
"changePasswordDelegationMasterPasswordPolicyInEffect": {
|
||||
"message": "One or more organization policies require the master password to meet the following requirements:"
|
||||
},
|
||||
"resetPasswordSuccess": {
|
||||
"message": "Password reset success!"
|
||||
},
|
||||
|
||||
@@ -71,8 +71,11 @@ export class ChangePasswordComponent implements OnInit {
|
||||
throw new Error("activeAccount not found");
|
||||
}
|
||||
|
||||
if (passwordInputResult.currentPassword == null) {
|
||||
throw new Error("currentPassword not found");
|
||||
if (
|
||||
passwordInputResult.currentPassword == null ||
|
||||
passwordInputResult.newPasswordHint == null
|
||||
) {
|
||||
throw new Error("currentPassword or newPasswordHint not found");
|
||||
}
|
||||
|
||||
await this.syncService.fullSync(true);
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("DefaultChangePasswordService", () => {
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
"invalid PasswordInputResult credentials, could not change password",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -130,7 +130,7 @@ describe("DefaultChangePasswordService", () => {
|
||||
|
||||
// Assert
|
||||
await expect(testFn).rejects.toThrow(
|
||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
||||
"invalid PasswordInputResult credentials, could not change password",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,8 +26,14 @@ export class DefaultChangePasswordService implements ChangePasswordService {
|
||||
if (!userId) {
|
||||
throw new Error("userId not found");
|
||||
}
|
||||
if (!passwordInputResult.currentMasterKey || !passwordInputResult.currentServerMasterKeyHash) {
|
||||
throw new Error("currentMasterKey or currentServerMasterKeyHash not found");
|
||||
if (
|
||||
!passwordInputResult.currentMasterKey ||
|
||||
!passwordInputResult.currentServerMasterKeyHash ||
|
||||
!passwordInputResult.newMasterKey ||
|
||||
!passwordInputResult.newServerMasterKeyHash ||
|
||||
passwordInputResult.newPasswordHint == null
|
||||
) {
|
||||
throw new Error("invalid PasswordInputResult credentials, could not change password");
|
||||
}
|
||||
|
||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<auth-password-callout
|
||||
*ngIf="masterPasswordPolicyOptions"
|
||||
[message]="
|
||||
flow === InputPasswordFlow.ChangePasswordDelegation
|
||||
? 'changePasswordDelegationMasterPasswordPolicyInEffect'
|
||||
: 'masterPasswordPolicyInEffect'
|
||||
"
|
||||
[policy]="masterPasswordPolicyOptions"
|
||||
></auth-password-callout>
|
||||
|
||||
@@ -35,6 +40,22 @@
|
||||
type="password"
|
||||
formControlName="newPassword"
|
||||
/>
|
||||
<button
|
||||
*ngIf="flow === InputPasswordFlow.ChangePasswordDelegation"
|
||||
type="button"
|
||||
bitIconButton="bwi-generate"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'generatePassword' | i18n"
|
||||
(click)="generatePassword()"
|
||||
></button>
|
||||
<button
|
||||
*ngIf="flow === InputPasswordFlow.ChangePasswordDelegation"
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton="bwi-clone"
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy()"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
@@ -42,7 +63,7 @@
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
<bit-hint>
|
||||
<bit-hint *ngIf="flow !== InputPasswordFlow.ChangePasswordDelegation">
|
||||
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||
{{ "masterPassImportant" | i18n }}
|
||||
{{ minPasswordLengthMsg }}.
|
||||
@@ -74,26 +95,32 @@
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input id="input-password-form_new-password-hint" bitInput formControlName="newPasswordHint" />
|
||||
<bit-hint>
|
||||
{{
|
||||
"masterPassHintText"
|
||||
| i18n: formGroup.value.newPasswordHint.length : maxHintLength.toString()
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
<ng-container *ngIf="flow !== InputPasswordFlow.ChangePasswordDelegation">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_new-password-hint"
|
||||
bitInput
|
||||
formControlName="newPasswordHint"
|
||||
/>
|
||||
<bit-hint>
|
||||
{{
|
||||
"masterPassHintText"
|
||||
| i18n: formGroup.value.newPasswordHint.length.toString() : maxHintLength.toString()
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
|
||||
<bit-form-control>
|
||||
<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>
|
||||
<input
|
||||
id="input-password-form_check-for-breaches"
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="checkForBreaches"
|
||||
/>
|
||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</ng-container>
|
||||
|
||||
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
||||
<input
|
||||
@@ -116,7 +143,11 @@
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div class="tw-flex tw-gap-2" [ngClass]="inlineButtons ? 'tw-flex-row' : 'tw-flex-col'">
|
||||
<div
|
||||
*ngIf="flow !== InputPasswordFlow.ChangePasswordDelegation"
|
||||
class="tw-flex tw-gap-2"
|
||||
[ngClass]="inlineButtons ? 'tw-flex-row' : 'tw-flex-col'"
|
||||
>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [loading]="loading">
|
||||
{{ primaryButtonTextStr || ("setMasterPassword" | i18n) }}
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormControl } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
@@ -13,6 +13,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
ToastService,
|
||||
Translation,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
DEFAULT_KDF_CONFIG,
|
||||
KdfConfig,
|
||||
@@ -53,31 +55,46 @@ import { PasswordInputResult } from "./password-input-result";
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* Form elements displayed:
|
||||
* - [Input] New password
|
||||
* - [Input] New password confirm
|
||||
* - [Input] New password hint
|
||||
* - [Checkbox] Check for breaches
|
||||
* Form Fields: `[newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||
*
|
||||
* Note: this flow does not receive an active account `userId` as an `@Input`
|
||||
*/
|
||||
SetInitialPasswordAccountRegistration,
|
||||
/**
|
||||
* Form Fields: `[newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||
*/
|
||||
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)
|
||||
/**
|
||||
* Form Fields: `[currentPassword, newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||
*/
|
||||
ChangePassword,
|
||||
/**
|
||||
* All form elements above, plus: [Checkbox] Rotate account encryption key (as the last element in the UI)
|
||||
* Form Fields: `[currentPassword, newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches, rotateUserKey]`
|
||||
*/
|
||||
ChangePasswordWithOptionalUserKeyRotation,
|
||||
/**
|
||||
* This flow is used when a user changes the password for another user's account, such as:
|
||||
* - Emergency Access Takeover
|
||||
* - Account Recovery
|
||||
*
|
||||
* Since both of those processes use a dialog, the `InputPasswordComponent` will not display
|
||||
* buttons for `ChangePasswordDelegation` because the dialog will have its own buttons.
|
||||
*
|
||||
* Form Fields: `[newPassword, newPasswordConfirm]`
|
||||
*
|
||||
* Note: this flow does not receive an active account `userId` or `email` as `@Input`s
|
||||
*/
|
||||
ChangePasswordDelegation,
|
||||
}
|
||||
|
||||
interface InputPasswordForm {
|
||||
currentPassword?: FormControl<string>;
|
||||
|
||||
newPassword: FormControl<string>;
|
||||
newPasswordConfirm: FormControl<string>;
|
||||
newPasswordHint: FormControl<string>;
|
||||
checkForBreaches: FormControl<boolean>;
|
||||
newPasswordHint?: FormControl<string>;
|
||||
|
||||
currentPassword?: FormControl<string>;
|
||||
checkForBreaches?: FormControl<boolean>;
|
||||
rotateUserKey?: FormControl<boolean>;
|
||||
}
|
||||
|
||||
@@ -91,20 +108,25 @@ interface InputPasswordForm {
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule,
|
||||
JslibModule,
|
||||
PasswordCalloutComponent,
|
||||
PasswordStrengthV2Component,
|
||||
JslibModule,
|
||||
ReactiveFormsModule,
|
||||
SharedModule,
|
||||
],
|
||||
})
|
||||
export class InputPasswordComponent implements OnInit {
|
||||
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent:
|
||||
| PasswordStrengthV2Component
|
||||
| undefined = undefined;
|
||||
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||
@Output() isSubmitting = new EventEmitter<boolean>();
|
||||
|
||||
@Input({ required: true }) flow!: InputPasswordFlow;
|
||||
@Input({ required: true, transform: (val: string) => val.trim().toLowerCase() }) email!: string;
|
||||
|
||||
@Input({ transform: (val: string) => val?.trim().toLowerCase() }) email?: string;
|
||||
@Input() userId?: UserId;
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
@@ -132,11 +154,6 @@ export class InputPasswordComponent implements OnInit {
|
||||
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: [
|
||||
@@ -146,12 +163,6 @@ export class InputPasswordComponent implements OnInit {
|
||||
"newPasswordConfirm",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldNotMatch,
|
||||
"newPassword",
|
||||
"newPasswordHint",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -176,9 +187,11 @@ export class InputPasswordComponent implements OnInit {
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private keyService: KeyService,
|
||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private policyService: PolicyService,
|
||||
private toastService: ToastService,
|
||||
private validationService: ValidationService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -187,6 +200,27 @@ export class InputPasswordComponent implements OnInit {
|
||||
}
|
||||
|
||||
private addFormFieldsIfNecessary() {
|
||||
if (this.flow !== InputPasswordFlow.ChangePasswordDelegation) {
|
||||
this.formGroup.addControl(
|
||||
"newPasswordHint",
|
||||
this.formBuilder.nonNullable.control("", [
|
||||
Validators.minLength(this.minHintLength),
|
||||
Validators.maxLength(this.maxHintLength),
|
||||
]),
|
||||
);
|
||||
|
||||
this.formGroup.addValidators([
|
||||
compareInputs(
|
||||
ValidationGoal.InputsShouldNotMatch,
|
||||
"newPassword",
|
||||
"newPasswordHint",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
]);
|
||||
|
||||
this.formGroup.addControl("checkForBreaches", this.formBuilder.nonNullable.control(true));
|
||||
}
|
||||
|
||||
if (
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
@@ -227,160 +261,201 @@ export class InputPasswordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
protected submit = async () => {
|
||||
this.verifyFlowAndUserId();
|
||||
submit = async () => {
|
||||
try {
|
||||
this.isSubmitting.emit(true);
|
||||
|
||||
this.formGroup.markAllAsTouched();
|
||||
this.verifyFlow();
|
||||
|
||||
if (this.formGroup.invalid) {
|
||||
this.showErrorSummary = true;
|
||||
return;
|
||||
}
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to create master key.");
|
||||
}
|
||||
|
||||
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) {
|
||||
if (this.formGroup.invalid) {
|
||||
this.showErrorSummary = true;
|
||||
return;
|
||||
}
|
||||
|
||||
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 ?? true;
|
||||
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
|
||||
await this.handleChangePasswordDelegationFlow(newPassword);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to create master key.");
|
||||
}
|
||||
|
||||
// 1. Determine kdfConfig
|
||||
if (this.flow === InputPasswordFlow.SetInitialPasswordAccountRegistration) {
|
||||
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.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 newServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const newLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig: this.kdfConfig,
|
||||
};
|
||||
|
||||
if (
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
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.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||
}
|
||||
|
||||
// 5. Emit cryptographic keys and other password related properties
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
} catch (e) {
|
||||
this.validationService.showError(e);
|
||||
} finally {
|
||||
this.isSubmitting.emit(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* We cannot mark the `userId` or `email` `@Input`s as required because some flows
|
||||
* require them, and some do not. This method enforces that:
|
||||
* - Certain flows MUST have a `userId` and/or `email` passed down
|
||||
* - Certain flows must NOT have a `userId` and/or `email` passed down
|
||||
*/
|
||||
private verifyFlow() {
|
||||
/** UserId checks */
|
||||
|
||||
// These flows require that an active account userId must NOT be passed down
|
||||
if (
|
||||
this.flow === InputPasswordFlow.SetInitialPasswordAccountRegistration ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordDelegation
|
||||
) {
|
||||
if (this.userId) {
|
||||
throw new Error("There should be no active account userId passed down in a this flow.");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Verify new password
|
||||
// All other flows require that an active account userId MUST be passed down
|
||||
if (
|
||||
this.flow !== InputPasswordFlow.SetInitialPasswordAccountRegistration &&
|
||||
this.flow !== InputPasswordFlow.ChangePasswordDelegation
|
||||
) {
|
||||
if (!this.userId) {
|
||||
throw new Error("This flow requires that an active account userId be passed down.");
|
||||
}
|
||||
}
|
||||
|
||||
/** Email checks */
|
||||
|
||||
// This flow requires that an email must NOT be passed down
|
||||
if (this.flow === InputPasswordFlow.ChangePasswordDelegation) {
|
||||
if (this.email) {
|
||||
throw new Error("There should be no email passed down in this flow.");
|
||||
}
|
||||
}
|
||||
|
||||
// All other flows require that an email MUST be passed down
|
||||
if (this.flow !== InputPasswordFlow.ChangePasswordDelegation) {
|
||||
if (!this.email) {
|
||||
throw new Error("This flow requires that an email be passed down.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handleChangePasswordDelegationFlow(newPassword: string) {
|
||||
const newPasswordVerified = await this.verifyNewPassword(
|
||||
newPassword,
|
||||
this.passwordStrengthScore,
|
||||
checkForBreaches,
|
||||
false,
|
||||
);
|
||||
if (!newPasswordVerified) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Create cryptographic keys and build a PasswordInputResult object
|
||||
const newMasterKey = await this.keyService.makeMasterKey(
|
||||
newPassword,
|
||||
this.email,
|
||||
this.kdfConfig,
|
||||
);
|
||||
|
||||
const newServerMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const newLocalMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
newMasterKey,
|
||||
newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash,
|
||||
newPasswordHint,
|
||||
kdfConfig: this.kdfConfig,
|
||||
};
|
||||
|
||||
if (
|
||||
this.flow === InputPasswordFlow.ChangePassword ||
|
||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
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.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.controls.rotateUserKey?.value;
|
||||
}
|
||||
|
||||
// 5. Emit cryptographic keys and other password related properties
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,16 +466,19 @@ export class InputPasswordComponent implements OnInit {
|
||||
currentPassword: string,
|
||||
kdfConfig: KdfConfig,
|
||||
): Promise<boolean> {
|
||||
if (!this.email) {
|
||||
throw new Error("Email is required to verify current password.");
|
||||
}
|
||||
if (!this.userId) {
|
||||
throw new Error("userId is required to verify current password.");
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -549,4 +627,33 @@ export class InputPasswordComponent implements OnInit {
|
||||
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
|
||||
this.passwordStrengthScore = score;
|
||||
}
|
||||
|
||||
protected async generatePassword() {
|
||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
||||
this.formGroup.patchValue({
|
||||
newPassword: await this.passwordGenerationService.generatePassword(options),
|
||||
});
|
||||
|
||||
if (!this.passwordStrengthComponent) {
|
||||
throw new Error("PasswordStrengthComponent is not initialized");
|
||||
}
|
||||
|
||||
this.passwordStrengthComponent.updatePasswordStrength(
|
||||
this.formGroup.controls.newPassword.value,
|
||||
);
|
||||
}
|
||||
|
||||
protected copy() {
|
||||
const value = this.formGroup.value.newPassword;
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: "",
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
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 `InputPasswordComponent` allows a user to enter master password related credentials.
|
||||
Specifically, it does the following:
|
||||
|
||||
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
|
||||
the parent component to act on those values as needed.
|
||||
1. Displays form fields in the UI
|
||||
2. Validates form fields
|
||||
3. Generates cryptographic properties based on the form inputs (e.g. `newMasterKey`,
|
||||
`newServerMasterKeyHash`, etc.)
|
||||
4. Emits the generated properties to the parent component
|
||||
|
||||
The `InputPasswordComponent` is central to our set/change password flows, allowing us to keep our
|
||||
form UI and validation logic consistent. As such, it is intended for re-use in different set/change
|
||||
password scenarios throughout the Bitwarden application. It is mostly presentational and simply
|
||||
emits values rather than acting on them itself. It is the job of the parent component to act on
|
||||
those values as needed.
|
||||
|
||||
<br />
|
||||
|
||||
@@ -22,11 +28,13 @@ the parent component to act on those values as needed.
|
||||
- [@Inputs](#inputs)
|
||||
- [@Outputs](#outputs)
|
||||
- [The InputPasswordFlow](#the-inputpasswordflow)
|
||||
- [Use Cases](#use-cases)
|
||||
- [HTML - Form Fields](#html---form-fields)
|
||||
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
||||
- [Difference between AccountRegistration and SetInitialPasswordAuthedUser](#difference-between-accountregistration-and-setinitialpasswordautheduser)
|
||||
- [Difference between SetInitialPasswordAccountRegistration and SetInitialPasswordAuthedUser](#difference-between-setinitialpasswordaccountregistration-and-setinitialpasswordautheduser)
|
||||
- [Validation](#validation)
|
||||
- [Submit Logic](#submit-logic)
|
||||
- [Submitting From a Parent Dialog Component](#submitting-from-a-parent-dialog-component)
|
||||
- [Example](#example)
|
||||
|
||||
<br />
|
||||
@@ -37,12 +45,24 @@ the parent component to act on those values as needed.
|
||||
|
||||
- `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.
|
||||
and emitted. [Click here](#the-inputpasswordflow) to learn more about the different
|
||||
`InputPasswordFlow` options.
|
||||
|
||||
**Optional (sometimes)**
|
||||
|
||||
These two `@Inputs` are optional on some flows, but required on others. Therefore these `@Inputs`
|
||||
are not marked as `{ required: true }`, but there _is_ component logic that ensures (requires) that
|
||||
the `email` and/or `userId` is present in certain flows, while not present in other flows.
|
||||
|
||||
- `email` - allows the `InputPasswordComponent` to generate a master key
|
||||
- `userId` - allows the `InputPasswordComponent` to do things like get the user's `kdfConfig`,
|
||||
verify that a current password is correct, and perform validation prior to user key rotation on
|
||||
the parent
|
||||
|
||||
**Optional**
|
||||
|
||||
These `@Inputs` are truly optional.
|
||||
|
||||
- `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` is true.
|
||||
@@ -57,6 +77,7 @@ the parent component to act on those values as needed.
|
||||
## `@Output()`'s
|
||||
|
||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||
([see more below](#submit-logic)).
|
||||
- `onSecondaryButtonClick` - on click, emits a notice that the secondary button has been clicked.
|
||||
The parent component can listen for this event and take some custom action as needed (go back,
|
||||
cancel, logout, etc.)
|
||||
@@ -66,79 +87,100 @@ the parent component to act on those values as needed.
|
||||
## The `InputPasswordFlow`
|
||||
|
||||
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
||||
credential generation logic of the component.
|
||||
credential generation logic of the component. It is important for the dev to understand when to use
|
||||
each flow.
|
||||
|
||||
<br />
|
||||
### Use Cases
|
||||
|
||||
**`SetInitialPasswordAccountRegistration`**
|
||||
|
||||
Used in 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 /><br />
|
||||
|
||||
**`SetInitialPasswordAuthedUser`**
|
||||
|
||||
Used in 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<br /><br />
|
||||
|
||||
**`ChangePassword`**
|
||||
|
||||
Used in scenarios where we simply want to offer the user the ability to change their password:
|
||||
|
||||
- User clicks an org email invite link an logs in with their password which does not meet the org's
|
||||
policy requirements
|
||||
- User logs in with password that does not meet the org's policy requirements
|
||||
- User logs in after their password was reset via Account Recovery (and now they must change their
|
||||
password)<br /><br />
|
||||
|
||||
**`ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
Used in scenarios where we want to offer users the additional option of rotating their user key:
|
||||
|
||||
- Account Settings (Web) - change password screen
|
||||
|
||||
Note that the user key rotation itself does not happen on the `InputPasswordComponent`, but rather
|
||||
on the parent component. The `InputPasswordComponent` simply emits a boolean value that indicates
|
||||
whether or not the user key should be rotated.<br /><br />
|
||||
|
||||
**`ChangePasswordDelegation`**
|
||||
|
||||
Used in scenarios where one user changes the password for another user's account:
|
||||
|
||||
- Emergency Access Takeover
|
||||
- Account Recovery<br /><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`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Input: Current password (as the first element in the UI)
|
||||
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
Includes everything above, plus:
|
||||
|
||||
- Checkbox: Rotate account encryption key (as the last element in the UI)
|
||||
Click through the individual Stories in Storybook to see how the `InputPassswordFlow` determines
|
||||
which form field UI elements get displayed.
|
||||
|
||||
<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`).
|
||||
- **`SetInitialPasswordAccountRegistration`** and **`SetInitialPasswordAuthedUser`**
|
||||
- These 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`).<br /><br />
|
||||
- **`ChangePassword`** and **`ChangePasswordWithOptionalUserKeyRotation`**
|
||||
- These 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 /><br />
|
||||
- **`ChangePasswordDelegation`**
|
||||
- This flow does not generate any credentials, but simply validates the new password and emits it
|
||||
up to the parent.
|
||||
|
||||
<br />
|
||||
|
||||
### Difference between `AccountRegistration` and `SetInitialPasswordAuthedUser`
|
||||
### Difference between `SetInitialPasswordAccountRegistration` 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 />
|
||||
|
||||
- `SetInitialPasswordAccountRegistration` involves scenarios where we have no existing user, and
|
||||
**thus NO active account `userId`**:
|
||||
- `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:
|
||||
@@ -148,12 +190,12 @@ the correct `kdfConfig` prior to key generation:
|
||||
`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:
|
||||
`SetInitialPasswordAccountRegistration` 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 `SetInitialPasswordAccountRegistration`, 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`
|
||||
|
||||
@@ -169,11 +211,6 @@ Form validators ensure that:
|
||||
- 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 />
|
||||
|
||||
## Submit Logic
|
||||
@@ -182,9 +219,10 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
||||
|
||||
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
|
||||
- Checks that the new password is not weak or found in any breaches (if the user selected the
|
||||
checkbox)
|
||||
- Checks that the new password adheres to any enforced master password policies that were
|
||||
optionally passed down by the parent
|
||||
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
|
||||
@@ -192,23 +230,83 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
||||
|
||||
```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;
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint?: string;
|
||||
newMasterKey?: MasterKey;
|
||||
newServerMasterKeyHash?: string;
|
||||
newLocalMasterKeyHash?: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||
kdfConfig?: KdfConfig;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Submitting From a Parent Dialog Component
|
||||
|
||||
Some of our set/change password flows use dialogs, such as Emergency Access Takeover and Account
|
||||
Recovery. These are covered by the `ChangePasswordDelegation` flow. Because dialogs have their own
|
||||
buttons, we don't want to display an additional Submit button in the `InputPasswordComponent` when
|
||||
embedded in a dialog.
|
||||
|
||||
Therefore we do the following:
|
||||
|
||||
- The `InputPasswordComponent` hides the button in the UI and exposes its `submit()` method as a
|
||||
public method.
|
||||
- The parent dialog component can then access this method via `@ViewChild()`.
|
||||
- When the user clicks the primary button on the parent dialog, we call the `submit()` method on the
|
||||
`InputPasswordComponent`.
|
||||
|
||||
```html
|
||||
<!-- emergency-access-takeover-dialog.component.html -->
|
||||
|
||||
<bit-dialog dialogSize="large">
|
||||
<span bitDialogTitle><!-- ... --></span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
></auth-input-password>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" (click)="handlePrimaryButtonClick()">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
```
|
||||
|
||||
```typescript
|
||||
// emergency-access-takeover-dialog.component.ts
|
||||
|
||||
export class EmergencyAccessTakeoverDialogComponent implements OnInit {
|
||||
@ViewChild(InputPasswordComponent)
|
||||
inputPasswordComponent: InputPasswordComponent;
|
||||
|
||||
// ...
|
||||
|
||||
handlePrimaryButtonClick = async () => {
|
||||
await this.inputPasswordComponent.submit();
|
||||
};
|
||||
|
||||
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
||||
// ... run logic that handles the `PasswordInputResult` object emission
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
|
||||
# Example
|
||||
|
||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||
|
||||
@@ -11,12 +11,14 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
||||
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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.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";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
// FIXME: remove `/apps` import from `/libs`
|
||||
@@ -73,6 +75,7 @@ export default {
|
||||
provide: PlatformUtilsService,
|
||||
useValue: {
|
||||
launchUri: () => Promise.resolve(true),
|
||||
copyToClipboard: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -125,16 +128,31 @@ export default {
|
||||
showToast: action("ToastService.showToast"),
|
||||
} as Partial<ToastService>,
|
||||
},
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: {
|
||||
getOptions: () => ({}),
|
||||
generatePassword: () => "generated-password",
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ValidationService,
|
||||
useValue: {
|
||||
showError: () => ["validation error"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
InputPasswordFlow: {
|
||||
AccountRegistration: InputPasswordFlow.AccountRegistration,
|
||||
SetInitialPasswordAccountRegistration:
|
||||
InputPasswordFlow.SetInitialPasswordAccountRegistration,
|
||||
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||
ChangePasswordWithOptionalUserKeyRotation:
|
||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||
ChangePasswordDelegation: InputPasswordFlow.ChangePasswordDelegation,
|
||||
},
|
||||
userId: "1" as UserId,
|
||||
email: "user@email.com",
|
||||
@@ -154,12 +172,12 @@ export default {
|
||||
|
||||
type Story = StoryObj<InputPasswordComponent>;
|
||||
|
||||
export const AccountRegistration: Story = {
|
||||
export const SetInitialPasswordAccountRegistration: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -205,14 +223,24 @@ export const ChangePasswordWithOptionalUserKeyRotation: Story = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const ChangePasswordDelegation: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [flow]="InputPasswordFlow.ChangePasswordDelegation"></auth-input-password>
|
||||
<br />
|
||||
<div>Note: no buttons here as this flow is expected to be used in a dialog, which will have its own buttons</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithPolicies: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
[userId]="userId"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
></auth-input-password>
|
||||
`,
|
||||
@@ -224,7 +252,7 @@ export const SecondaryButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
@@ -238,7 +266,7 @@ export const SecondaryButtonWithPlaceHolderText: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
@@ -252,7 +280,7 @@ export const InlineButton: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
[inlineButtons]="true"
|
||||
></auth-input-password>
|
||||
@@ -265,7 +293,7 @@ export const InlineButtons: Story = {
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[flow]="InputPasswordFlow.AccountRegistration"
|
||||
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||
[email]="email"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
[inlineButtons]="true"
|
||||
|
||||
@@ -8,11 +8,11 @@ export interface PasswordInputResult {
|
||||
currentLocalMasterKeyHash?: string;
|
||||
|
||||
newPassword: string;
|
||||
newPasswordHint: string;
|
||||
newMasterKey: MasterKey;
|
||||
newServerMasterKeyHash: string;
|
||||
newLocalMasterKeyHash: string;
|
||||
newPasswordHint?: string;
|
||||
newMasterKey?: MasterKey;
|
||||
newServerMasterKeyHash?: string;
|
||||
newLocalMasterKeyHash?: string;
|
||||
|
||||
kdfConfig: KdfConfig;
|
||||
kdfConfig?: KdfConfig;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
||||
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||
loading = true;
|
||||
submitting = false;
|
||||
email: string;
|
||||
|
||||
@@ -122,7 +122,11 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
};
|
||||
|
||||
credentials = {
|
||||
...passwordInputResult,
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier,
|
||||
orgId,
|
||||
resetPasswordAutoEnroll,
|
||||
|
||||
@@ -97,7 +97,11 @@ export class SetPasswordJitComponent implements OnInit {
|
||||
this.submitting = true;
|
||||
|
||||
const credentials: SetPasswordCredentials = {
|
||||
...passwordInputResult,
|
||||
newMasterKey: passwordInputResult.newMasterKey,
|
||||
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||
kdfConfig: passwordInputResult.kdfConfig,
|
||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||
orgId: this.orgId,
|
||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||
|
||||
Reference in New Issue
Block a user