Merge pull request #9791 from ArtemDzhereleiko/AD/imp/security-settings/password-length
Improvement for security settings
This commit is contained in:
commit
614d64dda2
@ -51,12 +51,13 @@
|
|||||||
<fieldset class="fields-group">
|
<fieldset class="fields-group">
|
||||||
<legend class="group-title" translate>admin.password-policy</legend>
|
<legend class="group-title" translate>admin.password-policy</legend>
|
||||||
<section formGroupName="passwordPolicy">
|
<section formGroupName="passwordPolicy">
|
||||||
<mat-form-field class="mat-block">
|
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||||
|
<mat-form-field fxFlex class="mat-block">
|
||||||
<mat-label translate>admin.minimum-password-length</mat-label>
|
<mat-label translate>admin.minimum-password-length</mat-label>
|
||||||
<input matInput type="number"
|
<input matInput type="number"
|
||||||
formControlName="minimumLength"
|
formControlName="minimumLength"
|
||||||
step="1"
|
step="1"
|
||||||
min="5"
|
min="6"
|
||||||
max="50"
|
max="50"
|
||||||
required/>
|
required/>
|
||||||
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
|
||||||
@ -69,6 +70,18 @@
|
|||||||
{{ 'admin.minimum-password-length-range' | translate }}
|
{{ 'admin.minimum-password-length-range' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
|
||||||
|
<mat-label translate>admin.maximum-password-length</mat-label>
|
||||||
|
<input matInput type="number" formControlName="maximumLength" step="1" min="6"/>
|
||||||
|
<mat-hint></mat-hint>
|
||||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('min')">
|
||||||
|
{{ 'admin.maximum-password-length-min' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.maximumLength').hasError('lessMin')">
|
||||||
|
{{ 'admin.maximum-password-length-less-min' | translate }}
|
||||||
|
</mat-error>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||||
<mat-form-field fxFlex class="mat-block">
|
<mat-form-field fxFlex class="mat-block">
|
||||||
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
|
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
|
||||||
@ -140,9 +153,16 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<mat-checkbox formControlName="allowWhitespaces" style="margin-bottom: 16px">
|
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
|
||||||
|
<mat-checkbox fxFlex formControlName="allowWhitespaces" style="margin-bottom: 16px">
|
||||||
<mat-label translate>admin.allow-whitespace</mat-label>
|
<mat-label translate>admin.allow-whitespace</mat-label>
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
|
<mat-checkbox fxFlex formControlName="forceUserToResetPasswordIfNotValid" style="margin-bottom: 16px">
|
||||||
|
<mat-label tb-hint-tooltip-icon="{{'admin.force-reset-password-if-no-valid-hint' | translate}}">
|
||||||
|
{{'admin.force-reset-password-if-no-valid' | translate}}
|
||||||
|
</mat-label>
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">
|
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">
|
||||||
|
|||||||
@ -19,7 +19,14 @@ import { Store } from '@ngrx/store';
|
|||||||
import { AppState } from '@core/core.state';
|
import { AppState } from '@core/core.state';
|
||||||
import { PageComponent } from '@shared/components/page.component';
|
import { PageComponent } from '@shared/components/page.component';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms';
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
UntypedFormBuilder,
|
||||||
|
UntypedFormControl,
|
||||||
|
UntypedFormGroup, ValidationErrors,
|
||||||
|
ValidatorFn,
|
||||||
|
Validators
|
||||||
|
} from '@angular/forms';
|
||||||
import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
|
import { JwtSettings, SecuritySettings } from '@shared/models/settings.models';
|
||||||
import { AdminService } from '@core/http/admin.service';
|
import { AdminService } from '@core/http/admin.service';
|
||||||
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
import { HasConfirmForm } from '@core/guards/confirm-on-exit.guard';
|
||||||
@ -28,7 +35,10 @@ import { randomAlphanumeric } from '@core/utils';
|
|||||||
import { AuthService } from '@core/auth/auth.service';
|
import { AuthService } from '@core/auth/auth.service';
|
||||||
import { DialogService } from '@core/services/dialog.service';
|
import { DialogService } from '@core/services/dialog.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, of } from 'rxjs';
|
import { forkJoin, Observable, of } from 'rxjs';
|
||||||
|
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||||
|
import { AlarmInfo } from '@shared/models/alarm.models';
|
||||||
|
import { QueueProcessingStrategyTypes, QueueProcessingStrategyTypesMap } from '@shared/models/queue.models';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-security-settings',
|
selector: 'tb-security-settings',
|
||||||
@ -67,14 +77,16 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
|||||||
userLockoutNotificationEmail: ['', []],
|
userLockoutNotificationEmail: ['', []],
|
||||||
passwordPolicy: this.fb.group(
|
passwordPolicy: this.fb.group(
|
||||||
{
|
{
|
||||||
minimumLength: [null, [Validators.required, Validators.min(5), Validators.max(50)]],
|
minimumLength: [null, [Validators.required, Validators.min(6), Validators.max(50)]],
|
||||||
|
maximumLength: [null, [Validators.min(6), this.maxPasswordValidation()]],
|
||||||
minimumUppercaseLetters: [null, Validators.min(0)],
|
minimumUppercaseLetters: [null, Validators.min(0)],
|
||||||
minimumLowercaseLetters: [null, Validators.min(0)],
|
minimumLowercaseLetters: [null, Validators.min(0)],
|
||||||
minimumDigits: [null, Validators.min(0)],
|
minimumDigits: [null, Validators.min(0)],
|
||||||
minimumSpecialCharacters: [null, Validators.min(0)],
|
minimumSpecialCharacters: [null, Validators.min(0)],
|
||||||
passwordExpirationPeriodDays: [null, Validators.min(0)],
|
passwordExpirationPeriodDays: [null, Validators.min(0)],
|
||||||
passwordReuseFrequencyDays: [null, Validators.min(0)],
|
passwordReuseFrequencyDays: [null, Validators.min(0)],
|
||||||
allowWhitespaces: [true]
|
allowWhitespaces: [true],
|
||||||
|
forceUserToResetPasswordIfNotValid: [false]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -113,6 +125,18 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
|||||||
})).subscribe(() => {});
|
})).subscribe(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private maxPasswordValidation(): ValidatorFn {
|
||||||
|
return (control: AbstractControl): ValidationErrors | null => {
|
||||||
|
const value: string = control.value;
|
||||||
|
if (value) {
|
||||||
|
if (value < control.parent.value?.minimumLength) {
|
||||||
|
return {lessMin: true};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
discardSetting() {
|
discardSetting() {
|
||||||
this.securitySettingsFormGroup.reset(this.securitySettings);
|
this.securitySettingsFormGroup.reset(this.securitySettings);
|
||||||
}
|
}
|
||||||
@ -189,5 +213,4 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
|
|||||||
confirmForm(): UntypedFormGroup {
|
confirmForm(): UntypedFormGroup {
|
||||||
return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
|
return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,10 +44,11 @@
|
|||||||
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
|
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary">
|
<mat-form-field class="mat-block same-color" hideRequiredMarker appearance="fill" color="primary" subscriptSizing="dynamic">
|
||||||
<mat-label translate>login.new-password</mat-label>
|
<mat-label translate>login.new-password</mat-label>
|
||||||
<input matInput type="password" name="new-password" formControlName="newPassword" autocomplete="new-password" required/>
|
<input matInput type="password" name="new-password" formControlName="newPassword" autocomplete="new-password" required/>
|
||||||
<tb-toggle-password [fxShow]="changePassword.get('newPassword').dirty || changePassword.get('newPassword').touched" matSuffix></tb-toggle-password>
|
<tb-toggle-password [fxShow]="changePassword.get('newPassword').dirty || changePassword.get('newPassword').touched" matSuffix></tb-toggle-password>
|
||||||
|
<mat-hint></mat-hint>
|
||||||
<mat-error *ngIf="changePassword.get('newPassword').errors
|
<mat-error *ngIf="changePassword.get('newPassword').errors
|
||||||
&& !changePassword.get('newPassword').hasError('alreadyUsed')
|
&& !changePassword.get('newPassword').hasError('alreadyUsed')
|
||||||
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
|
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
|
||||||
@ -115,6 +116,13 @@
|
|||||||
</tb-icon>
|
</tb-icon>
|
||||||
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
|
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="password-requirements" *ngIf="passwordPolicy.maximumLength > 0">
|
||||||
|
<h4 class="mat-h4" translate>security.password-requirement.at-most</h4>
|
||||||
|
<p class="mat-body">
|
||||||
|
<mat-icon class="tb-mat-20" [svgIcon]="changePassword.get('newPassword').hasError('maxLength') ? 'mdi:circle-small' : 'mdi:check'"></mat-icon>
|
||||||
|
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.maximumLength} }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">
|
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">
|
||||||
|
|||||||
@ -213,6 +213,10 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
errors.minLength = true;
|
errors.minLength = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) {
|
||||||
|
errors.maxLength = true;
|
||||||
|
}
|
||||||
|
|
||||||
return isEqual(errors, {}) ? null : errors;
|
return isEqual(errors, {}) ? null : errors;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,7 +56,8 @@
|
|||||||
<mat-icon matPrefix>lock</mat-icon>
|
<mat-icon matPrefix>lock</mat-icon>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div fxLayoutAlign="end center" class="forgot-password">
|
<div fxLayoutAlign="end center" class="forgot-password">
|
||||||
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">{{ 'login.forgot-password' | translate }}
|
<button class="tb-reset-password" mat-button type="button" routerLink="/login/resetPasswordRequest">
|
||||||
|
{{ (passwordViolation ? 'login.reset-password' : 'login.forgot-password') | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div fxLayout="column" class="tb-action-button">
|
<div fxLayout="column" class="tb-action-button">
|
||||||
|
|||||||
@ -32,6 +32,8 @@ import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
|
|||||||
})
|
})
|
||||||
export class LoginComponent extends PageComponent implements OnInit {
|
export class LoginComponent extends PageComponent implements OnInit {
|
||||||
|
|
||||||
|
passwordViolation = false;
|
||||||
|
|
||||||
loginFormGroup = this.fb.group({
|
loginFormGroup = this.fb.group({
|
||||||
username: '',
|
username: '',
|
||||||
password: ''
|
password: ''
|
||||||
@ -57,6 +59,8 @@ export class LoginComponent extends PageComponent implements OnInit {
|
|||||||
if (error && error.error && error.error.errorCode) {
|
if (error && error.error && error.error.errorCode) {
|
||||||
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
|
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
|
||||||
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);
|
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);
|
||||||
|
} else if (error.error.errorCode === Constants.serverErrorCode.passwordViolation) {
|
||||||
|
this.passwordViolation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,8 @@ export const Constants = {
|
|||||||
badRequestParams: 31,
|
badRequestParams: 31,
|
||||||
itemNotFound: 32,
|
itemNotFound: 32,
|
||||||
tooManyRequests: 33,
|
tooManyRequests: 33,
|
||||||
tooManyUpdates: 34
|
tooManyUpdates: 34,
|
||||||
|
passwordViolation: 45
|
||||||
},
|
},
|
||||||
entryPoints: {
|
entryPoints: {
|
||||||
login: '/api/auth/login',
|
login: '/api/auth/login',
|
||||||
|
|||||||
@ -99,12 +99,14 @@ export type DeviceConnectivitySettings = Record<DeviceConnectivityProtocol, Devi
|
|||||||
|
|
||||||
export interface UserPasswordPolicy {
|
export interface UserPasswordPolicy {
|
||||||
minimumLength: number;
|
minimumLength: number;
|
||||||
|
maximumLength: number;
|
||||||
minimumUppercaseLetters: number;
|
minimumUppercaseLetters: number;
|
||||||
minimumLowercaseLetters: number;
|
minimumLowercaseLetters: number;
|
||||||
minimumDigits: number;
|
minimumDigits: number;
|
||||||
minimumSpecialCharacters: number;
|
minimumSpecialCharacters: number;
|
||||||
passwordExpirationPeriodDays: number;
|
passwordExpirationPeriodDays: number;
|
||||||
allowWhitespaces: boolean;
|
allowWhitespaces: boolean;
|
||||||
|
forceUserToResetPasswordIfNotValid: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SecuritySettings {
|
export interface SecuritySettings {
|
||||||
|
|||||||
@ -182,7 +182,10 @@
|
|||||||
"password-policy": "Password policy",
|
"password-policy": "Password policy",
|
||||||
"minimum-password-length": "Minimum password length",
|
"minimum-password-length": "Minimum password length",
|
||||||
"minimum-password-length-required": "Minimum password length is required",
|
"minimum-password-length-required": "Minimum password length is required",
|
||||||
"minimum-password-length-range": "Minimum password length should be in a range from 5 to 50",
|
"minimum-password-length-range": "Minimum password length should be in a range from 6 to 50",
|
||||||
|
"maximum-password-length": "Maximum password length",
|
||||||
|
"maximum-password-length-min": "Maximum password length should be at least 6",
|
||||||
|
"maximum-password-length-less-min": "Maximum password length should be greater than minimum length",
|
||||||
"minimum-uppercase-letters": "Minimum number of uppercase letters",
|
"minimum-uppercase-letters": "Minimum number of uppercase letters",
|
||||||
"minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative",
|
"minimum-uppercase-letters-range": "Minimum number of uppercase letters can't be negative",
|
||||||
"minimum-lowercase-letters": "Minimum number of lowercase letters",
|
"minimum-lowercase-letters": "Minimum number of lowercase letters",
|
||||||
@ -196,6 +199,8 @@
|
|||||||
"password-reuse-frequency-days": "Password reuse frequency in days",
|
"password-reuse-frequency-days": "Password reuse frequency in days",
|
||||||
"password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative",
|
"password-reuse-frequency-days-range": "Password reuse frequency in days can't be negative",
|
||||||
"allow-whitespace": "Allow whitespace",
|
"allow-whitespace": "Allow whitespace",
|
||||||
|
"force-reset-password-if-no-valid": "Force to reset password if not valid",
|
||||||
|
"force-reset-password-if-no-valid-hint": "Please be careful when enabling this feature: it will require users with not valid password to reset their password via email.",
|
||||||
"general-policy": "General policy",
|
"general-policy": "General policy",
|
||||||
"max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
|
"max-failed-login-attempts": "Maximum number of failed login attempts, before account is locked",
|
||||||
"minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
|
"minimum-max-failed-login-attempts-range": "Maximum number of failed login attempts can't be negative",
|
||||||
@ -3610,7 +3615,8 @@
|
|||||||
"password-requirements": "Password requirements",
|
"password-requirements": "Password requirements",
|
||||||
"password-should-difference": "New password should be different from current",
|
"password-should-difference": "New password should be different from current",
|
||||||
"special-character": "{ count, plural, =1 {1 special character} other {# special characters} }",
|
"special-character": "{ count, plural, =1 {1 special character} other {# special characters} }",
|
||||||
"uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }"
|
"uppercase-letter": "{ count, plural, =1 {1 uppercase letter} other {# uppercase letters} }",
|
||||||
|
"at-most": "At most:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"relation": {
|
"relation": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user