mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
refactor(set-change-password): [Auth/PM-18206] Update InputPasswordComponent to handle multiple flows (#13745)
Updates the InputPasswordComponent so that it can eventually be used in multiple set/change password scenarios. Most importantly, this PR adds an InputPasswordFlow enum and @Input so that parent components can dictate which UI elements to show.
This commit is contained in:
@@ -4,14 +4,36 @@
|
||||
[policy]="masterPasswordPolicyOptions"
|
||||
></auth-password-callout>
|
||||
|
||||
<bit-form-field
|
||||
*ngIf="
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
"
|
||||
>
|
||||
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_current-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="currentPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
[(toggled)]="showPassword"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
|
||||
<div class="tw-mb-6">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "masterPassword" | i18n }}</bit-label>
|
||||
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_password"
|
||||
id="input-password-form_new-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="password"
|
||||
formControlName="newPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -30,7 +52,7 @@
|
||||
<tools-password-strength
|
||||
[showText]="true"
|
||||
[email]="email"
|
||||
[password]="formGroup.controls.password.value"
|
||||
[password]="formGroup.controls.newPassword.value"
|
||||
(passwordStrengthScore)="getPasswordStrengthScore($event)"
|
||||
></tools-password-strength>
|
||||
</div>
|
||||
@@ -38,10 +60,10 @@
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "confirmMasterPassword" | i18n }}</bit-label>
|
||||
<input
|
||||
id="input-password-form_confirmed-password"
|
||||
id="input-password-form_confirm-new-password"
|
||||
bitInput
|
||||
type="password"
|
||||
formControlName="confirmedPassword"
|
||||
formControlName="confirmNewPassword"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -65,16 +87,40 @@
|
||||
<bit-label>{{ "checkForBreaches" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="primary"
|
||||
[block]="btnBlock"
|
||||
[loading]="loading"
|
||||
<bit-form-control
|
||||
*ngIf="inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
>
|
||||
{{ buttonText || ("setMasterPassword" | i18n) }}
|
||||
</button>
|
||||
<input type="checkbox" bitCheckbox formControlName="rotateUserKey" />
|
||||
<bit-label>
|
||||
{{ "rotateAccountEncKey" | i18n }}
|
||||
<a
|
||||
href="https://bitwarden.com/help/account-encryption-key/#rotate-your-encryption-key"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
appA11yTitle="{{ 'impactOfRotatingYourEncryptionKey' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<div 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>
|
||||
|
||||
<button
|
||||
*ngIf="secondaryButtonText"
|
||||
type="button"
|
||||
bitButton
|
||||
bitFormButton
|
||||
buttonType="secondary"
|
||||
[loading]="loading"
|
||||
(click)="onSecondaryButtonClick.emit()"
|
||||
>
|
||||
{{ secondaryButtonTextStr }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<bit-error-summary *ngIf="showErrorSummary" [formGroup]="formGroup"></bit-error-summary>
|
||||
</form>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators } from "@angular/forms";
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
IconButtonModule,
|
||||
InputModule,
|
||||
ToastService,
|
||||
Translation,
|
||||
} from "@bitwarden/components";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -36,6 +35,29 @@ import { PasswordCalloutComponent } from "../password-callout/password-callout.c
|
||||
|
||||
import { PasswordInputResult } from "./password-input-result";
|
||||
|
||||
/**
|
||||
* Determines which form input elements will be displayed in the UI.
|
||||
*/
|
||||
export enum InputPasswordFlow {
|
||||
/**
|
||||
* - Input: New password
|
||||
* - Input: Confirm new password
|
||||
* - Input: Hint
|
||||
* - Checkbox: Check for breaches
|
||||
*/
|
||||
SetInitialPassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Input: Current password (as the first element in the UI)
|
||||
*/
|
||||
ChangePassword,
|
||||
/**
|
||||
* Everything above, plus:
|
||||
* - Checkbox: Rotate account encryption key (as the last element in the UI)
|
||||
*/
|
||||
ChangePasswordWithOptionalUserKeyRotation,
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "auth-input-password",
|
||||
@@ -54,44 +76,58 @@ import { PasswordInputResult } from "./password-input-result";
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class InputPasswordComponent {
|
||||
export class InputPasswordComponent implements OnInit {
|
||||
@Output() onPasswordFormSubmit = new EventEmitter<PasswordInputResult>();
|
||||
@Output() onSecondaryButtonClick = new EventEmitter<void>();
|
||||
|
||||
@Input({ required: true }) email: string;
|
||||
@Input() buttonText: string;
|
||||
@Input({ required: true }) inputPasswordFlow!: InputPasswordFlow;
|
||||
@Input({ required: true }) email!: string;
|
||||
|
||||
@Input() loading = false;
|
||||
@Input() masterPasswordPolicyOptions: MasterPasswordPolicyOptions | null = null;
|
||||
@Input() loading: boolean = false;
|
||||
@Input() btnBlock: boolean = true;
|
||||
|
||||
@Input() inlineButtons = false;
|
||||
@Input() primaryButtonText?: Translation;
|
||||
protected primaryButtonTextStr: string = "";
|
||||
@Input() secondaryButtonText?: Translation;
|
||||
protected secondaryButtonTextStr: string = "";
|
||||
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
private minHintLength = 0;
|
||||
protected maxHintLength = 50;
|
||||
protected minPasswordLength = Utils.minimumPasswordLength;
|
||||
protected minPasswordMsg = "";
|
||||
protected passwordStrengthScore: PasswordStrengthScore;
|
||||
protected passwordStrengthScore: PasswordStrengthScore = 0;
|
||||
protected showErrorSummary = false;
|
||||
protected showPassword = false;
|
||||
|
||||
protected formGroup = this.formBuilder.group(
|
||||
protected formGroup = this.formBuilder.nonNullable.group(
|
||||
{
|
||||
password: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmedPassword: ["", Validators.required],
|
||||
newPassword: ["", [Validators.required, Validators.minLength(this.minPasswordLength)]],
|
||||
confirmNewPassword: ["", Validators.required],
|
||||
hint: [
|
||||
"", // must be string (not null) because we check length in validation
|
||||
[Validators.minLength(this.minHintLength), Validators.maxLength(this.maxHintLength)],
|
||||
],
|
||||
checkForBreaches: true,
|
||||
checkForBreaches: [true],
|
||||
},
|
||||
{
|
||||
validators: [
|
||||
InputsFieldMatch.compareInputs(
|
||||
"doNotMatch",
|
||||
"currentPassword",
|
||||
"newPassword",
|
||||
this.i18nService.t("yourNewPasswordCannotBeTheSameAsYourCurrentPassword"),
|
||||
),
|
||||
InputsFieldMatch.compareInputs(
|
||||
"match",
|
||||
"password",
|
||||
"confirmedPassword",
|
||||
"newPassword",
|
||||
"confirmNewPassword",
|
||||
this.i18nService.t("masterPassDoesntMatch"),
|
||||
),
|
||||
InputsFieldMatch.compareInputs(
|
||||
"doNotMatch",
|
||||
"password",
|
||||
"newPassword",
|
||||
"hint",
|
||||
this.i18nService.t("hintEqualsPassword"),
|
||||
),
|
||||
@@ -109,6 +145,41 @@ export class InputPasswordComponent {
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"currentPassword",
|
||||
this.formBuilder.control("", Validators.required),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
// https://github.com/angular/angular/issues/48794
|
||||
(this.formGroup as FormGroup<any>).addControl(
|
||||
"rotateUserKey",
|
||||
this.formBuilder.control(false),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.primaryButtonText) {
|
||||
this.primaryButtonTextStr = this.i18nService.t(
|
||||
this.primaryButtonText.key,
|
||||
...(this.primaryButtonText?.placeholders ?? []),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.secondaryButtonText) {
|
||||
this.secondaryButtonTextStr = this.i18nService.t(
|
||||
this.secondaryButtonText.key,
|
||||
...(this.secondaryButtonText?.placeholders ?? []),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get minPasswordLengthMsg() {
|
||||
if (
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
@@ -132,10 +203,10 @@ export class InputPasswordComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.formGroup.controls.password.value;
|
||||
const newPassword = this.formGroup.controls.newPassword.value;
|
||||
|
||||
const passwordEvaluatedSuccessfully = await this.evaluatePassword(
|
||||
password,
|
||||
const passwordEvaluatedSuccessfully = await this.evaluateNewPassword(
|
||||
newPassword,
|
||||
this.passwordStrengthScore,
|
||||
this.formGroup.controls.checkForBreaches.value,
|
||||
);
|
||||
@@ -152,38 +223,55 @@ export class InputPasswordComponent {
|
||||
}
|
||||
|
||||
const masterKey = await this.keyService.makeMasterKey(
|
||||
password,
|
||||
newPassword,
|
||||
this.email.trim().toLowerCase(),
|
||||
kdfConfig,
|
||||
);
|
||||
|
||||
const masterKeyHash = await this.keyService.hashMasterKey(password, masterKey);
|
||||
const serverMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
newPassword,
|
||||
masterKey,
|
||||
HashPurpose.ServerAuthorization,
|
||||
);
|
||||
|
||||
const localMasterKeyHash = await this.keyService.hashMasterKey(
|
||||
password,
|
||||
newPassword,
|
||||
masterKey,
|
||||
HashPurpose.LocalAuthorization,
|
||||
);
|
||||
|
||||
this.onPasswordFormSubmit.emit({
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
localMasterKeyHash,
|
||||
kdfConfig,
|
||||
const passwordInputResult: PasswordInputResult = {
|
||||
newPassword,
|
||||
hint: this.formGroup.controls.hint.value,
|
||||
password,
|
||||
});
|
||||
kdfConfig,
|
||||
masterKey,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
};
|
||||
|
||||
if (
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePassword ||
|
||||
this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation
|
||||
) {
|
||||
passwordInputResult.currentPassword = this.formGroup.get("currentPassword")?.value;
|
||||
}
|
||||
|
||||
if (this.inputPasswordFlow === InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation) {
|
||||
passwordInputResult.rotateUserKey = this.formGroup.get("rotateUserKey")?.value;
|
||||
}
|
||||
|
||||
this.onPasswordFormSubmit.emit(passwordInputResult);
|
||||
};
|
||||
|
||||
// Returns true if the password passes all checks, false otherwise
|
||||
private async evaluatePassword(
|
||||
password: string,
|
||||
private async evaluateNewPassword(
|
||||
newPassword: string,
|
||||
passwordStrengthScore: PasswordStrengthScore,
|
||||
checkForBreaches: boolean,
|
||||
) {
|
||||
// Check if the password is breached, weak, or both
|
||||
const passwordIsBreached =
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(password));
|
||||
checkForBreaches && (await this.auditService.passwordLeaked(newPassword));
|
||||
|
||||
const passwordWeak = passwordStrengthScore != null && passwordStrengthScore < 3;
|
||||
|
||||
@@ -224,7 +312,7 @@ export class InputPasswordComponent {
|
||||
this.masterPasswordPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthScore,
|
||||
password,
|
||||
newPassword,
|
||||
this.masterPasswordPolicyOptions,
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as stories from "./input-password.stories.ts";
|
||||
|
||||
# InputPassword Component
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter a master password and hint. On submission it
|
||||
creates a master key, master key hash, and emits those values to the parent (along with the hint and
|
||||
default kdfConfig).
|
||||
The `InputPasswordComponent` allows a user to enter master password related credentials. On
|
||||
submission it creates a master key, master key hash, and emits those values to the parent (along
|
||||
with the other values found in `PasswordInputResult`).
|
||||
|
||||
The 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
|
||||
@@ -18,26 +18,66 @@ the parent component to act on those values as needed.
|
||||
|
||||
## `@Input()`'s
|
||||
|
||||
- `email` (**required**) - the parent component must provide an email so that the
|
||||
`InputPasswordComponent` can create a master key.
|
||||
- `buttonText` (optional) - an `i18n` translated string that can be used as button text (default
|
||||
text is "Set master password").
|
||||
- `masterPasswordPolicyOptions` (optional) - used to display and enforce master password policy
|
||||
requirements.
|
||||
**Required**
|
||||
|
||||
- `inputPasswordFlow` - the parent component must provide the correct flow, which is used to
|
||||
determine which form input elements will be displayed in the UI.
|
||||
- `email` - the parent component must provide an email so that the `InputPasswordComponent` can
|
||||
create a master key.
|
||||
|
||||
**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` true.
|
||||
- `masterPasswordPolicyOptions` - used to display and enforce master password policy requirements.
|
||||
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
||||
opposed to full-width)
|
||||
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
- `secondaryButtonText` - takes a `Translation` object that can be used as button text
|
||||
|
||||
## `@Output()`'s
|
||||
|
||||
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
||||
- `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.)
|
||||
|
||||
<br />
|
||||
|
||||
## Form Input Fields
|
||||
|
||||
The `InputPasswordComponent` allows a user to enter:
|
||||
The `InputPasswordComponent` can handle up to 6 different form input fields, depending on the
|
||||
`InputPasswordFlow` provided by the parent component.
|
||||
|
||||
1. Master password
|
||||
2. Master password confirmation
|
||||
3. Hint (optional)
|
||||
4. Chooses whether to check for password breaches (checkbox)
|
||||
**InputPasswordFlow.SetInitialPassword**
|
||||
|
||||
Validation ensures that the master password and confirmed master password are the same, and that the
|
||||
master password and hint values are not the same.
|
||||
- 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 />
|
||||
|
||||
## Validation
|
||||
|
||||
Validation ensures that:
|
||||
|
||||
- The current password and new password are NOT the same
|
||||
- The new password and confirmed new password are the same
|
||||
- The new password and password hint are NOT the same
|
||||
|
||||
<br />
|
||||
|
||||
@@ -57,19 +97,23 @@ When the form is submitted, the `InputPasswordComponent` does the following in o
|
||||
|
||||
```typescript
|
||||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string; // included if the flow is ChangePassword or ChangePasswordWithOptionalUserKeyRotation
|
||||
rotateUserKey?: boolean; // included if the flow is ChangePasswordWithOptionalUserKeyRotation
|
||||
}
|
||||
```
|
||||
|
||||
# Default Example
|
||||
# Example - InputPasswordFlow.SetInitialPassword
|
||||
|
||||
<Story of={stories.Default} />
|
||||
<Story of={stories.SetInitialPassword} />
|
||||
|
||||
<br />
|
||||
|
||||
# With Policy Requrements
|
||||
# Example - With Policy Requrements
|
||||
|
||||
<Story of={stories.WithPolicy} />
|
||||
<Story of={stories.WithPolicies} />
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
@@ -18,7 +16,7 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests";
|
||||
|
||||
import { InputPasswordComponent } from "./input-password.component";
|
||||
import { InputPasswordComponent, InputPasswordFlow } from "./input-password.component";
|
||||
|
||||
export default {
|
||||
title: "Auth/Input Password",
|
||||
@@ -62,7 +60,7 @@ export default {
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: {
|
||||
getPasswordStrength: (password) => {
|
||||
let score = 0;
|
||||
let score: number | null = null;
|
||||
if (password.length === 0) {
|
||||
score = null;
|
||||
} else if (password.length <= 4) {
|
||||
@@ -88,6 +86,12 @@ export default {
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
InputPasswordFlow: {
|
||||
SetInitialPassword: InputPasswordFlow.SetInitialPassword,
|
||||
ChangePassword: InputPasswordFlow.ChangePassword,
|
||||
ChangePasswordWithOptionalUserKeyRotation:
|
||||
InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation,
|
||||
},
|
||||
masterPasswordPolicyOptions: {
|
||||
minComplexity: 4,
|
||||
minLength: 14,
|
||||
@@ -96,25 +100,77 @@ export default {
|
||||
requireNumbers: true,
|
||||
requireSpecial: true,
|
||||
} as MasterPasswordPolicyOptions,
|
||||
argTypes: {
|
||||
onSecondaryButtonClick: { action: "onSecondaryButtonClick" },
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<InputPasswordComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
export const SetInitialPassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password></auth-input-password>
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithPolicy: Story = {
|
||||
export const ChangePassword: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"></auth-input-password>
|
||||
<auth-input-password [inputPasswordFlow]="InputPasswordFlow.ChangePassword"></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const ChangePasswordWithOptionalUserKeyRotation: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithPolicies: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SecondaryButton: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SecondaryButtonWithPlaceHolderText: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'backTo', placeholders: ['homepage'] }"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -123,7 +179,24 @@ export const InlineButton: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password [btnBlock]="false" [masterPasswordPolicyOptions]="masterPasswordPolicyOptions"></auth-input-password>
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[inlineButtons]="true"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const InlineButtons: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<auth-input-password
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[secondaryButtonText]="{ key: 'cancel' }"
|
||||
[inlineButtons]="true"
|
||||
(onSecondaryButtonClick)="onSecondaryButtonClick()"
|
||||
></auth-input-password>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -2,10 +2,12 @@ import { MasterKey } from "@bitwarden/common/types/key";
|
||||
import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface PasswordInputResult {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
newPassword: string;
|
||||
hint: string;
|
||||
password: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
masterKey: MasterKey;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
currentPassword?: string;
|
||||
rotateUserKey?: boolean;
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey;
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
hint: "hint",
|
||||
password: "password",
|
||||
newPassword: "password",
|
||||
};
|
||||
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
@@ -101,7 +101,7 @@ describe("DefaultRegistrationFinishService", () => {
|
||||
expect.objectContaining({
|
||||
email,
|
||||
emailVerificationToken: emailVerificationToken,
|
||||
masterPasswordHash: passwordInputResult.masterKeyHash,
|
||||
masterPasswordHash: passwordInputResult.serverMasterKeyHash,
|
||||
masterPasswordHint: passwordInputResult.hint,
|
||||
userSymmetricKey: userKeyEncString.encryptedString,
|
||||
userAsymmetricKeys: {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class DefaultRegistrationFinishService implements RegistrationFinishServi
|
||||
|
||||
const registerFinishRequest = new RegisterFinishRequest(
|
||||
email,
|
||||
passwordInputResult.masterKeyHash,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
passwordInputResult.hint,
|
||||
encryptedUserKey,
|
||||
userAsymmetricKeysRequest,
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
<auth-input-password
|
||||
*ngIf="!loading"
|
||||
[email]="email"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
[loading]="submitting"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
></auth-input-password>
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
PasswordLoginCredentials,
|
||||
} from "../../../common";
|
||||
import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service";
|
||||
import { InputPasswordComponent } from "../../input-password/input-password.component";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../../input-password/password-input-result";
|
||||
|
||||
import { RegistrationFinishService } from "./registration-finish.service";
|
||||
@@ -36,6 +39,8 @@ import { RegistrationFinishService } from "./registration-finish.service";
|
||||
export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
InputPasswordFlow = InputPasswordFlow;
|
||||
|
||||
loading = true;
|
||||
submitting = false;
|
||||
email: string;
|
||||
@@ -176,7 +181,7 @@ export class RegistrationFinishComponent implements OnInit, OnDestroy {
|
||||
try {
|
||||
const credentials = new PasswordLoginCredentials(
|
||||
this.email,
|
||||
passwordInputResult.password,
|
||||
passwordInputResult.newPassword,
|
||||
captchaBypassToken,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -112,11 +112,11 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
|
||||
passwordInputResult = {
|
||||
masterKey: masterKey,
|
||||
masterKeyHash: "masterKeyHash",
|
||||
serverMasterKeyHash: "serverMasterKeyHash",
|
||||
localMasterKeyHash: "localMasterKeyHash",
|
||||
hint: "hint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
password: "password",
|
||||
newPassword: "password",
|
||||
};
|
||||
|
||||
credentials = {
|
||||
@@ -131,7 +131,7 @@ describe("DefaultSetPasswordJitService", () => {
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
passwordInputResult.masterKeyHash,
|
||||
passwordInputResult.serverMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
passwordInputResult.hint,
|
||||
orgSsoIdentifier,
|
||||
|
||||
@@ -44,7 +44,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
async setPassword(credentials: SetPasswordCredentials): Promise<void> {
|
||||
const {
|
||||
masterKey,
|
||||
masterKeyHash,
|
||||
serverMasterKeyHash,
|
||||
localMasterKeyHash,
|
||||
hint,
|
||||
kdfConfig,
|
||||
@@ -70,7 +70,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
const [keyPair, keysRequest] = await this.makeKeyPairAndRequest(protectedUserKey);
|
||||
|
||||
const request = new SetPasswordRequest(
|
||||
masterKeyHash,
|
||||
serverMasterKeyHash,
|
||||
protectedUserKey[1].encryptedString,
|
||||
hint,
|
||||
orgSsoIdentifier,
|
||||
@@ -92,7 +92,7 @@ export class DefaultSetPasswordJitService implements SetPasswordJitService {
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
|
||||
if (resetPasswordAutoEnroll) {
|
||||
await this.handleResetPasswordAutoEnroll(masterKeyHash, orgId, userId);
|
||||
await this.handleResetPasswordAutoEnroll(serverMasterKeyHash, orgId, userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
</app-callout>
|
||||
|
||||
<auth-input-password
|
||||
[buttonText]="'createAccount' | i18n"
|
||||
[inputPasswordFlow]="InputPasswordFlow.SetInitialPassword"
|
||||
[primaryButtonText]="{ key: 'createAccount' }"
|
||||
[email]="email"
|
||||
[loading]="submitting"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
||||
|
||||
@@ -18,7 +18,10 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { ToastService } from "../../../../components/src/toast";
|
||||
import { InputPasswordComponent } from "../input-password/input-password.component";
|
||||
import {
|
||||
InputPasswordComponent,
|
||||
InputPasswordFlow,
|
||||
} from "../input-password/input-password.component";
|
||||
import { PasswordInputResult } from "../input-password/password-input-result";
|
||||
|
||||
import {
|
||||
@@ -33,6 +36,7 @@ import {
|
||||
imports: [CommonModule, InputPasswordComponent, JslibModule],
|
||||
})
|
||||
export class SetPasswordJitComponent implements OnInit {
|
||||
protected InputPasswordFlow = InputPasswordFlow;
|
||||
protected email: string;
|
||||
protected masterPasswordPolicyOptions: MasterPasswordPolicyOptions;
|
||||
protected orgId: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
export interface SetPasswordCredentials {
|
||||
masterKey: MasterKey;
|
||||
masterKeyHash: string;
|
||||
serverMasterKeyHash: string;
|
||||
localMasterKeyHash: string;
|
||||
kdfConfig: PBKDF2KdfConfig;
|
||||
hint: string;
|
||||
|
||||
Reference in New Issue
Block a user