mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +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 { Injectable } from "@angular/core";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
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.
|
* Intended for grantee.
|
||||||
* @param id emergency access id
|
* @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[]> {
|
async getGrantorPolicies(id: string): Promise<Policy[]> {
|
||||||
const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies(id);
|
const response = await this.emergencyAccessApiService.getEmergencyGrantorPolicies(id);
|
||||||
let policies: Policy[];
|
let policies: Policy[] = [];
|
||||||
if (response.data != null && response.data.length > 0) {
|
if (response.data != null && response.data.length > 0) {
|
||||||
policies = response.data.map((policyResponse) => new Policy(new PolicyData(policyResponse)));
|
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);
|
const encKey = await this.keyService.encryptUserKeyWithMasterKey(masterKey, grantorUserKey);
|
||||||
|
|
||||||
|
if (encKey == null || !encKey[1].encryptedString) {
|
||||||
|
throw new Error("masterKeyEncryptedUserKey not found");
|
||||||
|
}
|
||||||
|
|
||||||
const request = new EmergencyAccessPasswordRequest();
|
const request = new EmergencyAccessPasswordRequest();
|
||||||
request.newMasterPasswordHash = masterKeyHash;
|
request.newMasterPasswordHash = masterKeyHash;
|
||||||
request.key = encKey[1].encryptedString;
|
request.key = encKey[1].encryptedString;
|
||||||
@@ -405,6 +417,15 @@ export class EmergencyAccessService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async encryptKey(userKey: UserKey, publicKey: Uint8Array): Promise<EncryptedString> {
|
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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
@@ -34,6 +36,10 @@ import {
|
|||||||
EmergencyAccessAddEditComponent,
|
EmergencyAccessAddEditComponent,
|
||||||
EmergencyAccessAddEditDialogResult,
|
EmergencyAccessAddEditDialogResult,
|
||||||
} from "./emergency-access-add-edit.component";
|
} from "./emergency-access-add-edit.component";
|
||||||
|
import {
|
||||||
|
EmergencyAccessTakeoverDialogComponent,
|
||||||
|
EmergencyAccessTakeoverDialogResultType,
|
||||||
|
} from "./takeover/emergency-access-takeover-dialog.component";
|
||||||
import {
|
import {
|
||||||
EmergencyAccessTakeoverComponent,
|
EmergencyAccessTakeoverComponent,
|
||||||
EmergencyAccessTakeoverResultType,
|
EmergencyAccessTakeoverResultType,
|
||||||
@@ -69,6 +75,7 @@ export class EmergencyAccessComponent implements OnInit {
|
|||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private configService: ConfigService,
|
||||||
) {
|
) {
|
||||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||||
switchMap((account) =>
|
switchMap((account) =>
|
||||||
@@ -285,6 +292,45 @@ export class EmergencyAccessComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
takeover = async (details: GrantorEmergencyAccess) => {
|
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, {
|
const dialogRef = EmergencyAccessTakeoverComponent.open(this.dialogService, {
|
||||||
data: {
|
data: {
|
||||||
name: this.userNamePipe.transform(details),
|
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 {
|
export class CompleteTrialInitiationComponent implements OnInit, OnDestroy {
|
||||||
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
@ViewChild("stepper", { static: false }) verticalStepper: VerticalStepperComponent;
|
||||||
|
|
||||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||||
initializing = true;
|
initializing = true;
|
||||||
|
|
||||||
/** Password Manager or Secrets Manager */
|
/** Password Manager or Secrets Manager */
|
||||||
|
|||||||
@@ -5370,6 +5370,9 @@
|
|||||||
"emergencyRejected": {
|
"emergencyRejected": {
|
||||||
"message": "Emergency access rejected"
|
"message": "Emergency access rejected"
|
||||||
},
|
},
|
||||||
|
"grantorDetailsNotFound": {
|
||||||
|
"message": "Grantor details not found"
|
||||||
|
},
|
||||||
"passwordResetFor": {
|
"passwordResetFor": {
|
||||||
"message": "Password reset for $USER$. You can now login using the new password.",
|
"message": "Password reset for $USER$. You can now login using the new password.",
|
||||||
"placeholders": {
|
"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": {
|
"thisUser": {
|
||||||
"message": "this user"
|
"message": "this user"
|
||||||
},
|
},
|
||||||
"resetPasswordMasterPasswordPolicyInEffect": {
|
"resetPasswordMasterPasswordPolicyInEffect": {
|
||||||
"message": "One or more organization policies require the master password to meet the following requirements:"
|
"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": {
|
"resetPasswordSuccess": {
|
||||||
"message": "Password reset success!"
|
"message": "Password reset success!"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,8 +71,11 @@ export class ChangePasswordComponent implements OnInit {
|
|||||||
throw new Error("activeAccount not found");
|
throw new Error("activeAccount not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (passwordInputResult.currentPassword == null) {
|
if (
|
||||||
throw new Error("currentPassword not found");
|
passwordInputResult.currentPassword == null ||
|
||||||
|
passwordInputResult.newPasswordHint == null
|
||||||
|
) {
|
||||||
|
throw new Error("currentPassword or newPasswordHint not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.syncService.fullSync(true);
|
await this.syncService.fullSync(true);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ describe("DefaultChangePasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(testFn).rejects.toThrow(
|
await expect(testFn).rejects.toThrow(
|
||||||
"currentMasterKey or currentServerMasterKeyHash not found",
|
"invalid PasswordInputResult credentials, could not change password",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ describe("DefaultChangePasswordService", () => {
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await expect(testFn).rejects.toThrow(
|
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) {
|
if (!userId) {
|
||||||
throw new Error("userId not found");
|
throw new Error("userId not found");
|
||||||
}
|
}
|
||||||
if (!passwordInputResult.currentMasterKey || !passwordInputResult.currentServerMasterKeyHash) {
|
if (
|
||||||
throw new Error("currentMasterKey or currentServerMasterKeyHash not found");
|
!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(
|
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<auth-password-callout
|
<auth-password-callout
|
||||||
*ngIf="masterPasswordPolicyOptions"
|
*ngIf="masterPasswordPolicyOptions"
|
||||||
|
[message]="
|
||||||
|
flow === InputPasswordFlow.ChangePasswordDelegation
|
||||||
|
? 'changePasswordDelegationMasterPasswordPolicyInEffect'
|
||||||
|
: 'masterPasswordPolicyInEffect'
|
||||||
|
"
|
||||||
[policy]="masterPasswordPolicyOptions"
|
[policy]="masterPasswordPolicyOptions"
|
||||||
></auth-password-callout>
|
></auth-password-callout>
|
||||||
|
|
||||||
@@ -35,6 +40,22 @@
|
|||||||
type="password"
|
type="password"
|
||||||
formControlName="newPassword"
|
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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
bitIconButton
|
bitIconButton
|
||||||
@@ -42,7 +63,7 @@
|
|||||||
bitPasswordInputToggle
|
bitPasswordInputToggle
|
||||||
[(toggled)]="showPassword"
|
[(toggled)]="showPassword"
|
||||||
></button>
|
></button>
|
||||||
<bit-hint>
|
<bit-hint *ngIf="flow !== InputPasswordFlow.ChangePasswordDelegation">
|
||||||
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
<span class="tw-font-bold">{{ "important" | i18n }} </span>
|
||||||
{{ "masterPassImportant" | i18n }}
|
{{ "masterPassImportant" | i18n }}
|
||||||
{{ minPasswordLengthMsg }}.
|
{{ minPasswordLengthMsg }}.
|
||||||
@@ -74,26 +95,32 @@
|
|||||||
></button>
|
></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-field>
|
<ng-container *ngIf="flow !== InputPasswordFlow.ChangePasswordDelegation">
|
||||||
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
<bit-form-field>
|
||||||
<input id="input-password-form_new-password-hint" bitInput formControlName="newPasswordHint" />
|
<bit-label>{{ "masterPassHintLabel" | i18n }}</bit-label>
|
||||||
<bit-hint>
|
<input
|
||||||
{{
|
id="input-password-form_new-password-hint"
|
||||||
"masterPassHintText"
|
bitInput
|
||||||
| i18n: formGroup.value.newPasswordHint.length : maxHintLength.toString()
|
formControlName="newPasswordHint"
|
||||||
}}
|
/>
|
||||||
</bit-hint>
|
<bit-hint>
|
||||||
</bit-form-field>
|
{{
|
||||||
|
"masterPassHintText"
|
||||||
|
| i18n: formGroup.value.newPasswordHint.length.toString() : maxHintLength.toString()
|
||||||
|
}}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
|
||||||
<bit-form-control>
|
<bit-form-control>
|
||||||
<input
|
<input
|
||||||
id="input-password-form_check-for-breaches"
|
id="input-password-form_check-for-breaches"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bitCheckbox
|
bitCheckbox
|
||||||
formControlName="checkForBreaches"
|
formControlName="checkForBreaches"
|
||||||
/>
|
/>
|
||||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||||
</bit-form-control>
|
</bit-form-control>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
<bit-form-control *ngIf="flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation">
|
||||||
<input
|
<input
|
||||||
@@ -116,7 +143,11 @@
|
|||||||
</bit-label>
|
</bit-label>
|
||||||
</bit-form-control>
|
</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">
|
<button type="submit" bitButton bitFormButton buttonType="primary" [loading]="loading">
|
||||||
{{ primaryButtonTextStr || ("setMasterPassword" | i18n) }}
|
{{ primaryButtonTextStr || ("setMasterPassword" | i18n) }}
|
||||||
</button>
|
</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 { ReactiveFormsModule, FormBuilder, Validators, FormControl } from "@angular/forms";
|
||||||
import { firstValueFrom } from "rxjs";
|
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 { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { 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 { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
ToastService,
|
ToastService,
|
||||||
Translation,
|
Translation,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import {
|
import {
|
||||||
DEFAULT_KDF_CONFIG,
|
DEFAULT_KDF_CONFIG,
|
||||||
KdfConfig,
|
KdfConfig,
|
||||||
@@ -53,31 +55,46 @@ import { PasswordInputResult } from "./password-input-result";
|
|||||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||||
export enum InputPasswordFlow {
|
export enum InputPasswordFlow {
|
||||||
/**
|
/**
|
||||||
* Form elements displayed:
|
* Form Fields: `[newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||||
* - [Input] New password
|
*
|
||||||
* - [Input] New password confirm
|
* Note: this flow does not receive an active account `userId` as an `@Input`
|
||||||
* - [Input] New password hint
|
*/
|
||||||
* - [Checkbox] Check for breaches
|
SetInitialPasswordAccountRegistration,
|
||||||
|
/**
|
||||||
|
* Form Fields: `[newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||||
*/
|
*/
|
||||||
AccountRegistration, // important: this flow does not involve an activeAccount/userId
|
|
||||||
SetInitialPasswordAuthedUser,
|
SetInitialPasswordAuthedUser,
|
||||||
/*
|
/**
|
||||||
* All form elements above, plus: [Input] Current password (as the first element in the UI)
|
* Form Fields: `[currentPassword, newPassword, newPasswordConfirm, newPasswordHint, checkForBreaches]`
|
||||||
*/
|
*/
|
||||||
ChangePassword,
|
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,
|
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 {
|
interface InputPasswordForm {
|
||||||
|
currentPassword?: FormControl<string>;
|
||||||
|
|
||||||
newPassword: FormControl<string>;
|
newPassword: FormControl<string>;
|
||||||
newPasswordConfirm: FormControl<string>;
|
newPasswordConfirm: FormControl<string>;
|
||||||
newPasswordHint: FormControl<string>;
|
newPasswordHint?: FormControl<string>;
|
||||||
checkForBreaches: FormControl<boolean>;
|
|
||||||
|
|
||||||
currentPassword?: FormControl<string>;
|
checkForBreaches?: FormControl<boolean>;
|
||||||
rotateUserKey?: FormControl<boolean>;
|
rotateUserKey?: FormControl<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,20 +108,25 @@ interface InputPasswordForm {
|
|||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
InputModule,
|
InputModule,
|
||||||
ReactiveFormsModule,
|
JslibModule,
|
||||||
SharedModule,
|
|
||||||
PasswordCalloutComponent,
|
PasswordCalloutComponent,
|
||||||
PasswordStrengthV2Component,
|
PasswordStrengthV2Component,
|
||||||
JslibModule,
|
ReactiveFormsModule,
|
||||||
|
SharedModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class InputPasswordComponent implements OnInit {
|
export class InputPasswordComponent implements OnInit {
|
||||||
|
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent:
|
||||||
|
| PasswordStrengthV2Component
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||||
|
@Output() isSubmitting = new EventEmitter<boolean>();
|
||||||
|
|
||||||
@Input({ required: true }) flow!: InputPasswordFlow;
|
@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() userId?: UserId;
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||||
@@ -132,11 +154,6 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
Validators.minLength(this.minPasswordLength),
|
Validators.minLength(this.minPasswordLength),
|
||||||
]),
|
]),
|
||||||
newPasswordConfirm: this.formBuilder.nonNullable.control("", Validators.required),
|
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: [
|
validators: [
|
||||||
@@ -146,12 +163,6 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
"newPasswordConfirm",
|
"newPasswordConfirm",
|
||||||
this.i18nService.t("masterPassDoesntMatch"),
|
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 kdfConfigService: KdfConfigService,
|
||||||
private keyService: KeyService,
|
private keyService: KeyService,
|
||||||
private masterPasswordService: MasterPasswordServiceAbstraction,
|
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
|
private validationService: ValidationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -187,6 +200,27 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addFormFieldsIfNecessary() {
|
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 (
|
if (
|
||||||
this.flow === InputPasswordFlow.ChangePassword ||
|
this.flow === InputPasswordFlow.ChangePassword ||
|
||||||
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
this.flow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||||
@@ -227,160 +261,201 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected submit = async () => {
|
submit = async () => {
|
||||||
this.verifyFlowAndUserId();
|
try {
|
||||||
|
this.isSubmitting.emit(true);
|
||||||
|
|
||||||
this.formGroup.markAllAsTouched();
|
this.verifyFlow();
|
||||||
|
|
||||||
if (this.formGroup.invalid) {
|
this.formGroup.markAllAsTouched();
|
||||||
this.showErrorSummary = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.email) {
|
if (this.formGroup.invalid) {
|
||||||
throw new Error("Email is required to create master key.");
|
this.showErrorSummary = true;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
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(
|
const newPasswordVerified = await this.verifyNewPassword(
|
||||||
newPassword,
|
newPassword,
|
||||||
this.passwordStrengthScore,
|
this.passwordStrengthScore,
|
||||||
checkForBreaches,
|
false,
|
||||||
);
|
);
|
||||||
if (!newPasswordVerified) {
|
if (!newPasswordVerified) {
|
||||||
return;
|
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 = {
|
const passwordInputResult: PasswordInputResult = {
|
||||||
newPassword,
|
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.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,
|
currentPassword: string,
|
||||||
kdfConfig: KdfConfig,
|
kdfConfig: KdfConfig,
|
||||||
): Promise<boolean> {
|
): 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(
|
const currentMasterKey = await this.keyService.makeMasterKey(
|
||||||
currentPassword,
|
currentPassword,
|
||||||
this.email,
|
this.email,
|
||||||
kdfConfig,
|
kdfConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.userId) {
|
|
||||||
throw new Error("userId not passed down");
|
|
||||||
}
|
|
||||||
|
|
||||||
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
const decryptedUserKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||||
currentMasterKey,
|
currentMasterKey,
|
||||||
this.userId,
|
this.userId,
|
||||||
@@ -549,4 +627,33 @@ export class InputPasswordComponent implements OnInit {
|
|||||||
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
|
protected getPasswordStrengthScore(score: PasswordStrengthScore) {
|
||||||
this.passwordStrengthScore = score;
|
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
|
# InputPassword Component
|
||||||
|
|
||||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On form
|
The `InputPasswordComponent` allows a user to enter master password related credentials.
|
||||||
submission, the component creates cryptographic properties (`newMasterKey`,
|
Specifically, it does the following:
|
||||||
`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
|
1. Displays form fields in the UI
|
||||||
is mostly presentational and simply emits values rather than acting on them itself. It is the job of
|
2. Validates form fields
|
||||||
the parent component to act on those values as needed.
|
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 />
|
<br />
|
||||||
|
|
||||||
@@ -22,11 +28,13 @@ the parent component to act on those values as needed.
|
|||||||
- [@Inputs](#inputs)
|
- [@Inputs](#inputs)
|
||||||
- [@Outputs](#outputs)
|
- [@Outputs](#outputs)
|
||||||
- [The InputPasswordFlow](#the-inputpasswordflow)
|
- [The InputPasswordFlow](#the-inputpasswordflow)
|
||||||
|
- [Use Cases](#use-cases)
|
||||||
- [HTML - Form Fields](#html---form-fields)
|
- [HTML - Form Fields](#html---form-fields)
|
||||||
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
- [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)
|
- [Validation](#validation)
|
||||||
- [Submit Logic](#submit-logic)
|
- [Submit Logic](#submit-logic)
|
||||||
|
- [Submitting From a Parent Dialog Component](#submitting-from-a-parent-dialog-component)
|
||||||
- [Example](#example)
|
- [Example](#example)
|
||||||
|
|
||||||
<br />
|
<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
|
- `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
|
which form input elements will be displayed in the UI and which cryptographic keys will be created
|
||||||
and emitted.
|
and emitted. [Click here](#the-inputpasswordflow) to learn more about the different
|
||||||
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
`InputPasswordFlow` options.
|
||||||
create a master key.
|
|
||||||
|
**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**
|
**Optional**
|
||||||
|
|
||||||
|
These `@Inputs` are truly optional.
|
||||||
|
|
||||||
- `loading` - a boolean used to indicate that the parent component is performing some
|
- `loading` - a boolean used to indicate that the parent component is performing some
|
||||||
long-running/async operation and that the form should be disabled until the operation is complete.
|
long-running/async operation and that the form should be disabled until the operation is complete.
|
||||||
The primary button will also show a spinner if `loading` is true.
|
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
|
## `@Output()`'s
|
||||||
|
|
||||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
- `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.
|
- `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,
|
The parent component can listen for this event and take some custom action as needed (go back,
|
||||||
cancel, logout, etc.)
|
cancel, logout, etc.)
|
||||||
@@ -66,79 +87,100 @@ the parent component to act on those values as needed.
|
|||||||
## The `InputPasswordFlow`
|
## The `InputPasswordFlow`
|
||||||
|
|
||||||
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
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
|
### HTML - Form Fields
|
||||||
|
|
||||||
The `InputPasswordFlow` determines which form fields get displayed in the UI.
|
Click through the individual Stories in Storybook to see how the `InputPassswordFlow` determines
|
||||||
|
which form field UI elements get displayed.
|
||||||
**`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)
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
### TypeScript - Credential Generation
|
### TypeScript - Credential Generation
|
||||||
|
|
||||||
- The `AccountRegistration` and `SetInitialPasswordAuthedUser` flows involve a user setting their
|
- **`SetInitialPasswordAccountRegistration`** and **`SetInitialPasswordAuthedUser`**
|
||||||
password for the first time. Therefore on submit the component will only generate new credentials
|
- These flows involve a user setting their password for the first time. Therefore on submit the
|
||||||
(`newMasterKey`) and not current credentials (`currentMasterKey`).
|
component will only generate new credentials (`newMasterKey`) and not current credentials
|
||||||
- The `ChangePassword` and `ChangePasswordWithOptionalUserKeyRotation` flows both require the user
|
(`currentMasterKey`).<br /><br />
|
||||||
to enter a current password along with a new password. Therefore on submit the component will
|
- **`ChangePassword`** and **`ChangePasswordWithOptionalUserKeyRotation`**
|
||||||
generate current credentials (`currentMasterKey`) along with new credentials (`newMasterKey`).
|
- 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 />
|
<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
|
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:
|
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
|
- `SetInitialPasswordAccountRegistration` involves scenarios where we have no existing user, and
|
||||||
account `userId`**:
|
**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,
|
- `SetInitialPasswordAuthedUser` involves scenarios where we do have an existing and authed user,
|
||||||
and **thus an active account `userId`**:
|
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 presence or absence of an active account `userId` is important because it determines how we get
|
||||||
the correct `kdfConfig` prior to key generation:
|
the correct `kdfConfig` prior to key generation:
|
||||||
@@ -148,12 +190,12 @@ the correct `kdfConfig` prior to key generation:
|
|||||||
`userId`
|
`userId`
|
||||||
|
|
||||||
That said, we cannot mark the `userId` as a required via `@Input({ required: true })` because
|
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
|
`SetInitialPasswordAccountRegistration` flows will not have a `userId`. But we still want to require
|
||||||
`SetInitialPasswordAuthedUser` flow. Therefore the `InputPasswordComponent` has init logic that
|
a `userId` in a `SetInitialPasswordAuthedUser` flow. Therefore the `InputPasswordComponent` has init
|
||||||
ensures the following:
|
logic that ensures the following:
|
||||||
|
|
||||||
- If the passed down flow is `AccountRegistration`, require that the parent **MUST NOT** have passed
|
- If the passed down flow is `SetInitialPasswordAccountRegistration`, require that the parent **MUST
|
||||||
down a `userId`
|
NOT** have passed down a `userId`
|
||||||
- If the passed down flow is `SetInitialPasswordAuthedUser` require that the parent must also have
|
- If the passed down flow is `SetInitialPasswordAuthedUser` require that the parent must also have
|
||||||
passed down a `userId`
|
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 confirmed new password are the same
|
||||||
- The new password and password hint are NOT the same
|
- The new password and password hint are NOT the same
|
||||||
|
|
||||||
Additional submit logic validation ensures that:
|
|
||||||
|
|
||||||
- The new password adheres to any enforced master password policy options (that were passed down
|
|
||||||
from the parent)
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Submit Logic
|
## Submit Logic
|
||||||
@@ -182,9 +219,10 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
|||||||
|
|
||||||
1. Verifies inputs:
|
1. Verifies inputs:
|
||||||
- Checks that the current password is correct (if it was required in the flow)
|
- 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
|
- Checks that the new password is not weak or found in any breaches (if the user selected the
|
||||||
the checkbox)
|
checkbox)
|
||||||
- Checks that the new password meets any master password policy requirements enforced by an org
|
- 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`,
|
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
||||||
`newServerMasterKeyHash`, etc.)
|
`newServerMasterKeyHash`, etc.)
|
||||||
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
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
|
```typescript
|
||||||
export interface PasswordInputResult {
|
export interface PasswordInputResult {
|
||||||
// Properties starting with "current..." are included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
|
||||||
currentPassword?: string;
|
currentPassword?: string;
|
||||||
currentMasterKey?: MasterKey;
|
currentMasterKey?: MasterKey;
|
||||||
currentServerMasterKeyHash?: string;
|
currentServerMasterKeyHash?: string;
|
||||||
currentLocalMasterKeyHash?: string;
|
currentLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
newPasswordHint: string;
|
newPasswordHint?: string;
|
||||||
newMasterKey: MasterKey;
|
newMasterKey?: MasterKey;
|
||||||
newServerMasterKeyHash: string;
|
newServerMasterKeyHash?: string;
|
||||||
newLocalMasterKeyHash: string;
|
newLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
kdfConfig: KdfConfig;
|
kdfConfig?: KdfConfig;
|
||||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
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
|
# Example
|
||||||
|
|
||||||
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
|
|||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
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 { 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 { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { DialogService, ToastService } from "@bitwarden/components";
|
import { DialogService, ToastService } from "@bitwarden/components";
|
||||||
|
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||||
|
|
||||||
// FIXME: remove `/apps` import from `/libs`
|
// FIXME: remove `/apps` import from `/libs`
|
||||||
@@ -73,6 +75,7 @@ export default {
|
|||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useValue: {
|
useValue: {
|
||||||
launchUri: () => Promise.resolve(true),
|
launchUri: () => Promise.resolve(true),
|
||||||
|
copyToClipboard: () => true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -125,16 +128,31 @@ export default {
|
|||||||
showToast: action("ToastService.showToast"),
|
showToast: action("ToastService.showToast"),
|
||||||
} as Partial<ToastService>,
|
} as Partial<ToastService>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: PasswordGenerationServiceAbstraction,
|
||||||
|
useValue: {
|
||||||
|
getOptions: () => ({}),
|
||||||
|
generatePassword: () => "generated-password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ValidationService,
|
||||||
|
useValue: {
|
||||||
|
showError: () => ["validation error"],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
args: {
|
args: {
|
||||||
InputPasswordFlow: {
|
InputPasswordFlow: {
|
||||||
AccountRegistration: InputPasswordFlow.AccountRegistration,
|
SetInitialPasswordAccountRegistration:
|
||||||
|
InputPasswordFlow.SetInitialPasswordAccountRegistration,
|
||||||
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
SetInitialPasswordAuthedUser: InputPasswordFlow.SetInitialPasswordAuthedUser,
|
||||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||||
ChangePasswordWithOptionalUserKeyRotation:
|
ChangePasswordWithOptionalUserKeyRotation:
|
||||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||||
|
ChangePasswordDelegation: InputPasswordFlow.ChangePasswordDelegation,
|
||||||
},
|
},
|
||||||
userId: "1" as UserId,
|
userId: "1" as UserId,
|
||||||
email: "user@email.com",
|
email: "user@email.com",
|
||||||
@@ -154,12 +172,12 @@ export default {
|
|||||||
|
|
||||||
type Story = StoryObj<InputPasswordComponent>;
|
type Story = StoryObj<InputPasswordComponent>;
|
||||||
|
|
||||||
export const AccountRegistration: Story = {
|
export const SetInitialPasswordAccountRegistration: Story = {
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.AccountRegistration"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
></auth-input-password>
|
></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 = {
|
export const WithPolicies: Story = {
|
||||||
render: (args) => ({
|
render: (args) => ({
|
||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.SetInitialPasswordAuthedUser"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[userId]="userId"
|
|
||||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
`,
|
`,
|
||||||
@@ -224,7 +252,7 @@ export const SecondaryButton: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.AccountRegistration"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'cancel' }"
|
[secondaryButtonText]="{ key: 'cancel' }"
|
||||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||||
@@ -238,7 +266,7 @@ export const SecondaryButtonWithPlaceHolderText: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.AccountRegistration"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||||
@@ -252,7 +280,7 @@ export const InlineButton: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.AccountRegistration"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[inlineButtons]="true"
|
[inlineButtons]="true"
|
||||||
></auth-input-password>
|
></auth-input-password>
|
||||||
@@ -265,7 +293,7 @@ export const InlineButtons: Story = {
|
|||||||
props: args,
|
props: args,
|
||||||
template: `
|
template: `
|
||||||
<auth-input-password
|
<auth-input-password
|
||||||
[flow]="InputPasswordFlow.AccountRegistration"
|
[flow]="InputPasswordFlow.SetInitialPasswordAccountRegistration"
|
||||||
[email]="email"
|
[email]="email"
|
||||||
[secondaryButtonText]="{ key: 'cancel' }"
|
[secondaryButtonText]="{ key: 'cancel' }"
|
||||||
[inlineButtons]="true"
|
[inlineButtons]="true"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ export interface PasswordInputResult {
|
|||||||
currentLocalMasterKeyHash?: string;
|
currentLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
newPasswordHint: string;
|
newPasswordHint?: string;
|
||||||
newMasterKey: MasterKey;
|
newMasterKey?: MasterKey;
|
||||||
newServerMasterKeyHash: string;
|
newServerMasterKeyHash?: string;
|
||||||
newLocalMasterKeyHash: string;
|
newLocalMasterKeyHash?: string;
|
||||||
|
|
||||||
kdfConfig: KdfConfig;
|
kdfConfig?: KdfConfig;
|
||||||
rotateUserKey?: boolean;
|
rotateUserKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
|||||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
inputPasswordFlow = InputPasswordFlow.AccountRegistration;
|
inputPasswordFlow = InputPasswordFlow.SetInitialPasswordAccountRegistration;
|
||||||
loading = true;
|
loading = true;
|
||||||
submitting = false;
|
submitting = false;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -122,7 +122,11 @@ describe("DefaultSetPasswordJitService", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
credentials = {
|
credentials = {
|
||||||
...passwordInputResult,
|
newMasterKey: passwordInputResult.newMasterKey,
|
||||||
|
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
|
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||||
|
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
|
kdfConfig: passwordInputResult.kdfConfig,
|
||||||
orgSsoIdentifier,
|
orgSsoIdentifier,
|
||||||
orgId,
|
orgId,
|
||||||
resetPasswordAutoEnroll,
|
resetPasswordAutoEnroll,
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ export class SetPasswordJitComponent implements OnInit {
|
|||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
|
|
||||||
const credentials: SetPasswordCredentials = {
|
const credentials: SetPasswordCredentials = {
|
||||||
...passwordInputResult,
|
newMasterKey: passwordInputResult.newMasterKey,
|
||||||
|
newServerMasterKeyHash: passwordInputResult.newServerMasterKeyHash,
|
||||||
|
newLocalMasterKeyHash: passwordInputResult.newLocalMasterKeyHash,
|
||||||
|
newPasswordHint: passwordInputResult.newPasswordHint,
|
||||||
|
kdfConfig: passwordInputResult.kdfConfig,
|
||||||
orgSsoIdentifier: this.orgSsoIdentifier,
|
orgSsoIdentifier: this.orgSsoIdentifier,
|
||||||
orgId: this.orgId,
|
orgId: this.orgId,
|
||||||
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
resetPasswordAutoEnroll: this.resetPasswordAutoEnroll,
|
||||||
|
|||||||
Reference in New Issue
Block a user