Merge pull request #6646 from ViacheslavKlimov/2fa-improvements
[3.4] 2FA improvements
This commit is contained in:
commit
a12a59d8c4
@ -56,12 +56,31 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
|
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
|
||||||
|
PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null);
|
||||||
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
|
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
|
||||||
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings()))
|
.map(userAuthSettings -> {
|
||||||
.map(twoFaSettings -> {
|
AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings();
|
||||||
twoFaSettings.getConfigs().keySet().removeIf(providerType -> {
|
if (twoFaSettings == null) return null;
|
||||||
return getTwoFaProviderConfig(tenantId, providerType).isEmpty();
|
boolean updateNeeded;
|
||||||
|
|
||||||
|
Map<TwoFaProviderType, TwoFaAccountConfig> configs = twoFaSettings.getConfigs();
|
||||||
|
updateNeeded = configs.keySet().removeIf(providerType -> {
|
||||||
|
return platformTwoFaSettings == null || platformTwoFaSettings.getProviderConfig(providerType).isEmpty();
|
||||||
});
|
});
|
||||||
|
if (configs.size() == 1 && configs.containsKey(TwoFaProviderType.BACKUP_CODE)) {
|
||||||
|
configs.remove(TwoFaProviderType.BACKUP_CODE);
|
||||||
|
updateNeeded = true;
|
||||||
|
}
|
||||||
|
if (!configs.isEmpty() && configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
|
||||||
|
configs.values().stream()
|
||||||
|
.filter(config -> config.getProviderType() != TwoFaProviderType.BACKUP_CODE)
|
||||||
|
.findFirst().ifPresent(config -> config.setUseByDefault(true));
|
||||||
|
updateNeeded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateNeeded) {
|
||||||
|
twoFaSettings = saveAccountTwoFaSettings(tenantId, userId, twoFaSettings);
|
||||||
|
}
|
||||||
return twoFaSettings;
|
return twoFaSettings;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,6 +96,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
|
|||||||
|
|
||||||
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
||||||
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
|
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
|
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
|
||||||
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
|
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
|
||||||
twoFaSettings.setTotalAllowedTimeForVerification(3600);
|
twoFaSettings.setTotalAllowedTimeForVerification(3600);
|
||||||
@ -117,6 +118,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
|
|||||||
twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
|
twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
|
||||||
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
|
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
|
||||||
twoFaSettings.setTotalAllowedTimeForVerification(0);
|
twoFaSettings.setTotalAllowedTimeForVerification(0);
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
|
||||||
String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
|
String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
|
||||||
.andExpect(status().isBadRequest()));
|
.andExpect(status().isBadRequest()));
|
||||||
@ -156,6 +158,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
|
|||||||
private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
|
private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
|
||||||
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
||||||
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
|
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
twoFaSettings.setTotalAllowedTimeForVerification(100);
|
||||||
|
|
||||||
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
|
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
|
||||||
.andExpect(status().isBadRequest()));
|
.andExpect(status().isBadRequest()));
|
||||||
@ -432,8 +436,9 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
|
|||||||
|
|
||||||
private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
|
private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
|
||||||
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
||||||
|
|
||||||
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
|
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
twoFaSettings.setTotalAllowedTimeForVerification(100);
|
||||||
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
|
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -355,6 +355,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
|
|||||||
emailTwoFaProviderConfig.setVerificationCodeLifetime(60);
|
emailTwoFaProviderConfig.setVerificationCodeLifetime(60);
|
||||||
|
|
||||||
platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig));
|
platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig));
|
||||||
|
platformTwoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
platformTwoFaSettings.setTotalAllowedTimeForVerification(100);
|
||||||
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings);
|
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings);
|
||||||
|
|
||||||
User twoFaUser = new User();
|
User twoFaUser = new User();
|
||||||
@ -409,6 +411,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
|
|||||||
|
|
||||||
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
||||||
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
|
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
twoFaSettings.setTotalAllowedTimeForVerification(100);
|
||||||
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
|
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
|
||||||
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
|
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
|
||||||
|
|
||||||
@ -425,6 +429,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest {
|
|||||||
|
|
||||||
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
|
||||||
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
|
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
|
||||||
|
twoFaSettings.setMinVerificationCodeSendPeriod(5);
|
||||||
|
twoFaSettings.setTotalAllowedTimeForVerification(100);
|
||||||
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
|
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
|
||||||
|
|
||||||
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
|
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
|
||||||
|
|||||||
@ -35,12 +35,14 @@ public class PlatformTwoFaSettings {
|
|||||||
@NotNull
|
@NotNull
|
||||||
private List<TwoFaProviderConfig> providers;
|
private List<TwoFaProviderConfig> providers;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
@Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5")
|
@Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5")
|
||||||
private Integer minVerificationCodeSendPeriod;
|
private Integer minVerificationCodeSendPeriod;
|
||||||
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
|
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
|
||||||
private String verificationCodeCheckRateLimit;
|
private String verificationCodeCheckRateLimit;
|
||||||
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
|
||||||
private Integer maxVerificationFailuresBeforeUserLockout;
|
private Integer maxVerificationFailuresBeforeUserLockout;
|
||||||
|
@NotNull
|
||||||
@Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60")
|
@Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60")
|
||||||
private Integer totalAllowedTimeForVerification;
|
private Integer totalAllowedTimeForVerification;
|
||||||
|
|
||||||
|
|||||||
@ -376,7 +376,7 @@ export class MenuService {
|
|||||||
name: 'admin.system-settings',
|
name: 'admin.system-settings',
|
||||||
type: 'toggle',
|
type: 'toggle',
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
height: '120px',
|
height: '80px',
|
||||||
icon: 'settings',
|
icon: 'settings',
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright © 2016-2022 The Thingsboard Authors
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
@ -29,6 +29,7 @@ import { DatePipe } from '@angular/common';
|
|||||||
import { ClipboardService } from 'ngx-clipboard';
|
import { ClipboardService } from 'ngx-clipboard';
|
||||||
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
|
import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service';
|
||||||
import {
|
import {
|
||||||
|
AccountTwoFaSettingProviders,
|
||||||
AccountTwoFaSettings,
|
AccountTwoFaSettings,
|
||||||
BackupCodeTwoFactorAuthAccountConfig,
|
BackupCodeTwoFactorAuthAccountConfig,
|
||||||
EmailTwoFactorAuthAccountConfig,
|
EmailTwoFactorAuthAccountConfig,
|
||||||
@ -49,7 +50,7 @@ import { isDefinedAndNotNull } from '@core/utils';
|
|||||||
export class SecurityComponent extends PageComponent implements OnInit, OnDestroy {
|
export class SecurityComponent extends PageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
private readonly destroy$ = new Subject<void>();
|
private readonly destroy$ = new Subject<void>();
|
||||||
private accountConfig: AccountTwoFaSettings;
|
private accountConfig: AccountTwoFaSettingProviders;
|
||||||
|
|
||||||
twoFactorAuth: FormGroup;
|
twoFactorAuth: FormGroup;
|
||||||
user: User;
|
user: User;
|
||||||
@ -128,12 +129,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
}
|
}
|
||||||
|
|
||||||
private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) {
|
private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) {
|
||||||
this.accountConfig = setting;
|
this.accountConfig = setting?.configs || {};
|
||||||
const configs = this.accountConfig.configs;
|
|
||||||
Object.values(TwoFactorAuthProviderType).forEach(provider => {
|
Object.values(TwoFactorAuthProviderType).forEach(provider => {
|
||||||
if (configs[provider]) {
|
if (this.accountConfig[provider]) {
|
||||||
this.twoFactorAuth.get(provider).setValue(true);
|
this.twoFactorAuth.get(provider).setValue(true);
|
||||||
if (configs[provider].useByDefault) {
|
if (this.accountConfig[provider].useByDefault) {
|
||||||
this.useByDefault = provider;
|
this.useByDefault = provider;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -216,7 +216,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateNewBackupCode() {
|
generateNewBackupCode() {
|
||||||
const codeLeft = this.accountConfig.configs[TwoFactorAuthProviderType.BACKUP_CODE].codesLeft;
|
const codeLeft = (this.accountConfig[TwoFactorAuthProviderType.BACKUP_CODE] as BackupCodeTwoFactorAuthAccountConfig).codesLeft;
|
||||||
let subscription: Observable<boolean>;
|
let subscription: Observable<boolean>;
|
||||||
if (codeLeft) {
|
if (codeLeft) {
|
||||||
subscription = this.dialogService.confirm(
|
subscription = this.dialogService.confirm(
|
||||||
@ -240,7 +240,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro
|
|||||||
|
|
||||||
providerDataInfo(provider: TwoFactorAuthProviderType) {
|
providerDataInfo(provider: TwoFactorAuthProviderType) {
|
||||||
const info = {info: null};
|
const info = {info: null};
|
||||||
const providerConfig = this.accountConfig.configs[provider];
|
const providerConfig = this.accountConfig[provider];
|
||||||
if (isDefinedAndNotNull(providerConfig)) {
|
if (isDefinedAndNotNull(providerConfig)) {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case TwoFactorAuthProviderType.EMAIL:
|
case TwoFactorAuthProviderType.EMAIL:
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { interval, Subscription } from 'rxjs';
|
import { interval, Subscription } from 'rxjs';
|
||||||
import { isEqual } from '@core/utils';
|
import { isEqual } from '@core/utils';
|
||||||
|
import { ActionNotificationShow } from '@core/notification/notification.actions';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'tb-two-factor-auth-login',
|
selector: 'tb-two-factor-auth-login',
|
||||||
@ -118,6 +119,13 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit
|
|||||||
}
|
}
|
||||||
this.verificationForm.get('verificationCode').setErrors(errors);
|
this.verificationForm.get('verificationCode').setErrors(errors);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
} else {
|
||||||
|
this.store.dispatch(new ActionNotificationShow({
|
||||||
|
message: error.error.message,
|
||||||
|
type: 'error',
|
||||||
|
verticalPosition: 'top',
|
||||||
|
horizontalPosition: 'left'
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -88,11 +88,14 @@ export interface BackupCodeTwoFactorAuthAccountConfig extends GeneralTwoFactorAu
|
|||||||
export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig |
|
export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig |
|
||||||
EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig;
|
EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig;
|
||||||
|
|
||||||
|
|
||||||
export interface AccountTwoFaSettings {
|
export interface AccountTwoFaSettings {
|
||||||
configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig};
|
configs: AccountTwoFaSettingProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AccountTwoFaSettingProviders = {
|
||||||
|
[key in TwoFactorAuthProviderType]?: TwoFactorAuthAccountConfig;
|
||||||
|
};
|
||||||
|
|
||||||
export interface TwoFaProviderInfo {
|
export interface TwoFaProviderInfo {
|
||||||
type: TwoFactorAuthProviderType;
|
type: TwoFactorAuthProviderType;
|
||||||
default: boolean;
|
default: boolean;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user