1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 05:13:29 +00:00

[AC-1070] Enforce master password policy on login (#4795)

* [EC-1070] Introduce flag for enforcing master password policy on login

* [EC-1070] Update master password policy form

Add the ability to toggle enforceOnLogin flag in web

* [EC-1070] Add API method to retrieve all policies for the current user

* [EC-1070] Refactor forcePasswordReset in state service to support more options

- Use an options class to provide a reason and optional organization id
- Use the OnDiskMemory storage location so the option persists between the same auth session

* [AC-1070] Retrieve single master password policy from identity token response

Additionally, store the policy in the login strategy for future use

* [EC-1070] Introduce master password evaluation in the password login strategy

- If a master password policy is returned from the identity result, evaluate the password.
- If the password does not meet the requirements, save the forcePasswordReset options
- Add support for 2FA by storing the results of the password evaluation on the login strategy instance
- Add unit tests to password login strategy

* [AC-1070] Modify admin password reset component to support update master password on login

- Modify the warning message to depend on the reason

- Use the forcePasswordResetOptions in the update temp password component

* [EC-1070] Require current master password when updating weak mp on login

- Inject user verification service to verify the user
- Conditionally show the current master password field only when updating a weak mp. Admin reset does not require the current master password.

* [EC-1070] Implement password policy check during vault unlock

Checking the master password during unlock is the only applicable place to enforce the master password policy check for SSO users.

* [EC-1070] CLI - Add ability to load MP policies on login

Inject policyApi and organization services into the login command

* [EC-1070] CLI - Refactor update temp password logic to support updating weak passwords

- Introduce new shared method for collecting a valid and confirmed master password from the CLI and generating a new encryption key
- Add separate methods for updating temp passwords and weak passwords.
- Utilize those methods during login flow if not using an API key

* [EC-1070] Add route guard to force password reset when required

* [AC-1070] Use master password policy from verify password response in lock component

* [EC-1070] Update labels in update password component

* [AC-1070] Fix policy service tests

* [AC-1070] CLI - Force sync before any password reset flow

Move up the call to sync the vault before attempting to collect a new master password. Ensures the master password policies are available.

* [AC-1070] Remove unused getAllPolicies method from policy api service

* [AC-1070] Fix missing enforceOnLogin copy in policy service

* [AC-1070] Include current master password on desktop/browser update password page templates

* [AC-1070] Check for forced password reset on account switch in Desktop

* [AC-1070] Rename WeakMasterPasswordOnLogin to WeakMasterPassword

* [AC-1070] Update AuthServiceInitOptions

* [AC-1070] Add None force reset password reason

* [AC-1070] Remove redundant ForcePasswordResetOptions class and replace with ForcePasswordResetReason enum

* [AC-1070] Rename ForceResetPasswordReason file

* [AC-1070] Simplify conditional

* [AC-1070] Refactor logic that saves password reset flag

* [AC-1070] Remove redundant constructors

* [AC-1070] Remove unnecessary state service call

* [AC-1070] Update master password policy component

- Use typed reactive form
- Use CL form components
- Remove bootstrap
- Update error component to support min/max
- Use Utils.minimumPasswordLength value for min value form validation

* [AC-1070] Cleanup leftover html comment

* [AC-1070] Remove overridden default values from MasterPasswordPolicyResponse

* [AC-1070] Hide current master password input in browser for admin password reset

* [AC-1070] Remove clientside user verification

* [AC-1070] Update temp password web component to use CL

- Use CL for form inputs in the Web component template
- Remove most of the bootstrap classes in the Web component template
- Use userVerificationService to build the password request
- Remove redundant current master password null check

* [AC-1070] Replace repeated user inputs email parsing helpers

- Update passwordStrength() method to accept an optional email argument that will be parsed into separate user inputs for use with zxcvbn
- Remove all other repeated getUserInput helper methods that parsed user emails and use the new passwordStrength signature

* [AC-1070] Fix broken login command after forcePasswordReset enum refactor

* [AC-1070] Reduce side effects in base login strategy

- Remove masterPasswordPolicy property from base login.strategy.ts
- Include an IdentityResponse in base startLogin() in addition to AuthResult
- Use the new IdentityResponse to parse the master password policy info only in the PasswordLoginStrategy

* [AC-1070] Cleanup password login strategy tests

* [AC-1070] Remove unused field

* [AC-1070] Strongly type postAccountVerifyPassword API service method

- Remove redundant verify master password response
- Use MasterPasswordPolicyResponse instead

* [AC-1070] Use ForceResetPassword.None during account switch check

* [AC-1070] Fix check for forcePasswordReset reason after addition of None

* [AC-1070] Redirect a user home if on the update temp password page without a reason

* [AC-1070] Use bit-select and bit-option

* [AC-1070] Reduce explicit form control definitions for readability

* [AC-1070] Import SelectModule in Shared web module

* [AC-1070] Add check for missing 'at' symbol

* [AC-1070] Remove redundant unpacking and null coalescing

* [AC-1070] Update passwordStrength signature and add jsdocs

* [AC-1070] Remove variable abbreviation

* [AC-1070] Restore Id attributes on form inputs

* [AC-1070] Clarify input value min/max error messages

* [AC-1070] Add input min/max value example to storybook

* [AC-1070] Add missing spinner to update temp password form

* [AC-1070] Add missing ids to form elements

* [AC-1070] Remove duplicate force sync and update comment

* [AC-1070] Switch backticks to quotation marks

---------

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
Shane Melton
2023-04-17 07:35:37 -07:00
committed by GitHub
parent ad0c460687
commit 07c2c2af20
60 changed files with 1057 additions and 503 deletions

View File

@@ -11,22 +11,28 @@ import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunc
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import {
UserApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
UserApiLogInCredentials,
} from "@bitwarden/common/auth/models/domain/log-in-credentials";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { NodeUtils } from "@bitwarden/common/misc/nodeUtils";
import { Utils } from "@bitwarden/common/misc/utils";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -55,6 +61,8 @@ export class LoginCommand {
protected twoFactorService: TwoFactorService,
protected syncService: SyncService,
protected keyConnectorService: KeyConnectorService,
protected policyApiService: PolicyApiServiceAbstraction,
protected orgService: OrganizationService,
protected logoutCallback: () => Promise<void>
) {}
@@ -306,9 +314,20 @@ export class LoginCommand {
);
}
// Handle Updating Temp Password if NOT using an API Key for authentication
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
return await this.updateTempPassword();
// Run full sync before handling success response or password reset flows (to get Master Password Policies)
await this.syncService.fullSync(true);
// Handle updating passwords if NOT using an API Key for authentication
if (
response.forcePasswordReset != ForceResetPasswordReason.None &&
clientId == null &&
clientSecret == null
) {
if (response.forcePasswordReset === ForceResetPasswordReason.AdminForcePasswordReset) {
return await this.updateTempPassword();
} else if (response.forcePasswordReset === ForceResetPasswordReason.WeakMasterPassword) {
return await this.updateWeakPassword(password);
}
}
return await this.handleSuccessResponse();
@@ -323,8 +342,6 @@ export class LoginCommand {
}
private async handleSuccessResponse(): Promise<Response> {
await this.syncService.fullSync(true);
const usesKeyConnector = await this.keyConnectorService.getUsesKeyConnector();
if (
@@ -357,7 +374,59 @@ export class LoginCommand {
return Response.success(res);
}
private async updateTempPassword(error?: string): Promise<Response> {
private async handleUpdatePasswordSuccessResponse(): Promise<Response> {
await this.logoutCallback();
this.authService.logOut(() => {
/* Do nothing */
});
const res = new MessageResponse(
"Your master password has been updated!",
"\n" + "You have been logged out and must log in again to access the vault."
);
return Response.success(res);
}
private async updateWeakPassword(currentPassword: string) {
// If no interaction available, alert user to use web vault
if (!this.canInteract) {
await this.logoutCallback();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(
new MessageResponse(
"Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now via the web vault. You have been logged out.",
null
)
);
}
try {
const { newPasswordHash, newEncKey, hint } = await this.collectNewMasterPasswordDetails(
"Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now."
);
const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashPassword(currentPassword, null);
request.masterPasswordHint = hint;
request.newMasterPasswordHash = newPasswordHash;
request.key = newEncKey[1].encryptedString;
await this.apiService.postPassword(request);
return await this.handleUpdatePasswordSuccessResponse();
} catch (e) {
await this.logoutCallback();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(e);
}
}
private async updateTempPassword() {
// If no interaction available, alert user to use web vault
if (!this.canInteract) {
await this.logoutCallback();
@@ -372,14 +441,49 @@ export class LoginCommand {
);
}
try {
const { newPasswordHash, newEncKey, hint } = await this.collectNewMasterPasswordDetails(
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now."
);
const request = new UpdateTempPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
request.masterPasswordHint = hint;
await this.apiService.putUpdateTempPassword(request);
return await this.handleUpdatePasswordSuccessResponse();
} catch (e) {
await this.logoutCallback();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(e);
}
}
/**
* Collect new master password and hint from the CLI. The collected password
* is validated against any applicable master password policies and a new encryption
* key is generated
* @param prompt - Message that is displayed during the initial prompt
* @param error
*/
private async collectNewMasterPasswordDetails(
prompt: string,
error?: string
): Promise<{
newPasswordHash: string;
newEncKey: [SymmetricCryptoKey, EncString];
hint?: string;
}> {
if (this.email == null || this.email === "undefined") {
this.email = await this.stateService.getEmail();
}
// Get New Master Password
const baseMessage =
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" +
"Master password: ";
const baseMessage = `${prompt}\n` + "Master password: ";
const firstMessage = error != null ? error + baseMessage : baseMessage;
const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
type: "password",
@@ -390,11 +494,12 @@ export class LoginCommand {
// Master Password Validation
if (masterPassword == null || masterPassword === "") {
return this.updateTempPassword("Master password is required.\n");
return this.collectNewMasterPasswordDetails(prompt, "Master password is required.\n");
}
if (masterPassword.length < Utils.minimumPasswordLength) {
return this.updateTempPassword(
return this.collectNewMasterPasswordDetails(
prompt,
`Master password must be at least ${Utils.minimumPasswordLength} characters long.\n`
);
}
@@ -402,9 +507,28 @@ export class LoginCommand {
// Strength & Policy Validation
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput()
this.email
);
const enforcedPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$()
);
// Verify master password meets policy requirements
if (
enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
masterPassword,
enforcedPolicyOptions
)
) {
return this.collectNewMasterPasswordDetails(
prompt,
"Your new master password does not meet the policy requirements.\n"
);
}
// Get New Master Password Re-type
const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")";
const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
@@ -416,7 +540,10 @@ export class LoginCommand {
// Re-type Validation
if (masterPassword !== masterPasswordRetype) {
return this.updateTempPassword("Master password confirmation does not match.\n");
return this.collectNewMasterPasswordDetails(
prompt,
"Master password confirmation does not match.\n"
);
}
// Get Hint (optional)
@@ -426,59 +553,25 @@ export class LoginCommand {
message: "Master Password Hint (optional):",
});
const masterPasswordHint = hint.input;
// Retrieve details for key generation
const enforcedPolicyOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$()
);
const kdf = await this.stateService.getKdfType();
const kdfConfig = await this.stateService.getKdfConfig();
if (
enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
strengthResult.score,
masterPassword,
enforcedPolicyOptions
)
) {
return this.updateTempPassword(
"Your new master password does not meet the policy requirements.\n"
);
}
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(
masterPassword,
this.email.trim().toLowerCase(),
kdf,
kdfConfig
);
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
try {
// Create new key and hash new password
const newKey = await this.cryptoService.makeKey(
masterPassword,
this.email.trim().toLowerCase(),
kdf,
kdfConfig
);
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
// Grab user's current enc key
const userEncKey = await this.cryptoService.getEncKey();
// Grab user's current enc key
const userEncKey = await this.cryptoService.getEncKey();
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Create new encKey for the User
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
// Create request
const request = new UpdateTempPasswordRequest();
request.key = newEncKey[1].encryptedString;
request.newMasterPasswordHash = newPasswordHash;
request.masterPasswordHint = masterPasswordHint;
// Update user's password
await this.apiService.putUpdateTempPassword(request);
return this.handleSuccessResponse();
} catch (e) {
await this.logoutCallback();
this.authService.logOut(() => {
/* Do nothing */
});
return Response.error(e);
}
return { newPasswordHash, newEncKey, hint: masterPasswordHint };
}
private async handleCaptchaRequired(
@@ -523,21 +616,6 @@ export class LoginCommand {
}
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
this.email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
return userInput;
}
private async apiClientId(): Promise<string> {
let clientId: string = null;

View File

@@ -6,9 +6,11 @@ import * as jsdom from "jsdom";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { CollectionService } from "@bitwarden/common/admin-console/services/collection.service";
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service";
import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
@@ -109,6 +111,7 @@ export class Main {
encryptService: EncryptServiceImplementation;
authService: AuthService;
policyService: PolicyService;
policyApiService: PolicyApiServiceAbstraction;
program: Program;
vaultProgram: VaultProgram;
sendProgram: SendProgram;
@@ -277,6 +280,12 @@ export class Main {
this.policyService = new PolicyService(this.stateService, this.organizationService);
this.policyApiService = new PolicyApiService(
this.policyService,
this.apiService,
this.stateService
);
this.keyConnectorService = new KeyConnectorService(
this.stateService,
this.cryptoService,
@@ -290,6 +299,12 @@ export class Main {
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.authService = new AuthService(
this.cryptoService,
this.apiService,
@@ -303,7 +318,9 @@ export class Main {
this.stateService,
this.twoFactorService,
this.i18nService,
this.encryptService
this.encryptService,
this.passwordGenerationService,
this.policyService
);
const lockedCallback = async () =>
@@ -352,12 +369,6 @@ export class Main {
async (expired: boolean) => await this.logout()
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
this.importApiService = new ImportApiService(this.apiService);

View File

@@ -152,6 +152,8 @@ export class Program {
this.main.twoFactorService,
this.main.syncService,
this.main.keyConnectorService,
this.main.policyApiService,
this.main.organizationService,
async () => await this.main.logout()
);
const response = await command.run(email, password, options);