Browse Source

UI: Add 2FA resend code timer

pull/6235/head
Vladyslav_Prykhodko 4 years ago
parent
commit
cc310887c6
  1. 54
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html
  2. 70
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss
  3. 43
      ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts
  4. 25
      ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html
  5. 9
      ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss
  6. 39
      ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts
  7. 6
      ui-ngx/src/app/shared/models/two-factor-auth.models.ts
  8. 8
      ui-ngx/src/assets/locale/locale.constant-en_US.json

54
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html

@ -38,7 +38,7 @@
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center">
<mat-slide-toggle
(click)="toggleProviders($event, i)"
(click)="toggleExtensionPanel($event, i, provider.get('enable').value)"
formControlName="enable">
</mat-slide-toggle>
{{ provider.value.providerType }}
@ -114,6 +114,17 @@
{{ 'admin.2fa.total-allowed-time-for-verification-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.retry-verification-code-period</mat-label>
<input matInput required formControlName="minVerificationCodeSendPeriod" type="number" step="1" min="5">
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('required')">
{{ 'admin.2fa.retry-verification-code-period-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('pattern')
|| twoFaFormGroup.get('minVerificationCodeSendPeriod').hasError('min')">
{{ 'admin.2fa.retry-verification-code-period-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.max-verification-failures-before-user-lockout</mat-label>
<input matInput required formControlName="maxVerificationFailuresBeforeUserLockout" type="number" step="1" min="0" max="65535">
@ -127,48 +138,11 @@
</mat-error>
</mat-form-field>
</div>
<mat-expansion-panel class="provider">
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center">
<mat-slide-toggle
(click)="toggleExtensionPanel($event, 0, twoFaFormGroup.get('verificationCodeSendRateLimitEnable').value)"
formControlName="verificationCodeSendRateLimitEnable">
</mat-slide-toggle>
{{ 'admin.2fa.verification-code-send-rate-limit' | translate }}
</mat-panel-title>
</mat-expansion-panel-header>
<ng-template matExpansionPanelContent>
<div fxLayout="row" fxLayout.xs="column" fxLayoutGap.gt-xs="8px">
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.number-of-send-attempts</mat-label>
<input matInput formControlName="verificationCodeSendRateLimitNumber" type="number" step="1" min="1" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('required')">
{{ 'admin.2fa.number-of-send-attempts-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeSendRateLimitNumber').hasError('min')">
{{ 'admin.2fa.number-of-send-attempts-pattern' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field fxFlex class="mat-block">
<mat-label translate>admin.2fa.within-time</mat-label>
<input matInput formControlName="verificationCodeSendRateLimitTime" type="number" step="1" min="1" required>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('required')">
{{ 'admin.2fa.within-time-required' | translate }}
</mat-error>
<mat-error *ngIf="twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('pattern')
|| twoFaFormGroup.get('verificationCodeSendRateLimitTime').hasError('min')">
{{ 'admin.2fa.within-time-pattern' | translate }}
</mat-error>
</mat-form-field>
</div>
</ng-template>
</mat-expansion-panel>
<mat-divider></mat-divider>
<mat-expansion-panel class="provider">
<mat-expansion-panel-header>
<mat-panel-title fxLayoutAlign="start center">
<mat-slide-toggle (click)="toggleExtensionPanel($event, 1, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
<mat-slide-toggle (click)="toggleExtensionPanel($event, 3, twoFaFormGroup.get('verificationCodeCheckRateLimitEnable').value)"
formControlName="verificationCodeCheckRateLimitEnable">
</mat-slide-toggle>
{{ 'admin.2fa.verification-code-check-rate-limit' | translate }}

70
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.scss

@ -14,47 +14,55 @@
* limitations under the License.
*/
:host{
@import "../../../../../scss/constants";
.fields-group {
margin: 8px 0;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
position: relative;
padding-bottom: 8px;
:host{
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
margin: 0 8px;
mat-card.settings-card {
@media #{$mat-md} {
width: 90%;
}
.input-row {
padding: 8px 8px 0;
}
.fields-group {
margin: 8px 0;
border: 1px groove rgba(0, 0, 0, .25);
border-radius: 4px;
position: relative;
padding-bottom: 8px;
legend {
color: rgba(0, 0, 0, .7);
width: fit-content;
margin: 0 8px;
}
.input-row {
padding: 8px 8px 0;
}
&:last-of-type {
margin-bottom: 24px;
&:last-of-type {
margin-bottom: 24px;
}
}
}
.mat-expansion-panel {
box-shadow: none;
margin: 1px 0 0;
&.provider {
overflow: inherit;
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
height: 48px;
.mat-expansion-panel {
box-shadow: none;
margin: 1px 0 0;
&.provider {
overflow: inherit;
.mat-expansion-panel-header {
padding: 0 24px 0 8px;
&.mat-expanded {
height: 48px;
}
.mat-slide-toggle {
margin-right: 8px;
}
}
.mat-slide-toggle {
margin-right: 8px;
.mat-expansion-panel-header-title {
height: 40px;
}
}
.mat-expansion-panel-header-title {
height: 40px;
}
}
}
}

43
ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts

@ -35,7 +35,7 @@ import { MatExpansionPanel } from '@angular/material/expansion';
@Component({
selector: 'tb-2fa-settings',
templateUrl: './two-factor-auth-settings.component.html',
styleUrls: ['./two-factor-auth-settings.component.scss', './settings-card.scss']
styleUrls: [ './settings-card.scss', './two-factor-auth-settings.component.scss']
})
export class TwoFactorAuthSettingsComponent extends PageComponent implements OnInit, HasConfirmForm, OnDestroy {
@ -73,7 +73,6 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
if (this.twoFaFormGroup.valid) {
const setting = this.twoFaFormGroup.value as TwoFactorAuthSettingsForm;
this.joinRateLimit(setting, 'verificationCodeCheckRateLimit');
this.joinRateLimit(setting, 'verificationCodeSendRateLimit');
const providers = setting.providers.filter(provider => provider.enable);
providers.forEach(provider => delete provider.enable);
const config = Object.assign(setting, {providers});
@ -91,18 +90,14 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
}
}
toggleProviders($event: Event, i: number): void {
this.toggleExtensionPanel($event, i + 2, this.providersForm.at(i).get('enable').value);
}
toggleExtensionPanel($event: Event, i: number, currentState: boolean) {
toggleExtensionPanel($event: Event, index: number, currentState: boolean) {
if ($event) {
$event.stopPropagation();
}
if (currentState) {
this.getByIndexPanel(i).close();
this.getByIndexPanel(index).close();
} else {
this.getByIndexPanel(i).open();
this.getByIndexPanel(index).open();
}
}
@ -130,9 +125,7 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
verificationCodeCheckRateLimitEnable: [false],
verificationCodeCheckRateLimitNumber: ['3', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeCheckRateLimitTime: ['900', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeSendRateLimitEnable: [false],
verificationCodeSendRateLimitNumber: ['1', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
verificationCodeSendRateLimitTime: ['60', [Validators.required, Validators.min(1), Validators.pattern(/^\d*$/)]],
minVerificationCodeSendPeriod: ['30', [Validators.required, Validators.min(5), Validators.pattern(/^\d*$/)]],
providers: this.fb.array([])
});
Object.values(TwoFactorAuthProviderType).forEach(provider => {
@ -149,43 +142,25 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
this.twoFaFormGroup.get('verificationCodeCheckRateLimitTime').disable({emitEvent: false});
}
});
this.twoFaFormGroup.get('verificationCodeSendRateLimitEnable').valueChanges.pipe(
takeUntil(this.destroy$)
).subscribe(value => {
if (value) {
this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').enable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').enable({emitEvent: false});
} else {
this.twoFaFormGroup.get('verificationCodeSendRateLimitNumber').disable({emitEvent: false});
this.twoFaFormGroup.get('verificationCodeSendRateLimitTime').disable({emitEvent: false});
}
});
}
private setAuthConfigFormValue(settings: TwoFactorAuthSettings) {
const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings.verificationCodeCheckRateLimit);
const [sendRateLimitNumber, sendRateLimitTime] = this.splitRateLimit(settings.verificationCodeSendRateLimit);
const allowProvidersConfig = settings.providers.map(provider => provider.providerType);
const processFormValue: TwoFactorAuthSettingsForm = Object.assign(deepClone(settings), {
verificationCodeCheckRateLimitEnable: checkRateLimitNumber > 0,
verificationCodeCheckRateLimitNumber: checkRateLimitNumber || 3,
verificationCodeCheckRateLimitTime: checkRateLimitTime || 900,
verificationCodeSendRateLimitEnable: sendRateLimitNumber > 0,
verificationCodeSendRateLimitNumber: sendRateLimitNumber || 1,
verificationCodeSendRateLimitTime: sendRateLimitTime || 60,
providers: []
});
if (sendRateLimitNumber > 0) {
this.getByIndexPanel(0).open();
}
if (checkRateLimitNumber > 0) {
this.getByIndexPanel(1).open();
this.getByIndexPanel(3).open();
}
Object.values(TwoFactorAuthProviderType).forEach((provider, index) => {
const findIndex = allowProvidersConfig.indexOf(provider);
if (findIndex > -1) {
processFormValue.providers.push(Object.assign(settings.providers[findIndex], {enable: true}));
this.getByIndexPanel(index + 2).open();
this.getByIndexPanel(index).open();
} else {
processFormValue.providers.push({enable: false});
}
@ -203,9 +178,9 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI
formControlConfig.issuerName = [{value: 'ThingsBoard', disabled: true}, Validators.required];
break;
case TwoFactorAuthProviderType.SMS:
formControlConfig.smsVerificationMessageTemplate = [{value: 'Verification code: ${сode}', disabled: true}, [
formControlConfig.smsVerificationMessageTemplate = [{value: 'Verification code: ${code}', disabled: true}, [
Validators.required,
Validators.pattern(/\${сode}/)
Validators.pattern(/\${code}/)
]];
formControlConfig.verificationCodeLifetime = [{value: 120, disabled: true}, [
Validators.required,

25
ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.html

@ -46,20 +46,25 @@
inputmode="numeric" pattern="[0-9]*"
autocomplete="off"
placeholder="{{ 'security.2fa.dialog.verification-code' | translate }}"/>
<mat-error *ngIf="verificationForm.get('verificationCode').invalid">
<button *ngIf="selectedProvider !== twoFactorAuthProvider.TOTP"
mat-button matSuffix
(click)="sendCode($event)"
[disabled]="disabledResendButton"
type="button" tabindex="-1">
{{ 'login.resend-code' | translate }}
</button>
<mat-error *ngIf="verificationForm.get('verificationCode').invalid" style="margin-bottom: 8px">
{{ 'security.2fa.dialog.verification-code-invalid' | translate }}
</mat-error>
<mat-error *ngIf="verificationForm.get('verificationCode').invalid && selectedProvider !== twoFactorAuthProvider.TOTP && countDownTime" class="timer">
{{ 'login.resend-code-wait' | translate : {time: countDownTime} }}
</mat-error>
<mat-hint *ngIf="selectedProvider !== twoFactorAuthProvider.TOTP && countDownTime" class="timer">
{{ 'login.resend-code-wait' | translate : {time: countDownTime} }}
</mat-hint>
</mat-form-field>
<div fxLayoutAlign="start center" *ngIf="selectedProvider !== twoFactorAuthProvider.TOTP">
<button
mat-button
(click)="sendCode()"
type="button">
{{ 'login.resend-code' | translate }}
</button>
</div>
</div>
<span style="height: 40px;"></span>
<span style="height: 50px;"></span>
<div fxLayout="column" fxLayoutGap="8px">
<button mat-raised-button
color="accent"

9
ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.scss

@ -53,6 +53,12 @@
padding-bottom: 8px;
}
}
.timer {
font: 400 12px / 14px Roboto, "Helvetica Neue", sans-serif;
letter-spacing: 0.4px;
color: rgba(255, 255, 255, 0.8);
}
}
}
::ng-deep{
@ -68,5 +74,8 @@
vertical-align: sub;
}
}
.mat-form-field-invalid .mat-hint {
margin-top: 20px;
}
}
}

39
ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts

@ -14,7 +14,7 @@
/// limitations under the License.
///
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { AuthService } from '@core/auth/auth.service';
import { Store } from '@ngrx/store';
import { AppState } from '@core/core.state';
@ -27,16 +27,19 @@ import {
TwoFaProviderInfo
} from '@shared/models/two-factor-auth.models';
import { TranslateService } from '@ngx-translate/core';
import { interval, Subscription } from 'rxjs';
@Component({
selector: 'tb-two-factor-auth-login',
templateUrl: './two-factor-auth-login.component.html',
styleUrls: ['./two-factor-auth-login.component.scss']
})
export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit {
export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit, OnDestroy {
private providersInfo: TwoFaProviderInfo[];
private prevProvider: TwoFactorAuthProviderType;
private timer: Subscription;
private minVerificationPeriod = 0;
selectedProvider: TwoFactorAuthProviderType;
twoFactorAuthProvider = TwoFactorAuthProviderType;
@ -44,6 +47,8 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
providersData = twoFactorAuthProvidersLoginData;
providerDescription = '';
disabledResendButton = true;
countDownTime = 0;
verificationForm = this.fb.group({
verificationCode: ['', [
@ -72,6 +77,7 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
this.providerDescription = this.translate.instant(this.providersData.get(providerConfig.type).description, {
contact: providerConfig.contact
});
this.minVerificationPeriod = providerConfig?.minVerificationCodeSendPeriod || 30;
}
this.allowProviders.push(providerConfig.type);
}
@ -79,6 +85,12 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
if (this.selectedProvider !== TwoFactorAuthProviderType.TOTP) {
this.sendCode();
}
this.timer = interval(1000).subscribe(() => this.updatedTime());
}
ngOnDestroy() {
super.ngOnDestroy();
this.timer.unsubscribe();
}
sendVerificationCode() {
@ -93,18 +105,28 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
this.prevProvider = type === null ? this.selectedProvider : null;
this.selectedProvider = type;
if (type !== null) {
this.verificationForm.get('verificationCode').reset();
const providerConfig = this.providersInfo.find(config => config.type === type);
this.providerDescription = this.translate.instant(this.providersData.get(providerConfig.type).description, {
contact: providerConfig.contact
});
this.minVerificationPeriod = providerConfig?.minVerificationCodeSendPeriod || 30;
if (type !== TwoFactorAuthProviderType.TOTP) {
this.sendCode();
}
}
}
sendCode() {
this.twoFactorAuthService.requestTwoFaVerificationCodeSend(this.selectedProvider).subscribe(() => {});
sendCode($event?: Event) {
if ($event) {
$event.stopPropagation();
}
this.disabledResendButton = true;
this.twoFactorAuthService.requestTwoFaVerificationCodeSend(this.selectedProvider).subscribe(() => {
this.countDownTime = this.minVerificationPeriod;
}, () => {
this.countDownTime = 30;
});
}
cancelLogin() {
@ -115,4 +137,13 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
this.authService.logout();
}
}
private updatedTime() {
if (this.countDownTime > 0) {
this.countDownTime--;
if (this.countDownTime === 0) {
this.disabledResendButton = false;
}
}
}
}

6
ui-ngx/src/app/shared/models/two-factor-auth.models.ts

@ -20,7 +20,7 @@ export interface TwoFactorAuthSettings {
totalAllowedTimeForVerification: number;
useSystemTwoFactorAuthSettings: boolean;
verificationCodeCheckRateLimit: string;
verificationCodeSendRateLimit: string;
minVerificationCodeSendPeriod: number;
}
export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
@ -28,9 +28,6 @@ export interface TwoFactorAuthSettingsForm extends TwoFactorAuthSettings{
verificationCodeCheckRateLimitEnable: boolean;
verificationCodeCheckRateLimitNumber: number;
verificationCodeCheckRateLimitTime: number;
verificationCodeSendRateLimitEnable: boolean;
verificationCodeSendRateLimitNumber: number;
verificationCodeSendRateLimitTime: number;
}
export type TwoFactorAuthProviderConfig = Partial<TotpTwoFactorAuthProviderConfig | SmsTwoFactorAuthProviderConfig |
@ -93,6 +90,7 @@ export interface TwoFaProviderInfo {
type: TwoFactorAuthProviderType;
default: boolean;
contact?: string;
minVerificationCodeSendPeriod?: number;
}
export interface TwoFactorAuthProviderData {

8
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -323,10 +323,10 @@
"number-of-checking-attempts": "Number of checking attempts",
"number-of-checking-attempts-pattern": "Number of checking attempts must be a positive integer.",
"number-of-checking-attempts-required": "Number of checking attempts is required.",
"number-of-send-attempts": "Number of send attempts",
"number-of-send-attempts-pattern": "Number of send attempts must be a positive integer.",
"number-of-send-attempts-required": "Number of send attempts is required.",
"provider": "Provider",
"retry-verification-code-period": "Retry verification code period (sec)",
"retry-verification-code-period-pattern": "Minimal period time is 5 sec",
"retry-verification-code-period-required": "Retry verification code period is required.",
"total-allowed-time-for-verification": "Total allowed time for verification (sec)",
"total-allowed-time-for-verification-pattern": "Total allowed time must be a positive integer.",
"total-allowed-time-for-verification-required": "Total allowed time is required.",
@ -335,7 +335,6 @@
"verification-code-lifetime": "Verification code lifetime (sec)",
"verification-code-lifetime-pattern": "Verification code lifetime must be a positive integer.",
"verification-code-lifetime-required": "Verification code lifetime is required.",
"verification-code-send-rate-limit": "Verification code send rate limit",
"verification-message-template": "Verification message template",
"verification-limitations": "Verification limitations",
"verification-message-template-pattern": "Verification message need to contains pattern: ${code}",
@ -2484,6 +2483,7 @@
"verify-your-identity": "Verify your identity",
"select-way-to-verify": "Select a way to verify",
"resend-code": "Resend code",
"resend-code-wait": "Resend code wait { time, plural, 1 {1 second} other {# seconds} }",
"try-another-way": "Try another way",
"totp-auth-description": "Please enter the security code from your authenticator app.",
"sms-auth-description": "A security code has been sent to your phone at {{contact}}.",

Loading…
Cancel
Save