Merge pull request #9791 from ArtemDzhereleiko/AD/imp/security-settings/password-length

Improvement for security settings
This commit is contained in:
Andrew Shvayka 2023-12-11 17:18:31 +02:00 committed by GitHub
commit 614d64dda2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 100 additions and 31 deletions

View File

@ -51,24 +51,37 @@
<fieldset class="fields-group">
<legend class="group-title" translate>admin.password-policy</legend>
<section formGroupName="passwordPolicy">
<mat-form-field class="mat-block">
<mat-label translate>admin.minimum-password-length</mat-label>
<input matInput type="number"
formControlName="minimumLength"
step="1"
min="5"
max="50"
required/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
{{ 'admin.minimum-password-length-required' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
</mat-form-field>
<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>
<input matInput type="number"
formControlName="minimumLength"
step="1"
min="6"
max="50"
required/>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('required')">
{{ 'admin.minimum-password-length-required' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('min')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
<mat-error *ngIf="securitySettingsFormGroup.get('passwordPolicy.minimumLength').hasError('max')">
{{ 'admin.minimum-password-length-range' | translate }}
</mat-error>
</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">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.minimum-uppercase-letters</mat-label>
@ -140,9 +153,16 @@
</mat-error>
</mat-form-field>
</div>
<mat-checkbox formControlName="allowWhitespaces" style="margin-bottom: 16px">
<mat-label translate>admin.allow-whitespace</mat-label>
</mat-checkbox>
<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-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>
</fieldset>
<div fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="8px" class="layout-wrap" style="margin-top: 16px">

View File

@ -19,7 +19,14 @@ import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
import { PageComponent } from '@shared/components/page.component';
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 { AdminService } from '@core/http/admin.service';
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 { DialogService } from '@core/services/dialog.service';
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({
selector: 'tb-security-settings',
@ -67,14 +77,16 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
userLockoutNotificationEmail: ['', []],
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)],
minimumLowercaseLetters: [null, Validators.min(0)],
minimumDigits: [null, Validators.min(0)],
minimumSpecialCharacters: [null, Validators.min(0)],
passwordExpirationPeriodDays: [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(() => {});
}
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() {
this.securitySettingsFormGroup.reset(this.securitySettings);
}
@ -189,5 +213,4 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi
confirmForm(): UntypedFormGroup {
return this.securitySettingsFormGroup.dirty ? this.securitySettingsFormGroup : this.jwtSecuritySettingsFormGroup;
}
}

View File

@ -44,10 +44,11 @@
{{ 'security.password-requirement.incorrect-password-try-again' | translate }}
</mat-error>
</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>
<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>
<mat-hint></mat-hint>
<mat-error *ngIf="changePassword.get('newPassword').errors
&& !changePassword.get('newPassword').hasError('alreadyUsed')
&& !changePassword.get('newPassword').hasError('hasWhitespaces')
@ -115,6 +116,13 @@
</tb-icon>
{{ 'security.password-requirement.character' | translate : {count: passwordPolicy.minimumLength} }}
</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>
</ng-template>
<div fxLayout="row" fxLayoutGap="8px" style="margin-top: 18px;" [fxShow]="changePassword.dirty || changePassword.touched">

View File

@ -213,6 +213,10 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
errors.minLength = true;
}
if (!value.length || this.passwordPolicy.maximumLength > 0 && value.length > this.passwordPolicy.maximumLength) {
errors.maxLength = true;
}
return isEqual(errors, {}) ? null : errors;
};
}

View File

@ -56,7 +56,8 @@
<mat-icon matPrefix>lock</mat-icon>
</mat-form-field>
<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>
</div>
<div fxLayout="column" class="tb-action-button">

View File

@ -32,6 +32,8 @@ import { OAuth2ClientInfo } from '@shared/models/oauth2.models';
})
export class LoginComponent extends PageComponent implements OnInit {
passwordViolation = false;
loginFormGroup = this.fb.group({
username: '',
password: ''
@ -57,6 +59,8 @@ export class LoginComponent extends PageComponent implements OnInit {
if (error && error.error && error.error.errorCode) {
if (error.error.errorCode === Constants.serverErrorCode.credentialsExpired) {
this.router.navigateByUrl(`login/resetExpiredPassword?resetToken=${error.error.resetToken}`);
} else if (error.error.errorCode === Constants.serverErrorCode.passwordViolation) {
this.passwordViolation = true;
}
}
}

View File

@ -30,7 +30,8 @@ export const Constants = {
badRequestParams: 31,
itemNotFound: 32,
tooManyRequests: 33,
tooManyUpdates: 34
tooManyUpdates: 34,
passwordViolation: 45
},
entryPoints: {
login: '/api/auth/login',

View File

@ -99,12 +99,14 @@ export type DeviceConnectivitySettings = Record<DeviceConnectivityProtocol, Devi
export interface UserPasswordPolicy {
minimumLength: number;
maximumLength: number;
minimumUppercaseLetters: number;
minimumLowercaseLetters: number;
minimumDigits: number;
minimumSpecialCharacters: number;
passwordExpirationPeriodDays: number;
allowWhitespaces: boolean;
forceUserToResetPasswordIfNotValid: boolean;
}
export interface SecuritySettings {

View File

@ -182,7 +182,10 @@
"password-policy": "Password policy",
"minimum-password-length": "Minimum password length",
"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-range": "Minimum number of uppercase letters can't be negative",
"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-range": "Password reuse frequency in days can't be negative",
"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",
"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",
@ -3610,7 +3615,8 @@
"password-requirements": "Password requirements",
"password-should-difference": "New password should be different from current",
"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": {