mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-14 10:26:19 +01: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:
parent
ad0c460687
commit
07c2c2af20
@ -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"
|
||||
},
|
||||
|
@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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' }}"
|
||||
|
@ -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 {}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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' }}"
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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">!@#$%^&*</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>!@#$%^&*</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {}
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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 });
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ describe("State Migration Service", () => {
|
||||
lastSync: "LAST_SYNC",
|
||||
userId: userId,
|
||||
usesKeyConnector: false,
|
||||
forcePasswordReset: false,
|
||||
forcePasswordResetReason: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 } = {};
|
||||
|
@ -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":
|
||||
|
@ -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">
|
||||
|
Loading…
Reference in New Issue
Block a user