mirror of
https://github.com/bitwarden/browser
synced 2025-12-14 07:13:32 +00:00
[AC-1070] Enforce master password policy on login (#4795)
* [EC-1070] Introduce flag for enforcing master password policy on login * [EC-1070] Update master password policy form Add the ability to toggle enforceOnLogin flag in web * [EC-1070] Add API method to retrieve all policies for the current user * [EC-1070] Refactor forcePasswordReset in state service to support more options - Use an options class to provide a reason and optional organization id - Use the OnDiskMemory storage location so the option persists between the same auth session * [AC-1070] Retrieve single master password policy from identity token response Additionally, store the policy in the login strategy for future use * [EC-1070] Introduce master password evaluation in the password login strategy - If a master password policy is returned from the identity result, evaluate the password. - If the password does not meet the requirements, save the forcePasswordReset options - Add support for 2FA by storing the results of the password evaluation on the login strategy instance - Add unit tests to password login strategy * [AC-1070] Modify admin password reset component to support update master password on login - Modify the warning message to depend on the reason - Use the forcePasswordResetOptions in the update temp password component * [EC-1070] Require current master password when updating weak mp on login - Inject user verification service to verify the user - Conditionally show the current master password field only when updating a weak mp. Admin reset does not require the current master password. * [EC-1070] Implement password policy check during vault unlock Checking the master password during unlock is the only applicable place to enforce the master password policy check for SSO users. * [EC-1070] CLI - Add ability to load MP policies on login Inject policyApi and organization services into the login command * [EC-1070] CLI - Refactor update temp password logic to support updating weak passwords - Introduce new shared method for collecting a valid and confirmed master password from the CLI and generating a new encryption key - Add separate methods for updating temp passwords and weak passwords. - Utilize those methods during login flow if not using an API key * [EC-1070] Add route guard to force password reset when required * [AC-1070] Use master password policy from verify password response in lock component * [EC-1070] Update labels in update password component * [AC-1070] Fix policy service tests * [AC-1070] CLI - Force sync before any password reset flow Move up the call to sync the vault before attempting to collect a new master password. Ensures the master password policies are available. * [AC-1070] Remove unused getAllPolicies method from policy api service * [AC-1070] Fix missing enforceOnLogin copy in policy service * [AC-1070] Include current master password on desktop/browser update password page templates * [AC-1070] Check for forced password reset on account switch in Desktop * [AC-1070] Rename WeakMasterPasswordOnLogin to WeakMasterPassword * [AC-1070] Update AuthServiceInitOptions * [AC-1070] Add None force reset password reason * [AC-1070] Remove redundant ForcePasswordResetOptions class and replace with ForcePasswordResetReason enum * [AC-1070] Rename ForceResetPasswordReason file * [AC-1070] Simplify conditional * [AC-1070] Refactor logic that saves password reset flag * [AC-1070] Remove redundant constructors * [AC-1070] Remove unnecessary state service call * [AC-1070] Update master password policy component - Use typed reactive form - Use CL form components - Remove bootstrap - Update error component to support min/max - Use Utils.minimumPasswordLength value for min value form validation * [AC-1070] Cleanup leftover html comment * [AC-1070] Remove overridden default values from MasterPasswordPolicyResponse * [AC-1070] Hide current master password input in browser for admin password reset * [AC-1070] Remove clientside user verification * [AC-1070] Update temp password web component to use CL - Use CL for form inputs in the Web component template - Remove most of the bootstrap classes in the Web component template - Use userVerificationService to build the password request - Remove redundant current master password null check * [AC-1070] Replace repeated user inputs email parsing helpers - Update passwordStrength() method to accept an optional email argument that will be parsed into separate user inputs for use with zxcvbn - Remove all other repeated getUserInput helper methods that parsed user emails and use the new passwordStrength signature * [AC-1070] Fix broken login command after forcePasswordReset enum refactor * [AC-1070] Reduce side effects in base login strategy - Remove masterPasswordPolicy property from base login.strategy.ts - Include an IdentityResponse in base startLogin() in addition to AuthResult - Use the new IdentityResponse to parse the master password policy info only in the PasswordLoginStrategy * [AC-1070] Cleanup password login strategy tests * [AC-1070] Remove unused field * [AC-1070] Strongly type postAccountVerifyPassword API service method - Remove redundant verify master password response - Use MasterPasswordPolicyResponse instead * [AC-1070] Use ForceResetPassword.None during account switch check * [AC-1070] Fix check for forcePasswordReset reason after addition of None * [AC-1070] Redirect a user home if on the update temp password page without a reason * [AC-1070] Use bit-select and bit-option * [AC-1070] Reduce explicit form control definitions for readability * [AC-1070] Import SelectModule in Shared web module * [AC-1070] Add check for missing 'at' symbol * [AC-1070] Remove redundant unpacking and null coalescing * [AC-1070] Update passwordStrength signature and add jsdocs * [AC-1070] Remove variable abbreviation * [AC-1070] Restore Id attributes on form inputs * [AC-1070] Clarify input value min/max error messages * [AC-1070] Add input min/max value example to storybook * [AC-1070] Add missing spinner to update temp password form * [AC-1070] Add missing ids to form elements * [AC-1070] Remove duplicate force sync and update comment * [AC-1070] Switch backticks to quotation marks --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
|
||||
this.tokenRequest.setPasswordlessAccessCode(credentials.authRequestId);
|
||||
return this.startLogIn();
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
return this.startLogIn();
|
||||
const [authResult] = await this.startLogIn();
|
||||
return authResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 } = {};
|
||||
|
||||
Reference in New Issue
Block a user