1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02: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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1057 additions and 503 deletions

View File

@ -203,7 +203,7 @@
},
"contactSupport": {
"message": "Contact Bitwarden support"
},
},
"sync": {
"message": "Sync"
},
@ -1464,6 +1464,15 @@
"setMasterPassword": {
"message": "Set master password"
},
"currentMasterPass": {
"message": "Current master password"
},
"newMasterPass": {
"message": "New master password"
},
"confirmNewMasterPass": {
"message": "Confirm new master password"
},
"masterPasswordPolicyInEffect": {
"message": "One or more organization policies require your master password to meet the following requirements:"
},
@ -1844,6 +1853,9 @@
"updateMasterPasswordWarning": {
"message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "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. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"resetPasswordPolicyAutoEnroll": {
"message": "Automatic enrollment"
},

View File

@ -1,6 +1,10 @@
import { AuthService as AbstractAuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import {
policyServiceFactory,
PolicyServiceInitOptions,
} from "../../../admin-console/background/service-factories/policy-service.factory";
import {
apiServiceFactory,
ApiServiceInitOptions,
@ -11,8 +15,8 @@ import {
CryptoServiceInitOptions,
} from "../../../background/service_factories/crypto-service.factory";
import {
EncryptServiceInitOptions,
encryptServiceFactory,
EncryptServiceInitOptions,
} from "../../../background/service_factories/encrypt-service.factory";
import {
environmentServiceFactory,
@ -24,20 +28,24 @@ import {
FactoryOptions,
} from "../../../background/service_factories/factory-options";
import {
I18nServiceInitOptions,
i18nServiceFactory,
I18nServiceInitOptions,
} from "../../../background/service_factories/i18n-service.factory";
import {
logServiceFactory,
LogServiceInitOptions,
} from "../../../background/service_factories/log-service.factory";
import {
MessagingServiceInitOptions,
messagingServiceFactory,
MessagingServiceInitOptions,
} from "../../../background/service_factories/messaging-service.factory";
import {
PlatformUtilsServiceInitOptions,
passwordGenerationServiceFactory,
PasswordGenerationServiceInitOptions,
} from "../../../background/service_factories/password-generation-service.factory";
import {
platformUtilsServiceFactory,
PlatformUtilsServiceInitOptions,
} from "../../../background/service_factories/platform-utils-service.factory";
import {
stateServiceFactory,
@ -45,11 +53,11 @@ import {
} from "../../../background/service_factories/state-service.factory";
import {
KeyConnectorServiceInitOptions,
keyConnectorServiceFactory,
KeyConnectorServiceInitOptions,
} from "./key-connector-service.factory";
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
import { TwoFactorServiceInitOptions, twoFactorServiceFactory } from "./two-factor-service.factory";
import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory";
import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory";
type AuthServiceFactoyOptions = FactoryOptions;
@ -65,7 +73,9 @@ export type AuthServiceInitOptions = AuthServiceFactoyOptions &
StateServiceInitOptions &
TwoFactorServiceInitOptions &
I18nServiceInitOptions &
EncryptServiceInitOptions;
EncryptServiceInitOptions &
PolicyServiceInitOptions &
PasswordGenerationServiceInitOptions;
export function authServiceFactory(
cache: { authService?: AbstractAuthService } & CachedServices,
@ -89,7 +99,9 @@ export function authServiceFactory(
await stateServiceFactory(cache, opts),
await twoFactorServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts)
await encryptServiceFactory(cache, opts),
await passwordGenerationServiceFactory(cache, opts),
await policyServiceFactory(cache, opts)
)
);
}

View File

@ -12,9 +12,12 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { BiometricErrors, BiometricErrorTypes } from "../../models/biometricErrors";
@ -42,6 +45,9 @@ export class LockComponent extends BaseLockComponent {
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
private authService: AuthService
) {
super(
@ -57,7 +63,10 @@ export class LockComponent extends BaseLockComponent {
apiService,
logService,
keyConnectorService,
ngZone
ngZone,
policyApiService,
policyService,
passwordGenerationService
);
this.successRoute = "/tabs/current";
this.isInitialLockScreen = (window as any).previousPopupUrl == null;

View File

@ -15,7 +15,7 @@
</header>
<main tabindex="-1">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ "updateMasterPasswordWarning" | i18n }}
{{ masterPasswordWarningText }}
</app-callout>
<app-callout
type="info"
@ -23,13 +23,35 @@
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box" *ngIf="requireCurrentPassword">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="currentMasterPassword">
{{ "currentMasterPass" | i18n }}
</label>
<input
id="currentMasterPassword"
type="password"
name="CurrentMasterPassword"
class="monospaced"
[(ngModel)]="verification.secret"
required
appInputVerbatim
/>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
{{ "newMasterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
@ -75,7 +97,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"

View File

@ -1,45 +1,9 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
policyService: PolicyService,
cryptoService: CryptoService,
stateService: StateService,
messagingService: MessagingService,
apiService: ApiService,
syncService: SyncService,
logService: LogService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
}
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@ -353,6 +353,11 @@ export default class MainBackground {
this.collectionService,
this.policyService
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService);
@ -378,7 +383,9 @@ export default class MainBackground {
this.stateService,
this.twoFactorService,
this.i18nService,
this.encryptService
this.encryptService,
this.passwordGenerationService,
this.policyService
);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
@ -446,11 +453,6 @@ export default class MainBackground {
this.organizationService,
this.eventUploadService
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.policyService,
this.stateService
);
this.totpService = new TotpService(this.cryptoFunctionService, this.logService);
this.autofillService = new AutofillService(
this.cipherService,

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);

View File

@ -34,6 +34,7 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
@ -350,8 +351,13 @@ export class AppComponent implements OnInit, OnDestroy {
const locked =
(await this.authService.getAuthStatus(message.userId)) ===
AuthenticationStatus.Locked;
const forcedPasswordReset =
(await this.stateService.getForcePasswordResetReason({ userId: message.userId })) !=
ForceResetPasswordReason.None;
if (locked) {
this.messagingService.send("locked", { userId: message.userId });
} else if (forcedPasswordReset) {
this.router.navigate(["update-temp-password"]);
} else {
this.messagingService.send("unlocked");
this.loading = true;

View File

@ -14,7 +14,10 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
const BroadcasterSubscriptionId = "LockComponent";
@ -39,6 +42,9 @@ export class LockComponent extends BaseLockComponent {
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService,
keyConnectorService: KeyConnectorService
) {
@ -55,7 +61,10 @@ export class LockComponent extends BaseLockComponent {
apiService,
logService,
keyConnectorService,
ngZone
ngZone,
policyApiService,
policyService,
passwordGenerationService
);
}

View File

@ -1,7 +1,7 @@
<form id="update-temp-password-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ "updateMasterPasswordWarning" | i18n }}
{{ masterPasswordWarningText }}
</app-callout>
<app-callout
type="info"
@ -9,13 +9,35 @@
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box" *ngIf="requireCurrentPassword">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="currentMasterPassword">
{{ "currentMasterPass" | i18n }}
</label>
<input
id="currentMasterPassword"
type="password"
name="currentMasterPassword"
class="monospaced"
[(ngModel)]="verification.secret"
required
appInputVerbatim
/>
</div>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
{{ "newMasterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
@ -62,7 +84,7 @@
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<label for="masterPasswordRetype">{{ "confirmNewMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"

View File

@ -1,45 +1,9 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
policyService: PolicyService,
cryptoService: CryptoService,
messagingService: MessagingService,
apiService: ApiService,
syncService: SyncService,
logService: LogService,
stateService: StateService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
}
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@ -1527,6 +1527,9 @@
"ssoCompleteRegistration": {
"message": "In order to complete logging in with SSO, please set a master password to access and protect your vault."
},
"currentMasterPass": {
"message": "Current master password"
},
"newMasterPass": {
"message": "New master password"
},
@ -1843,6 +1846,9 @@
"updateMasterPasswordWarning": {
"message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "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. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"hours": {
"message": "Hours"
},

View File

@ -3,81 +3,53 @@
</app-callout>
<div [formGroup]="data">
<div class="form-group">
<div class="form-check">
<bit-form-control>
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
<bit-label>{{ "turnOn" | i18n }}</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="enforceOnLogin" id="enforceOnLogin" />
<bit-label>{{ "enforceOnLoginDesc" | i18n }}</bit-label>
</bit-form-control>
<div class="tw-flex tw-space-x-4">
<bit-form-field class="tw-flex-auto">
<bit-label>{{ "minComplexityScore" | i18n }}</bit-label>
<bit-select formControlName="minComplexity" id="minComplexity">
<bit-option
*ngFor="let o of passwordScores"
[value]="o.value"
[label]="o.name"
></bit-option>
</bit-select>
</bit-form-field>
<bit-form-field class="tw-flex-auto">
<bit-label>{{ "minLength" | i18n }}</bit-label>
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
bitInput
type="number"
formControlName="minLength"
id="minLength"
[min]="MinPasswordLength"
/>
<label class="form-check-label" for="enabled">{{ "turnOn" | i18n }}</label>
</div>
</bit-form-field>
</div>
<div class="row">
<div class="col-6 form-group">
<label for="minComplexity">{{ "minComplexityScore" | i18n }}</label>
<select
id="minComplexity"
name="minComplexity"
formControlName="minComplexity"
class="form-control"
>
<option *ngFor="let o of passwordScores" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="col-6 form-group">
<label for="minLength">{{ "minLength" | i18n }}</label>
<input
id="minLength"
class="form-control"
type="number"
min="8"
name="minLength"
formControlName="minLength"
/>
</div>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requireUpper"
name="requireUpper"
formControlName="requireUpper"
/>
<label class="form-check-label" for="requireUpper">A-Z</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requireLower"
name="requireLower"
formControlName="requireLower"
/>
<label class="form-check-label" for="requireLower">a-z</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requireNumbers"
name="requireNumbers"
formControlName="requireNumbers"
/>
<label class="form-check-label" for="requireNumbers">0-9</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requireSpecial"
name="requireSpecial"
formControlName="requireSpecial"
/>
<label class="form-check-label" for="requireSpecial">!@#$%^&amp;*</label>
</div>
<bit-form-control class="!tw-mb-2">
<input type="checkbox" bitCheckbox formControlName="requireUpper" id="requireUpper" />
<bit-label>A-Z</bit-label>
</bit-form-control>
<bit-form-control class="!tw-mb-2">
<input type="checkbox" bitCheckbox formControlName="requireLower" id="requireLower" />
<bit-label>a-z</bit-label>
</bit-form-control>
<bit-form-control class="!tw-mb-2">
<input type="checkbox" bitCheckbox formControlName="requireNumbers" id="requireNumbers" />
<bit-label>0-9</bit-label>
</bit-form-control>
<bit-form-control>
<input type="checkbox" bitCheckbox formControlName="requireSpecial" id="requireSpecial" />
<bit-label>!@#$%^&amp;*</bit-label>
</bit-form-control>
</div>

View File

@ -1,9 +1,12 @@
import { Component } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { ControlsOf } from "@bitwarden/angular/types/controls-of";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { Utils } from "@bitwarden/common/misc/utils";
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
@ -19,20 +22,23 @@ export class MasterPasswordPolicy extends BasePolicy {
templateUrl: "master-password.component.html",
})
export class MasterPasswordPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
MinPasswordLength = Utils.minimumPasswordLength;
data: FormGroup<ControlsOf<MasterPasswordPolicyOptions>> = this.formBuilder.group({
minComplexity: [null],
minLength: [null],
requireUpper: [null],
requireLower: [null],
requireNumbers: [null],
requireSpecial: [null],
minLength: [this.MinPasswordLength, [Validators.min(Utils.minimumPasswordLength)]],
requireUpper: [false],
requireLower: [false],
requireNumbers: [false],
requireSpecial: [false],
enforceOnLogin: [false],
});
passwordScores: { name: string; value: number }[];
showKeyConnectorInfo = false;
constructor(
private formBuilder: UntypedFormBuilder,
private formBuilder: FormBuilder,
i18nService: I18nService,
private organizationService: OrganizationService
) {

View File

@ -79,6 +79,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen
}
const result = this.passwordGenerationService.passwordStrength(
c.login.password,
null,
userInput.length > 0 ? userInput : null
);
this.passwordStrengthCache.set(cacheKey, result.score);

View File

@ -24,6 +24,7 @@ import {
MenuModule,
MultiSelectModule,
RadioButtonModule,
SelectModule,
TableModule,
TabsModule,
ToggleGroupModule,
@ -103,6 +104,7 @@ import "./locales";
MenuModule,
MultiSelectModule,
RadioButtonModule,
SelectModule,
TableModule,
TabsModule,
ToggleGroupModule,

View File

@ -12,7 +12,10 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { RouterService } from "../app/core";
@ -35,7 +38,10 @@ export class LockComponent extends BaseLockComponent {
apiService: ApiService,
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone
ngZone: NgZone,
policyApiService: PolicyApiServiceAbstraction,
policyService: InternalPolicyService,
passwordGenerationService: PasswordGenerationServiceAbstraction
) {
super(
router,
@ -50,7 +56,10 @@ export class LockComponent extends BaseLockComponent {
apiService,
logService,
keyConnectorService,
ngZone
ngZone,
policyApiService,
policyService,
passwordGenerationService
);
}

View File

@ -155,7 +155,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
if (this.enforcedPasswordPolicyOptions != null) {
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput()
this.formGroup.value.email
);
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
@ -208,20 +208,4 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
}
await super.submit(false);
}
private getPasswordStrengthUserInput() {
const email = this.formGroup.value.email;
let userInput: string[] = [];
const atPosition = email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
return userInput;
}
}

View File

@ -1,107 +1,98 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-4">
<p class="lead text-center mb-4">{{ "updateMasterPassword" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<app-callout type="warning">{{ "updateMasterPasswordWarning" | i18n }} </app-callout>
<div class="form-group">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordHash"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint" />
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<div class="tw-mt-12 tw-flex tw-justify-center">
<div class="tw-w-1/3">
<h1 bitTypography="h1" class="tw-mb-4 tw-text-center">{{ "updateMasterPassword" | i18n }}</h1>
<div
class="tw-block tw-rounded tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-8"
>
<app-callout type="warning">{{ masterPasswordWarningText }} </app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<bit-form-field *ngIf="requireCurrentPassword">
<bit-label>{{ "currentMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="verification.secret"
name="currentMasterPassword"
id="currentMasterPassword"
[appAutofocus]="requireCurrentPassword"
/>
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field>
<div class="tw-mb-4">
<bit-form-field class="!tw-mb-1">
<bit-label>{{ "newMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="masterPassword"
name="masterPassword"
id="masterPassword"
/>
<button
type="button"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button>
</div>
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
<app-password-strength
[password]="masterPassword"
[email]="email"
[showText]="true"
(passwordStrengthResult)="getStrengthResult($event)"
>
</app-password-strength>
</div>
<bit-form-field>
<bit-label>{{ "confirmNewMasterPass" | i18n }}</bit-label>
<input
bitInput
type="password"
appInputVerbatim
required
[(ngModel)]="masterPasswordRetype"
name="masterPasswordRetype"
id="masterPasswordRetype"
/>
<button
type="button"
bitIconButton
bitSuffix
bitPasswordInputToggle
[(toggled)]="showPassword"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "masterPassHint" | i18n }}</bit-label>
<input bitInput type="text" [(ngModel)]="hint" name="hint" id="hint" />
<bit-hint>{{ "masterPassHintDesc" | i18n }}</bit-hint>
</bit-form-field>
<hr />
<div class="tw-flex tw-space-x-2">
<button
type="submit"
bitButton
[block]="true"
buttonType="primary"
[loading]="form.loading"
[disabled]="form.loading"
>
{{ "submit" | i18n }}
</button>
<button type="button" bitButton [block]="true" buttonType="secondary" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>

View File

@ -1,45 +1,9 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
policyService: PolicyService,
cryptoService: CryptoService,
messagingService: MessagingService,
apiService: ApiService,
logService: LogService,
stateService: StateService,
syncService: SyncService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
}
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {}

View File

@ -4813,6 +4813,9 @@
"masterPasswordInvalidWarning": {
"message": "Your master password does not meet the policy requirements of this organization. In order to join the organization, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"updateWeakMasterPasswordWarning": {
"message": "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. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour."
},
"maximumVaultTimeout": {
"message": "Vault timeout"
},
@ -5647,6 +5650,24 @@
}
}
},
"inputMinValue": {
"message": "Input value must be at least $MIN$.",
"placeholders": {
"min": {
"content": "$1",
"example": "8"
}
}
},
"inputMaxValue": {
"message": "Input value must not exceed $MAX$.",
"placeholders": {
"max": {
"content": "$1",
"example": "100"
}
}
},
"multipleInputEmails": {
"message": "1 or more emails are invalid"
},
@ -6714,5 +6735,8 @@
},
"notAvailableForFreeOrganization": {
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
},
"enforceOnLoginDesc": {
"message": "Require existing members to change their passwords"
}
}

View File

@ -1,6 +1,6 @@
import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject } from "rxjs";
import { firstValueFrom, Subject } from "rxjs";
import { concatMap, take, takeUntil } from "rxjs/operators";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@ -13,12 +13,18 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request";
import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response";
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/enums";
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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@Directive()
export class LockComponent implements OnInit, OnDestroy {
@ -28,18 +34,21 @@ export class LockComponent implements OnInit, OnDestroy {
email: string;
pinLock = false;
webVaultHostname = "";
formPromise: Promise<any>;
formPromise: Promise<MasterPasswordPolicyResponse>;
supportsBiometric: boolean;
biometricLock: boolean;
biometricText: string;
hideInput: boolean;
protected successRoute = "vault";
protected forcePasswordResetRoute = "update-temp-password";
protected onSuccessfulSubmit: () => Promise<void>;
private invalidPinAttempts = 0;
private pinSet: [boolean, boolean];
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
private destroy$ = new Subject<void>();
constructor(
@ -55,7 +64,10 @@ export class LockComponent implements OnInit, OnDestroy {
protected apiService: ApiService,
protected logService: LogService,
private keyConnectorService: KeyConnectorService,
protected ngZone: NgZone
protected ngZone: NgZone,
protected policyApiService: PolicyApiServiceAbstraction,
protected policyService: InternalPolicyService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction
) {}
async ngOnInit() {
@ -207,7 +219,8 @@ export class LockComponent implements OnInit, OnDestroy {
request.masterPasswordHash = serverKeyHash;
try {
this.formPromise = this.apiService.postAccountVerifyPassword(request);
await this.formPromise;
const response = await this.formPromise;
this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse(response);
passwordValid = true;
const localKeyHash = await this.cryptoService.hashPassword(
this.masterPassword,
@ -250,6 +263,27 @@ export class LockComponent implements OnInit, OnDestroy {
const disableFavicon = await this.stateService.getDisableFavicon();
await this.stateService.setDisableFavicon(!!disableFavicon);
this.messagingService.send("unlocked");
try {
// If we do not have any saved policies, attempt to load them from the service
if (this.enforcedMasterPasswordOptions == undefined) {
this.enforcedMasterPasswordOptions = await firstValueFrom(
this.policyService.masterPasswordPolicyOptions$()
);
}
if (this.requirePasswordChange()) {
await this.stateService.setForcePasswordResetReason(
ForceResetPasswordReason.WeakMasterPassword
);
this.router.navigate([this.forcePasswordResetRoute]);
return;
}
} catch (e) {
// Do not prevent unlock if there is an error evaluating policies
this.logService.error(e);
}
if (this.onSuccessfulSubmit != null) {
await this.onSuccessfulSubmit();
} else if (this.router != null) {
@ -282,4 +316,28 @@ export class LockComponent implements OnInit, OnDestroy {
webVaultUrl === "https://vault.bitwarden.com" ? "https://bitwarden.com" : webVaultUrl;
this.webVaultHostname = Utils.getHostname(vaultUrl);
}
/**
* Checks if the master password meets the enforced policy requirements
* If not, returns false
*/
private requirePasswordChange(): boolean {
if (
this.enforcedMasterPasswordOptions == undefined ||
!this.enforcedMasterPasswordOptions.enforceOnLogin
) {
return false;
}
const passwordStrength = this.passwordGenerationService.passwordStrength(
this.masterPassword,
this.email
)?.score;
return !this.policyService.evaluateMasterPassword(
passwordStrength,
this.masterPassword,
this.enforcedMasterPasswordOptions
);
}
}

View File

@ -16,6 +16,7 @@ import { ValidationService } from "@bitwarden/common/abstractions/validation.ser
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordlessLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-create-auth.request";
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
@ -134,7 +135,7 @@ export class LoginWithDeviceComponent
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else if (loginResponse.forcePasswordReset) {
} else if (loginResponse.forcePasswordReset != ForceResetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {

View File

@ -18,6 +18,7 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
import { Utils } from "@bitwarden/common/misc/utils";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
@ -144,7 +145,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
} else {
this.router.navigate([this.twoFactorRoute]);
}
} else if (response.forcePasswordReset) {
} else if (response.forcePasswordReset != ForceResetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {

View File

@ -11,6 +11,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { SsoLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials";
import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response";
import { Utils } from "@bitwarden/common/misc/utils";
@ -204,7 +205,7 @@ export class SsoComponent {
},
});
}
} else if (response.forcePasswordReset) {
} else if (response.forcePasswordReset !== ForceResetPasswordReason.None) {
if (this.onSuccessfulLoginForceResetNavigate != null) {
this.onSuccessfulLoginForceResetNavigate();
} else {

View File

@ -15,6 +15,7 @@ import { LoginService } from "@bitwarden/common/auth/abstractions/login.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 { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request";
import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service";
@ -208,7 +209,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
if (response.resetMasterPassword) {
this.successRoute = "set-password";
}
if (response.forcePasswordReset) {
if (response.forcePasswordReset !== ForceResetPasswordReason.None) {
this.successRoute = "update-temp-password";
}
if (this.onSuccessfulLoginNavigate != null) {

View File

@ -1,4 +1,5 @@
import { Directive } from "@angular/core";
import { Router } from "@angular/router";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
@ -7,12 +8,17 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction";
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 { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { UpdateTempPasswordRequest } from "@bitwarden/common/auth/models/request/update-temp-password.request";
import { EncString } from "@bitwarden/common/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
import { Verification } from "@bitwarden/common/types/verification";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ChangePasswordComponent as BaseChangePasswordComponent } from "./change-password.component";
@ -23,9 +29,18 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
key: string;
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showPassword = false;
reason: ForceResetPasswordReason = ForceResetPasswordReason.None;
verification: Verification = {
type: VerificationType.MasterPassword,
secret: "",
};
onSuccessfulChangePassword: () => Promise<any>;
get requireCurrentPassword(): boolean {
return this.reason === ForceResetPasswordReason.WeakMasterPassword;
}
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
@ -36,7 +51,9 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
private apiService: ApiService,
stateService: StateService,
private syncService: SyncService,
private logService: LogService
private logService: LogService,
private userVerificationService: UserVerificationService,
private router: Router
) {
super(
i18nService,
@ -51,7 +68,22 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
async ngOnInit() {
await this.syncService.fullSync(true);
super.ngOnInit();
this.reason = await this.stateService.getForcePasswordResetReason();
// If we somehow end up here without a reason, go back to the home page
if (this.reason == ForceResetPasswordReason.None) {
this.router.navigate(["/"]);
return;
}
await super.ngOnInit();
}
get masterPasswordWarningText(): string {
return this.reason == ForceResetPasswordReason.WeakMasterPassword
? this.i18nService.t("updateWeakMasterPasswordWarning")
: this.i18nService.t("updateMasterPasswordWarning");
}
togglePassword(confirmField: boolean) {
@ -104,14 +136,15 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
encKey: [SymmetricCryptoKey, EncString]
) {
try {
// Create request
const request = new UpdateTempPasswordRequest();
request.key = encKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
switch (this.reason) {
case ForceResetPasswordReason.AdminForcePasswordReset:
this.formPromise = this.updateTempPassword(masterPasswordHash, encKey);
break;
case ForceResetPasswordReason.WeakMasterPassword:
this.formPromise = this.updatePassword(masterPasswordHash, encKey);
break;
}
// Update user's password
this.formPromise = this.apiService.putUpdateTempPassword(request);
await this.formPromise;
this.platformUtilsService.showToast(
"success",
@ -119,6 +152,8 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
this.i18nService.t("updatedMasterPassword")
);
await this.stateService.setForcePasswordResetReason(ForceResetPasswordReason.None);
if (this.onSuccessfulChangePassword != null) {
this.onSuccessfulChangePassword();
} else {
@ -128,4 +163,30 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
this.logService.error(e);
}
}
private async updateTempPassword(
masterPasswordHash: string,
encKey: [SymmetricCryptoKey, EncString]
) {
const request = new UpdateTempPasswordRequest();
request.key = encKey[1].encryptedString;
request.newMasterPasswordHash = masterPasswordHash;
request.masterPasswordHint = this.hint;
return this.apiService.putUpdateTempPassword(request);
}
private async updatePassword(
newMasterPasswordHash: string,
encKey: [SymmetricCryptoKey, EncString]
) {
const request = await this.userVerificationService.buildRequest(
this.verification,
PasswordRequest
);
request.masterPasswordHint = this.hint;
request.newMasterPasswordHash = newMasterPasswordHash;
request.key = encKey[1].encryptedString;
return this.apiService.postPassword(request);
}
}

View File

@ -2,9 +2,11 @@ import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason";
@Injectable()
export class AuthGuard implements CanActivate {
@ -12,7 +14,8 @@ export class AuthGuard implements CanActivate {
private authService: AuthService,
private router: Router,
private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService
private keyConnectorService: KeyConnectorService,
private stateService: StateService
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
@ -37,6 +40,13 @@ export class AuthGuard implements CanActivate {
return this.router.createUrlTree(["/remove-password"]);
}
if (
!routerState.url.includes("update-temp-password") &&
(await this.stateService.getForcePasswordResetReason()) != ForceResetPasswordReason.None
) {
return this.router.createUrlTree(["/update-temp-password"]);
}
return true;
}
}

View File

@ -23,8 +23,8 @@ import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/comm
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/abstractions/organization-domain/org-domain-api.service.abstraction";
import {
OrgDomainServiceAbstraction,
OrgDomainInternalServiceAbstraction,
OrgDomainServiceAbstraction,
} from "@bitwarden/common/abstractions/organization-domain/org-domain.service.abstraction";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
@ -232,6 +232,9 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
EncryptService,
PasswordGenerationServiceAbstraction,
PolicyServiceAbstraction,
],
},
{

View File

@ -98,32 +98,13 @@ export class PasswordStrengthComponent implements OnChanges {
const strengthResult = this.passwordGenerationService.passwordStrength(
masterPassword,
this.getPasswordStrengthUserInput()
this.email,
this.name?.trim().toLowerCase().split(" ")
);
this.passwordStrengthResult.emit(strengthResult);
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
}
getPasswordStrengthUserInput() {
let userInput: string[] = [];
const email = this.email;
const name = this.name;
const atPosition = email?.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
if (name != null && name !== "") {
userInput = userInput.concat(name.trim().toLowerCase().split(" "));
}
return userInput;
}
setPasswordScoreText(color: string, text: string) {
color = color.slice(3);
this.passwordScoreColor.emit({ color: color, text: text });

View File

@ -161,6 +161,7 @@ describe("PolicyService", () => {
requireNumbers: false,
requireSpecial: false,
requireUpper: true,
enforceOnLogin: false,
});
});
@ -200,6 +201,7 @@ describe("PolicyService", () => {
requireNumbers: false,
requireSpecial: false,
requireUpper: false,
enforceOnLogin: false,
});
});
});

View File

@ -57,7 +57,7 @@ describe("State Migration Service", () => {
lastSync: "LAST_SYNC",
userId: userId,
usesKeyConnector: false,
forcePasswordReset: false,
forcePasswordResetReason: null,
},
};

View File

@ -80,6 +80,7 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
import { MasterPasswordPolicyResponse } from "../auth/models/response/master-password-policy.response";
import { PreloginResponse } from "../auth/models/response/prelogin.response";
import { RegisterResponse } from "../auth/models/response/register.response";
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
@ -187,7 +188,9 @@ export abstract class ApiService {
postAccountKeys: (request: KeysRequest) => Promise<any>;
postAccountVerifyEmail: () => Promise<any>;
postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise<any>;
postAccountVerifyPassword: (request: SecretVerificationRequest) => Promise<any>;
postAccountVerifyPassword: (
request: SecretVerificationRequest
) => Promise<MasterPasswordPolicyResponse>;
postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise<any>;
postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise<any>;
postAccountKdf: (request: KdfRequest) => Promise<any>;

View File

@ -8,6 +8,7 @@ import { ProviderData } from "../admin-console/models/data/provider.data";
import { Policy } from "../admin-console/models/domain/policy";
import { CollectionView } from "../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../auth/models/domain/kdf-config";
import { KdfType, ThemeType, UriMatchType } from "../enums";
import { EventData } from "../models/data/event.data";
@ -261,8 +262,11 @@ export abstract class StateService<T extends Account = Account> {
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordReset: (options?: StorageOptions) => Promise<boolean>;
setForcePasswordReset: (value: boolean, options?: StorageOptions) => Promise<void>;
getForcePasswordResetReason: (options?: StorageOptions) => Promise<ForceResetPasswordReason>;
setForcePasswordResetReason: (
value: ForceResetPasswordReason,
options?: StorageOptions
) => Promise<void>;
getInstalledVersion: (options?: StorageOptions) => Promise<string>;
setInstalledVersion: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;

View File

@ -7,6 +7,7 @@ import { PolicyResponse } from "../../models/response/policy.response";
export class PolicyApiServiceAbstraction {
getPolicy: (organizationId: string, type: PolicyType) => Promise<PolicyResponse>;
getPolicies: (organizationId: string) => Promise<ListResponse<PolicyResponse>>;
getPoliciesByToken: (
organizationId: string,
token: string,

View File

@ -1,3 +1,4 @@
import { MasterPasswordPolicyResponse } from "../../../auth/models/response/master-password-policy.response";
import Domain from "../../../models/domain/domain-base";
export class MasterPasswordPolicyOptions extends Domain {
@ -7,4 +8,26 @@ export class MasterPasswordPolicyOptions extends Domain {
requireLower = false;
requireNumbers = false;
requireSpecial = false;
/**
* Flag to indicate if the policy should be enforced on login.
* If true, and the user's password does not meet the policy requirements,
* the user will be forced to update their password.
*/
enforceOnLogin = false;
static fromResponse(policy: MasterPasswordPolicyResponse): MasterPasswordPolicyOptions {
if (policy == null) {
return null;
}
const options = new MasterPasswordPolicyOptions();
options.minComplexity = policy.minComplexity;
options.minLength = policy.minLength;
options.requireUpper = policy.requireUpper;
options.requireLower = policy.requireLower;
options.requireNumbers = policy.requireNumbers;
options.requireSpecial = policy.requireSpecial;
options.enforceOnLogin = policy.enforceOnLogin;
return options;
}
}

View File

@ -1,4 +1,4 @@
import { of, concatMap, BehaviorSubject, Observable, map } from "rxjs";
import { BehaviorSubject, concatMap, map, Observable, of } from "rxjs";
import { StateService } from "../../../abstractions/state.service";
import { Utils } from "../../../misc/utils";
@ -137,6 +137,10 @@ export class PolicyService implements InternalPolicyServiceAbstraction {
if (currentPolicy.data.requireSpecial) {
enforcedOptions.requireSpecial = true;
}
if (currentPolicy.data.enforceOnLogin) {
enforcedOptions.enforceOnLogin = true;
}
});
return enforcedOptions;

View File

@ -7,20 +7,24 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { Utils } from "../../misc/utils";
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
import { EncString } from "../../models/domain/enc-string";
import { PasswordGenerationService } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import { PasswordLogInStrategy } from "./password-login.strategy";
@ -50,7 +54,9 @@ const twoFactorProviderType = TwoFactorProviderType.Authenticator;
const twoFactorToken = "TWO_FACTOR_TOKEN";
const twoFactorRemember = true;
export function identityTokenResponseFactory() {
export function identityTokenResponseFactory(
masterPasswordPolicyResponse: MasterPasswordPolicyResponse = null
) {
return new IdentityTokenResponse({
ForcePasswordReset: false,
Kdf: kdf,
@ -63,6 +69,7 @@ export function identityTokenResponseFactory() {
refresh_token: refreshToken,
scope: "api offline_access",
token_type: "Bearer",
MasterPasswordPolicy: masterPasswordPolicyResponse,
});
}
@ -77,6 +84,8 @@ describe("LogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@ -92,6 +101,8 @@ describe("LogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
@ -107,6 +118,8 @@ describe("LogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
policyService,
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
@ -155,7 +168,7 @@ describe("LogInStrategy", () => {
const result = await passwordLogInStrategy.logIn(credentials);
expect(result).toEqual({
forcePasswordReset: true,
forcePasswordReset: ForceResetPasswordReason.AdminForcePasswordReset,
resetMasterPassword: true,
twoFactorProviders: null,
captchaSiteKey: "",

View File

@ -11,11 +11,12 @@ import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import {
UserApiLogInCredentials,
PasswordlessLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
UserApiLogInCredentials,
} from "../models/domain/log-in-credentials";
import { DeviceRequest } from "../models/request/identity-token/device.request";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
@ -26,6 +27,8 @@ import { IdentityCaptchaResponse } from "../models/response/identity-captcha.res
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
export abstract class LogInStrategy {
protected abstract tokenRequest: UserApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
protected captchaBypassToken: string = null;
@ -58,20 +61,21 @@ export abstract class LogInStrategy {
captchaResponse: string = null
): Promise<AuthResult> {
this.tokenRequest.setTwoFactor(twoFactor);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
protected async startLogIn(): Promise<AuthResult> {
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
this.twoFactorService.clearSelectedProvider();
const response = await this.apiService.postIdentityToken(this.tokenRequest);
if (response instanceof IdentityTwoFactorResponse) {
return this.processTwoFactorResponse(response);
return [await this.processTwoFactorResponse(response), response];
} else if (response instanceof IdentityCaptchaResponse) {
return this.processCaptchaResponse(response);
return [await this.processCaptchaResponse(response), response];
} else if (response instanceof IdentityTokenResponse) {
return this.processTokenResponse(response);
return [await this.processTokenResponse(response), response];
}
throw new Error("Invalid response object.");
@ -126,7 +130,10 @@ export abstract class LogInStrategy {
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
const result = new AuthResult();
result.resetMasterPassword = response.resetMasterPassword;
result.forcePasswordReset = response.forcePasswordReset;
if (response.forcePasswordReset) {
result.forcePasswordReset = ForceResetPasswordReason.AdminForcePasswordReset;
}
await this.saveAccountInformation(response);
@ -153,6 +160,7 @@ export abstract class LogInStrategy {
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
const result = new AuthResult();
result.twoFactorProviders = response.twoFactorProviders2;
this.twoFactorService.setProviders(response);
this.captchaBypassToken = response.captchaToken ?? null;
return result;

View File

@ -7,13 +7,19 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { HashPurpose } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PasswordGenerationService } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { MasterPasswordPolicyResponse } from "../models/response/master-password-policy.response";
import { identityTokenResponseFactory } from "./login.strategy.spec";
import { PasswordLogInStrategy } from "./password-login.strategy";
@ -28,6 +34,10 @@ const preloginKey = new SymmetricCryptoKey(
)
);
const deviceId = Utils.newGuid();
const masterPasswordPolicy = new MasterPasswordPolicyResponse({
EnforceOnLogin: true,
MinLength: 8,
});
describe("PasswordLogInStrategy", () => {
let cryptoService: MockProxy<CryptoService>;
@ -40,6 +50,8 @@ describe("PasswordLogInStrategy", () => {
let stateService: MockProxy<StateService>;
let twoFactorService: MockProxy<TwoFactorService>;
let authService: MockProxy<AuthService>;
let policyService: MockProxy<PolicyService>;
let passwordGenerationService: MockProxy<PasswordGenerationService>;
let passwordLogInStrategy: PasswordLogInStrategy;
let credentials: PasswordLogInCredentials;
@ -55,6 +67,8 @@ describe("PasswordLogInStrategy", () => {
stateService = mock<StateService>();
twoFactorService = mock<TwoFactorService>();
authService = mock<AuthService>();
policyService = mock<PolicyService>();
passwordGenerationService = mock<PasswordGenerationService>();
appIdService.getAppId.mockResolvedValue(deviceId);
tokenService.decodeToken.mockResolvedValue({});
@ -68,6 +82,8 @@ describe("PasswordLogInStrategy", () => {
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
.mockResolvedValue(localHashedPassword);
policyService.evaluateMasterPassword.mockReturnValue(true);
passwordLogInStrategy = new PasswordLogInStrategy(
cryptoService,
apiService,
@ -78,11 +94,15 @@ describe("PasswordLogInStrategy", () => {
logService,
stateService,
twoFactorService,
passwordGenerationService,
policyService,
authService
);
credentials = new PasswordLogInCredentials(email, masterPassword);
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
apiService.postIdentityToken.mockResolvedValue(
identityTokenResponseFactory(masterPasswordPolicy)
);
});
it("sends master password credentials to the server", async () => {
@ -110,4 +130,75 @@ describe("PasswordLogInStrategy", () => {
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
});
it("does not force the user to update their master password when there are no requirements", async () => {
apiService.postIdentityToken.mockResolvedValueOnce(identityTokenResponseFactory(null));
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).not.toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
});
it("does not force the user to update their master password when it meets requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 5 } as any);
policyService.evaluateMasterPassword.mockReturnValue(true);
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
});
it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const result = await passwordLogInStrategy.logIn(credentials);
expect(policyService.evaluateMasterPassword).toHaveBeenCalled();
expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
ForceResetPasswordReason.WeakMasterPassword
);
expect(result.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
});
it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => {
passwordGenerationService.passwordStrength.mockReturnValue({ score: 0 } as any);
policyService.evaluateMasterPassword.mockReturnValue(false);
const token2FAResponse = new IdentityTwoFactorResponse({
TwoFactorProviders: ["0"],
TwoFactorProviders2: { 0: null },
error: "invalid_grant",
error_description: "Two factor required.",
MasterPasswordPolicy: masterPasswordPolicy,
});
// First login request fails requiring 2FA
apiService.postIdentityToken.mockResolvedValueOnce(token2FAResponse);
const firstResult = await passwordLogInStrategy.logIn(credentials);
// Second login request succeeds
apiService.postIdentityToken.mockResolvedValueOnce(
identityTokenResponseFactory(masterPasswordPolicy)
);
const secondResult = await passwordLogInStrategy.logInTwoFactor(
{
provider: TwoFactorProviderType.Authenticator,
token: "123456",
remember: false,
},
""
);
// First login attempt should not save the force password reset options
expect(firstResult.forcePasswordReset).toEqual(ForceResetPasswordReason.None);
// Second login attempt should save the force password reset options and return in result
expect(stateService.setForcePasswordResetReason).toHaveBeenCalledWith(
ForceResetPasswordReason.WeakMasterPassword
);
expect(secondResult.forcePasswordReset).toEqual(ForceResetPasswordReason.WeakMasterPassword);
});
});

View File

@ -5,15 +5,22 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "../../admin-console/models/domain/master-password-policy-options";
import { HashPurpose } from "../../enums";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { AuthService } from "../abstractions/auth.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthResult } from "../models/domain/auth-result";
import { ForceResetPasswordReason } from "../models/domain/force-reset-password-reason";
import { PasswordLogInCredentials } from "../models/domain/log-in-credentials";
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
import { LogInStrategy } from "./login.strategy";
@ -31,6 +38,12 @@ export class PasswordLogInStrategy extends LogInStrategy {
private localHashedPassword: string;
private key: SymmetricCryptoKey;
/**
* Options to track if the user needs to update their password due to a password that does not meet an organization's
* master password policy.
*/
private forcePasswordResetReason: ForceResetPasswordReason = ForceResetPasswordReason.None;
constructor(
cryptoService: CryptoService,
apiService: ApiService,
@ -39,8 +52,10 @@ export class PasswordLogInStrategy extends LogInStrategy {
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
stateService: StateService,
protected stateService: StateService,
twoFactorService: TwoFactorService,
private passwordGenerationService: PasswordGenerationServiceAbstraction,
private policyService: PolicyService,
private authService: AuthService
) {
super(
@ -66,7 +81,19 @@ export class PasswordLogInStrategy extends LogInStrategy {
captchaResponse: string
): Promise<AuthResult> {
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
return super.logInTwoFactor(twoFactor);
const result = await super.logInTwoFactor(twoFactor);
// 2FA was successful, save the force update password options with the state service if defined
if (
!result.requiresTwoFactor &&
!result.requiresCaptcha &&
this.forcePasswordResetReason != ForceResetPasswordReason.None
) {
await this.stateService.setForcePasswordResetReason(this.forcePasswordResetReason);
result.forcePasswordReset = this.forcePasswordResetReason;
}
return result;
}
async logIn(credentials: PasswordLogInCredentials) {
@ -90,6 +117,52 @@ export class PasswordLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [authResult, identityResponse] = await this.startLogIn();
const masterPasswordPolicyOptions =
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
// The identity result can contain master password policies for the user's organizations
if (masterPasswordPolicyOptions?.enforceOnLogin) {
// If there is a policy active, evaluate the supplied password before its no longer in memory
const meetsRequirements = this.evaluateMasterPassword(
credentials,
masterPasswordPolicyOptions
);
if (!meetsRequirements) {
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
// Save the flag to this strategy for later use as the master password is about to pass out of scope
this.forcePasswordResetReason = ForceResetPasswordReason.WeakMasterPassword;
} else {
// Authentication was successful, save the force update password options with the state service
await this.stateService.setForcePasswordResetReason(
ForceResetPasswordReason.WeakMasterPassword
);
authResult.forcePasswordReset = ForceResetPasswordReason.WeakMasterPassword;
}
}
}
return authResult;
}
private getMasterPasswordPolicyOptionsFromResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse
): MasterPasswordPolicyOptions {
if (response == null || response instanceof IdentityCaptchaResponse) {
return null;
}
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
}
private evaluateMasterPassword(
{ masterPassword, email }: PasswordLogInCredentials,
options: MasterPasswordPolicyOptions
): boolean {
const passwordStrength = this.passwordGenerationService.passwordStrength(
masterPassword,
email
)?.score;
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
}
}

View File

@ -81,6 +81,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
);
this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
}

View File

@ -65,6 +65,7 @@ export class SsoLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
}

View File

@ -59,7 +59,8 @@ export class UserApiLogInStrategy extends LogInStrategy {
await this.buildDeviceRequest()
);
return this.startLogIn();
const [authResult] = await this.startLogIn();
return authResult;
}
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {

View File

@ -1,10 +1,12 @@
import { Utils } from "../../../misc/utils";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { ForceResetPasswordReason } from "./force-reset-password-reason";
export class AuthResult {
captchaSiteKey = "";
resetMasterPassword = false;
forcePasswordReset = false;
forcePasswordReset: ForceResetPasswordReason = ForceResetPasswordReason.None;
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
get requiresCaptcha() {

View File

@ -0,0 +1,17 @@
export enum ForceResetPasswordReason {
/**
* A password reset should not be forced.
*/
None,
/**
* Occurs when an organization admin forces a user to reset their password.
*/
AdminForcePasswordReset,
/**
* Occurs when a user logs in / unlocks their vault with a master password that does not meet an organization's
* master password policy that is enforced on login/unlock.
*/
WeakMasterPassword,
}

View File

@ -1,6 +1,8 @@
import { KdfType } from "../../../enums";
import { BaseResponse } from "../../../models/response/base.response";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
expiresIn: number;
@ -16,6 +18,7 @@ export class IdentityTokenResponse extends BaseResponse {
kdfMemory?: number;
kdfParallelism?: number;
forcePasswordReset: boolean;
masterPasswordPolicy: MasterPasswordPolicyResponse;
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
@ -37,5 +40,8 @@ export class IdentityTokenResponse extends BaseResponse {
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset");
this.apiUseKeyConnector = this.getResponseProperty("ApiUseKeyConnector");
this.keyConnectorUrl = this.getResponseProperty("KeyConnectorUrl");
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
}
}

View File

@ -1,10 +1,13 @@
import { BaseResponse } from "../../../models/response/base.response";
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
import { MasterPasswordPolicyResponse } from "./master-password-policy.response";
export class IdentityTwoFactorResponse extends BaseResponse {
twoFactorProviders: TwoFactorProviderType[];
twoFactorProviders2 = new Map<TwoFactorProviderType, { [key: string]: string }>();
captchaToken: string;
masterPasswordPolicy: MasterPasswordPolicyResponse;
constructor(response: any) {
super(response);
@ -19,5 +22,8 @@ export class IdentityTwoFactorResponse extends BaseResponse {
}
}
}
this.masterPasswordPolicy = new MasterPasswordPolicyResponse(
this.getResponseProperty("MasterPasswordPolicy")
);
}
}

View File

@ -0,0 +1,29 @@
import { BaseResponse } from "../../../models/response/base.response";
export class MasterPasswordPolicyResponse extends BaseResponse {
minComplexity: number;
minLength: number;
requireUpper: boolean;
requireLower: boolean;
requireNumbers: boolean;
requireSpecial: boolean;
/**
* Flag to indicate if the policy should be enforced on login.
* If true, and the user's password does not meet the policy requirements,
* the user will be forced to update their password.
*/
enforceOnLogin: boolean;
constructor(response: any) {
super(response);
this.minComplexity = this.getResponseProperty("MinComplexity");
this.minLength = this.getResponseProperty("MinLength");
this.requireUpper = this.getResponseProperty("RequireUpper");
this.requireLower = this.getResponseProperty("RequireLower");
this.requireNumbers = this.getResponseProperty("RequireNumbers");
this.requireSpecial = this.getResponseProperty("RequireSpecial");
this.enforceOnLogin = this.getResponseProperty("EnforceOnLogin");
}
}

View File

@ -10,12 +10,14 @@ import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { KdfType, KeySuffixOptions } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PreloginRequest } from "../../models/request/prelogin.request";
import { ErrorResponse } from "../../models/response/error.response";
import { AuthRequestPushNotification } from "../../models/response/notification.response";
import { PasswordGenerationServiceAbstraction } from "../../tools/generator/password";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
@ -29,10 +31,10 @@ import { UserApiLogInStrategy } from "../login-strategies/user-api-login.strateg
import { AuthResult } from "../models/domain/auth-result";
import { KdfConfig } from "../models/domain/kdf-config";
import {
UserApiLogInCredentials,
PasswordlessLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
UserApiLogInCredentials,
} from "../models/domain/log-in-credentials";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
@ -92,7 +94,9 @@ export class AuthService implements AuthServiceAbstraction {
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
protected encryptService: EncryptService
protected encryptService: EncryptService,
protected passwordGenerationService: PasswordGenerationServiceAbstraction,
protected policyService: PolicyService
) {}
async logIn(
@ -122,6 +126,8 @@ export class AuthService implements AuthServiceAbstraction {
this.logService,
this.stateService,
this.twoFactorService,
this.passwordGenerationService,
this.policyService,
this
);
break;

View File

@ -9,6 +9,7 @@ import { Policy } from "../../admin-console/models/domain/policy";
import { CollectionView } from "../../admin-console/models/view/collection.view";
import { AuthenticationStatus } from "../../auth/enums/authentication-status";
import { EnvironmentUrls } from "../../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../../auth/models/domain/force-reset-password-reason";
import { KdfType, UriMatchType } from "../../enums";
import { Utils } from "../../misc/utils";
import { GeneratedPasswordHistory } from "../../tools/generator/password";
@ -180,7 +181,7 @@ export class AccountProfile {
entityId?: string;
entityType?: string;
everBeenUnlocked?: boolean;
forcePasswordReset?: boolean;
forcePasswordResetReason?: ForceResetPasswordReason;
hasPremiumPersonally?: boolean;
hasPremiumFromOrganization?: boolean;
lastSync?: string;

View File

@ -87,6 +87,7 @@ import { IdentityCaptchaResponse } from "../auth/models/response/identity-captch
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
import { MasterPasswordPolicyResponse } from "../auth/models/response/master-password-policy.response";
import { PreloginResponse } from "../auth/models/response/prelogin.response";
import { RegisterResponse } from "../auth/models/response/register.response";
import { SsoPreValidateResponse } from "../auth/models/response/sso-pre-validate.response";
@ -421,8 +422,10 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("POST", "/accounts/verify-email-token", request, false, false);
}
postAccountVerifyPassword(request: SecretVerificationRequest): Promise<any> {
return this.send("POST", "/accounts/verify-password", request, true, false);
postAccountVerifyPassword(
request: SecretVerificationRequest
): Promise<MasterPasswordPolicyResponse> {
return this.send("POST", "/accounts/verify-password", request, true, true);
}
postAccountRecoverDelete(request: DeleteRecoverRequest): Promise<any> {

View File

@ -16,6 +16,7 @@ import { ProviderData } from "../admin-console/models/data/provider.data";
import { Policy } from "../admin-console/models/domain/policy";
import { CollectionView } from "../admin-console/models/view/collection.view";
import { EnvironmentUrls } from "../auth/models/domain/environment-urls";
import { ForceResetPasswordReason } from "../auth/models/domain/force-reset-password-reason";
import { KdfConfig } from "../auth/models/domain/kdf-config";
import { HtmlStorageLocation, KdfType, StorageLocation, ThemeType, UriMatchType } from "../enums";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
@ -1635,21 +1636,27 @@ export class StateService<
);
}
async getForcePasswordReset(options?: StorageOptions): Promise<boolean> {
async getForcePasswordResetReason(options?: StorageOptions): Promise<ForceResetPasswordReason> {
return (
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
?.profile?.forcePasswordReset ?? false
(
await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
)
)?.profile?.forcePasswordResetReason ?? ForceResetPasswordReason.None
);
}
async setForcePasswordReset(value: boolean, options?: StorageOptions): Promise<void> {
async setForcePasswordResetReason(
value: ForceResetPasswordReason,
options?: StorageOptions
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions())
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
);
account.profile.forcePasswordReset = value;
account.profile.forcePasswordResetReason = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions())
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())
);
}

View File

@ -17,7 +17,11 @@ export abstract class PasswordGenerationServiceAbstraction {
getHistory: () => Promise<GeneratedPasswordHistory[]>;
addHistory: (password: string) => Promise<void>;
clear: (userId?: string) => Promise<void>;
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
passwordStrength: (
password: string,
email?: string,
userInputs?: string[]
) => zxcvbn.ZXCVBNResult;
normalizeOptions: (
options: PasswordGeneratorOptions,
enforcedPolicyOptions: PasswordGeneratorPolicyOptions

View File

@ -387,14 +387,27 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
}
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
/**
* Calculates a password strength score using zxcvbn.
* @param password The password to calculate the strength of.
* @param emailInput An unparsed email address to use as user input.
* @param userInputs An array of additional user inputs to use when calculating the strength.
*/
passwordStrength(
password: string,
emailInput: string = null,
userInputs: string[] = null
): zxcvbn.ZXCVBNResult {
if (password == null || password.length === 0) {
return null;
}
let globalUserInputs = ["bitwarden", "bit", "warden"];
if (userInputs != null && userInputs.length > 0) {
globalUserInputs = globalUserInputs.concat(userInputs);
}
const globalUserInputs = [
"bitwarden",
"bit",
"warden",
...(userInputs ?? []),
...this.emailToUserInputs(emailInput),
];
// Use a hash set to get rid of any duplicate user inputs
const finalUserInputs = Array.from(new Set(globalUserInputs));
const result = zxcvbn(password, finalUserInputs);
@ -463,6 +476,27 @@ export class PasswordGenerationService implements PasswordGenerationServiceAbstr
this.sanitizePasswordLength(options, false);
}
/**
* Convert an email address into a list of user inputs for zxcvbn by
* taking the local part of the email address and splitting it into words.
* @param email
* @private
*/
private emailToUserInputs(email: string): string[] {
if (email == null || email.length === 0) {
return [];
}
const atPosition = email.indexOf("@");
if (atPosition < 0) {
return [];
}
return email
.substring(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/);
}
private capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View File

@ -15,6 +15,7 @@ import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { CollectionDetailsResponse } from "../../../admin-console/models/response/collection.response";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
import { ForceResetPasswordReason } from "../../../auth/models/domain/force-reset-password-reason";
import { sequentialize } from "../../../misc/sequentialize";
import { DomainsResponse } from "../../../models/response/domains.response";
import {
@ -311,9 +312,15 @@ export class SyncService implements SyncServiceAbstraction {
await this.stateService.setEmailVerified(response.emailVerified);
await this.stateService.setHasPremiumPersonally(response.premiumPersonally);
await this.stateService.setHasPremiumFromOrganization(response.premiumFromOrganization);
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
// The `forcePasswordReset` flag indicates an admin has reset the user's password and must be updated
if (response.forcePasswordReset) {
await this.stateService.setForcePasswordResetReason(
ForceResetPasswordReason.AdminForcePasswordReset
);
}
await this.syncProfileOrganizations(response);
const providers: { [id: string]: ProviderData } = {};

View File

@ -30,6 +30,10 @@ export class BitErrorComponent {
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
case "maxlength":
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
case "min":
return this.i18nService.t("inputMinValue", this.error[1]?.min);
case "max":
return this.i18nService.t("inputMaxValue", this.error[1]?.max);
case "forbiddenCharacters":
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
case "multipleEmails":

View File

@ -1,11 +1,11 @@
import {
AbstractControl,
FormBuilder,
FormsModule,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators,
FormBuilder,
} from "@angular/forms";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
@ -47,6 +47,8 @@ export default {
checkboxRequired: "Option is required",
inputRequired: "Input is required.",
inputEmail: "Input is not an email-address.",
inputMinValue: (min) => `Input value must be at least ${min}.`,
inputMaxValue: (max) => `Input value must not exceed ${max}.`,
});
},
},
@ -68,6 +70,7 @@ const exampleFormObj = fb.group({
country: [undefined as string | undefined, [Validators.required]],
terms: [false, [Validators.requiredTrue]],
updates: ["yes"],
age: [null, [Validators.min(0), Validators.max(150)]],
});
// Custom error message, `message` is shown as the error message
@ -103,6 +106,17 @@ const FullExampleTemplate: Story = (args) => ({
</bit-select>
</bit-form-field>
<bit-form-field>
<bit-label>Age</bit-label>
<input
bitInput
type="number"
formControlName="age"
min="0"
max="150"
/>
</bit-form-field>
<bit-form-control>
<bit-label>Agree to terms</bit-label>
<input type="checkbox" bitCheckbox formControlName="terms">