mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
315 lines
12 KiB
Plaintext
315 lines
12 KiB
Plaintext
import { Meta, Story } from "@storybook/addon-docs/blocks";
|
|
|
|
import * as stories from "./input-password.stories.ts";
|
|
|
|
<Meta of={stories} />
|
|
|
|
# InputPassword Component
|
|
|
|
The `InputPasswordComponent` allows a user to enter master password related credentials.
|
|
Specifically, it does the following:
|
|
|
|
1. Displays form fields in the UI
|
|
2. Validates form fields
|
|
3. Generates cryptographic properties based on the form inputs (e.g. `newMasterKey`,
|
|
`newServerMasterKeyHash`, etc.)
|
|
4. Emits the generated properties to the parent component
|
|
|
|
The `InputPasswordComponent` is central to our set/change password flows, allowing us to keep our
|
|
form UI and validation logic consistent. As such, it is intended for re-use in different set/change
|
|
password scenarios throughout the Bitwarden application. It is mostly presentational and simply
|
|
emits values rather than acting on them itself. It is the job of the parent component to act on
|
|
those values as needed.
|
|
|
|
<br />
|
|
|
|
## Table of Contents
|
|
|
|
- [@Inputs](#inputs)
|
|
- [@Outputs](#outputs)
|
|
- [The InputPasswordFlow](#the-inputpasswordflow)
|
|
- [Use Cases](#use-cases)
|
|
- [HTML - Form Fields](#html---form-fields)
|
|
- [TypeScript - Credential Generation](#typescript---credential-generation)
|
|
- [Difference between SetInitialPasswordAccountRegistration and SetInitialPasswordAuthedUser](#difference-between-setinitialpasswordaccountregistration-and-setinitialpasswordautheduser)
|
|
- [Validation](#validation)
|
|
- [Submit Logic](#submit-logic)
|
|
- [Submitting From a Parent Dialog Component](#submitting-from-a-parent-dialog-component)
|
|
- [Example](#example)
|
|
|
|
<br />
|
|
|
|
## `@Input()`'s
|
|
|
|
**Required**
|
|
|
|
- `flow` - the parent component must provide an `InputPasswordFlow`, which is used to determine
|
|
which form input elements will be displayed in the UI and which cryptographic keys will be created
|
|
and emitted. [Click here](#the-inputpasswordflow) to learn more about the different
|
|
`InputPasswordFlow` options.
|
|
|
|
**Optional (sometimes)**
|
|
|
|
These two `@Inputs` are optional on some flows, but required on others. Therefore these `@Inputs`
|
|
are not marked as `{ required: true }`, but there _is_ component logic that ensures (requires) that
|
|
the `email` and/or `userId` is present in certain flows, while not present in other flows.
|
|
|
|
- `email` - allows the `InputPasswordComponent` to generate a master key
|
|
- `userId` - allows the `InputPasswordComponent` to do things like get the user's `kdfConfig`,
|
|
verify that a current password is correct, and perform validation prior to user key rotation on
|
|
the parent
|
|
|
|
**Optional**
|
|
|
|
These `@Inputs` are truly optional.
|
|
|
|
- `loading` - a boolean used to indicate that the parent component is performing some
|
|
long-running/async operation and that the form should be disabled until the operation is complete.
|
|
The primary button will also show a spinner if `loading` is true.
|
|
- `masterPasswordPolicyOptions` - used to display and enforce master password policy requirements.
|
|
- `inlineButtons` - takes a boolean that determines if the button(s) should be displayed inline (as
|
|
opposed to full-width)
|
|
- `primaryButtonText` - takes a `Translation` object that can be used as button text
|
|
- `secondaryButtonText` - takes a `Translation` object that can be used as button text
|
|
|
|
<br />
|
|
|
|
## `@Output()`'s
|
|
|
|
- `onPasswordFormSubmit` - on form submit, emits a `PasswordInputResult` object
|
|
([see more below](#submit-logic)).
|
|
- `onSecondaryButtonClick` - on click, emits a notice that the secondary button has been clicked.
|
|
The parent component can listen for this event and take some custom action as needed (go back,
|
|
cancel, logout, etc.)
|
|
|
|
<br />
|
|
|
|
## The `InputPasswordFlow`
|
|
|
|
The `InputPasswordFlow` is a crucial and required `@Input` that influences both the HTML and the
|
|
credential generation logic of the component. It is important for the dev to understand when to use
|
|
each flow.
|
|
|
|
### 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
|
|
|
|
Click through the individual Stories in Storybook to see how the `InputPassswordFlow` determines
|
|
which form field UI elements get displayed.
|
|
|
|
<br />
|
|
|
|
### TypeScript - Credential Generation
|
|
|
|
- **`SetInitialPasswordAccountRegistration`** and **`SetInitialPasswordAuthedUser`**
|
|
- These flows involve a user setting their password for the first time. Therefore on submit the
|
|
component will only generate new credentials (`newMasterKey`) and not current credentials
|
|
(`currentMasterKey`).<br /><br />
|
|
- **`ChangePassword`** and **`ChangePasswordWithOptionalUserKeyRotation`**
|
|
- These flows both require the user to enter a current password along with a new password.
|
|
Therefore on submit the component will generate current credentials (`currentMasterKey`) along
|
|
with new credentials (`newMasterKey`).<br /><br />
|
|
- **`ChangePasswordDelegation`**
|
|
- This flow does not generate any credentials, but simply validates the new password and emits it
|
|
up to the parent.
|
|
|
|
<br />
|
|
|
|
### Difference between `SetInitialPasswordAccountRegistration` and `SetInitialPasswordAuthedUser`
|
|
|
|
These two flows are similar in that they display the same form fields and only generate new
|
|
credentials, but we need to keep them separate for the following reasons:
|
|
|
|
- `SetInitialPasswordAccountRegistration` involves scenarios where we have no existing user, and
|
|
**thus NO active account `userId`**:
|
|
- `SetInitialPasswordAuthedUser` involves scenarios where we do have an existing and authed user,
|
|
and **thus an active account `userId`**:
|
|
|
|
The presence or absence of an active account `userId` is important because it determines how we get
|
|
the correct `kdfConfig` prior to key generation:
|
|
|
|
- If there is no `userId` passed down from the parent, we default to `DEFAULT_KDF_CONFIG`
|
|
- If there is a `userId` passed down from the parent, we get the `kdfConfig` from state using the
|
|
`userId`
|
|
|
|
That said, we cannot mark the `userId` as a required via `@Input({ required: true })` because
|
|
`SetInitialPasswordAccountRegistration` flows will not have a `userId`. But we still want to require
|
|
a `userId` in a `SetInitialPasswordAuthedUser` flow. Therefore the `InputPasswordComponent` has init
|
|
logic that ensures the following:
|
|
|
|
- If the passed down flow is `SetInitialPasswordAccountRegistration`, require that the parent **MUST
|
|
NOT** have passed down a `userId`
|
|
- If the passed down flow is `SetInitialPasswordAuthedUser` require that the parent must also have
|
|
passed down a `userId`
|
|
|
|
If either of these checks is not met, the component throws to alert the dev of a mistake.
|
|
|
|
<br />
|
|
|
|
## Validation
|
|
|
|
Form validators ensure that:
|
|
|
|
- The current password and new password are NOT the same
|
|
- The new password and confirmed new password are the same
|
|
- The new password and password hint are NOT the same
|
|
|
|
<br />
|
|
|
|
## Submit Logic
|
|
|
|
When the form is submitted, the `InputPasswordComponent` does the following in order:
|
|
|
|
1. Verifies inputs:
|
|
- Checks that the current password is correct (if it was required in the flow)
|
|
- Checks that the new password is not weak or found in any breaches (if the user selected the
|
|
checkbox)
|
|
- Checks that the new password adheres to any enforced master password policies that were
|
|
optionally passed down by the parent
|
|
2. Uses the form inputs to create cryptographic properties (`newMasterKey`,
|
|
`newServerMasterKeyHash`, etc.)
|
|
3. Emits those cryptographic properties up to the parent (along with other values defined in
|
|
`PasswordInputResult`) to be used by the parent as needed.
|
|
|
|
```typescript
|
|
export interface PasswordInputResult {
|
|
currentPassword?: string;
|
|
currentMasterKey?: MasterKey;
|
|
currentServerMasterKeyHash?: string;
|
|
currentLocalMasterKeyHash?: string;
|
|
|
|
newPassword: string;
|
|
newPasswordHint?: string;
|
|
newMasterKey?: MasterKey;
|
|
newServerMasterKeyHash?: string;
|
|
newLocalMasterKeyHash?: string;
|
|
|
|
kdfConfig?: KdfConfig;
|
|
rotateUserKey?: boolean;
|
|
}
|
|
```
|
|
|
|
## Submitting From a Parent Dialog Component
|
|
|
|
Some of our set/change password flows use dialogs, such as Emergency Access Takeover and Account
|
|
Recovery. These are covered by the `ChangePasswordDelegation` flow. Because dialogs have their own
|
|
buttons, we don't want to display an additional Submit button in the `InputPasswordComponent` when
|
|
embedded in a dialog.
|
|
|
|
Therefore we do the following:
|
|
|
|
- The `InputPasswordComponent` hides the button in the UI and exposes its `submit()` method as a
|
|
public method.
|
|
- The parent dialog component can then access this method via `@ViewChild()`.
|
|
- When the user clicks the primary button on the parent dialog, we call the `submit()` method on the
|
|
`InputPasswordComponent`.
|
|
|
|
```html
|
|
<!-- emergency-access-takeover-dialog.component.html -->
|
|
|
|
<bit-dialog dialogSize="large">
|
|
<span bitDialogTitle><!-- ... --></span>
|
|
|
|
<div bitDialogContent>
|
|
<auth-input-password
|
|
[flow]="inputPasswordFlow"
|
|
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions"
|
|
(onPasswordFormSubmit)="handlePasswordFormSubmit($event)"
|
|
></auth-input-password>
|
|
</div>
|
|
|
|
<ng-container bitDialogFooter>
|
|
<button type="button" bitButton buttonType="primary" (click)="handlePrimaryButtonClick()">
|
|
{{ "save" | i18n }}
|
|
</button>
|
|
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
|
{{ "cancel" | i18n }}
|
|
</button>
|
|
</ng-container>
|
|
</bit-dialog>
|
|
```
|
|
|
|
```typescript
|
|
// emergency-access-takeover-dialog.component.ts
|
|
|
|
export class EmergencyAccessTakeoverDialogComponent implements OnInit {
|
|
@ViewChild(InputPasswordComponent)
|
|
inputPasswordComponent: InputPasswordComponent;
|
|
|
|
// ...
|
|
|
|
handlePrimaryButtonClick = async () => {
|
|
await this.inputPasswordComponent.submit();
|
|
};
|
|
|
|
async handlePasswordFormSubmit(passwordInputResult: PasswordInputResult) {
|
|
// ... run logic that handles the `PasswordInputResult` object emission
|
|
}
|
|
}
|
|
```
|
|
|
|
<br />
|
|
|
|
# Example
|
|
|
|
**`InputPasswordFlow.ChangePasswordWithOptionalUserKeyRotation`**
|
|
|
|
<Story of={stories.ChangePasswordWithOptionalUserKeyRotation} />
|