diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java index 23e6dc065f..ad2ecebd2c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java @@ -56,12 +56,31 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { @Override public Optional getAccountTwoFaSettings(TenantId tenantId, UserId userId) { + PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null); return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId)) - .flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings())) - .map(twoFaSettings -> { - twoFaSettings.getConfigs().keySet().removeIf(providerType -> { - return getTwoFaProviderConfig(tenantId, providerType).isEmpty(); + .map(userAuthSettings -> { + AccountTwoFaSettings twoFaSettings = userAuthSettings.getTwoFaSettings(); + if (twoFaSettings == null) return null; + boolean updateNeeded; + + Map 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; }); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java index ee93cadaa4..db15e6fd82 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java @@ -96,6 +96,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig)); + twoFaSettings.setMinVerificationCodeSendPeriod(5); twoFaSettings.setVerificationCodeCheckRateLimit("3:900"); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10); twoFaSettings.setTotalAllowedTimeForVerification(3600); @@ -117,6 +118,7 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { twoFaSettings.setVerificationCodeCheckRateLimit("0:12"); twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1); twoFaSettings.setTotalAllowedTimeForVerification(0); + twoFaSettings.setMinVerificationCodeSendPeriod(5); String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) .andExpect(status().isBadRequest())); @@ -156,6 +158,8 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig)); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings) .andExpect(status().isBadRequest())); @@ -432,8 +436,9 @@ public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest { private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); - twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk()); } diff --git a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java index b825571694..9c30ebbbd0 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java @@ -355,6 +355,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { emailTwoFaProviderConfig.setVerificationCodeLifetime(60); platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig)); + platformTwoFaSettings.setMinVerificationCodeSendPeriod(5); + platformTwoFaSettings.setTotalAllowedTimeForVerification(100); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings); User twoFaUser = new User(); @@ -409,6 +411,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings)); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); @@ -425,6 +429,8 @@ public abstract class TwoFactorAuthTest extends AbstractControllerTest { PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings(); twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList())); + twoFaSettings.setMinVerificationCodeSendPeriod(5); + twoFaSettings.setTotalAllowedTimeForVerification(100); twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings); SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig(); diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java index fd72e7a027..ec2acf8733 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java @@ -35,12 +35,14 @@ public class PlatformTwoFaSettings { @NotNull private List providers; + @NotNull @Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5") private Integer minVerificationCodeSendPeriod; @Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid") private String verificationCodeCheckRateLimit; @Min(value = 0, message = "maximum number of verification failure before user lockout must be positive") private Integer maxVerificationFailuresBeforeUserLockout; + @NotNull @Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60") private Integer totalAllowedTimeForVerification; diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index f042ff6b5e..78c791ba10 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -376,7 +376,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '120px', + height: '80px', icon: 'settings', pages: [ { diff --git a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss b/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss deleted file mode 100644 index ec6f008a80..0000000000 --- a/ui-ngx/src/app/modules/home/pages/profile/authentication-dialog/email-auth-dialog.component.scss +++ /dev/null @@ -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. - */ diff --git a/ui-ngx/src/app/modules/home/pages/security/security.component.ts b/ui-ngx/src/app/modules/home/pages/security/security.component.ts index ab7d470e9e..560ec2b7d2 100644 --- a/ui-ngx/src/app/modules/home/pages/security/security.component.ts +++ b/ui-ngx/src/app/modules/home/pages/security/security.component.ts @@ -29,6 +29,7 @@ import { DatePipe } from '@angular/common'; import { ClipboardService } from 'ngx-clipboard'; import { TwoFactorAuthenticationService } from '@core/http/two-factor-authentication.service'; import { + AccountTwoFaSettingProviders, AccountTwoFaSettings, BackupCodeTwoFactorAuthAccountConfig, EmailTwoFactorAuthAccountConfig, @@ -49,7 +50,7 @@ import { isDefinedAndNotNull } from '@core/utils'; export class SecurityComponent extends PageComponent implements OnInit, OnDestroy { private readonly destroy$ = new Subject(); - private accountConfig: AccountTwoFaSettings; + private accountConfig: AccountTwoFaSettingProviders; twoFactorAuth: FormGroup; user: User; @@ -128,12 +129,11 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro } private processTwoFactorAuthConfig(setting: AccountTwoFaSettings) { - this.accountConfig = setting; - const configs = this.accountConfig.configs; + this.accountConfig = setting?.configs || {}; Object.values(TwoFactorAuthProviderType).forEach(provider => { - if (configs[provider]) { + if (this.accountConfig[provider]) { this.twoFactorAuth.get(provider).setValue(true); - if (configs[provider].useByDefault) { + if (this.accountConfig[provider].useByDefault) { this.useByDefault = provider; } } else { @@ -216,7 +216,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro } generateNewBackupCode() { - const codeLeft = this.accountConfig.configs[TwoFactorAuthProviderType.BACKUP_CODE].codesLeft; + const codeLeft = (this.accountConfig[TwoFactorAuthProviderType.BACKUP_CODE] as BackupCodeTwoFactorAuthAccountConfig).codesLeft; let subscription: Observable; if (codeLeft) { subscription = this.dialogService.confirm( @@ -240,7 +240,7 @@ export class SecurityComponent extends PageComponent implements OnInit, OnDestro providerDataInfo(provider: TwoFactorAuthProviderType) { const info = {info: null}; - const providerConfig = this.accountConfig.configs[provider]; + const providerConfig = this.accountConfig[provider]; if (isDefinedAndNotNull(providerConfig)) { switch (provider) { case TwoFactorAuthProviderType.EMAIL: diff --git a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts index 73401f2a0c..2a98f0b530 100644 --- a/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts +++ b/ui-ngx/src/app/modules/login/pages/login/two-factor-auth-login.component.ts @@ -29,6 +29,7 @@ import { import { TranslateService } from '@ngx-translate/core'; import { interval, Subscription } from 'rxjs'; import { isEqual } from '@core/utils'; +import { ActionNotificationShow } from '@core/notification/notification.actions'; @Component({ selector: 'tb-two-factor-auth-login', @@ -118,6 +119,13 @@ export class TwoFactorAuthLoginComponent extends PageComponent implements OnInit } this.verificationForm.get('verificationCode').setErrors(errors); }, 5000); + } else { + this.store.dispatch(new ActionNotificationShow({ + message: error.error.message, + type: 'error', + verticalPosition: 'top', + horizontalPosition: 'left' + })); } } ); diff --git a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts index 10040e00e7..d3b2a74fbd 100644 --- a/ui-ngx/src/app/shared/models/two-factor-auth.models.ts +++ b/ui-ngx/src/app/shared/models/two-factor-auth.models.ts @@ -88,11 +88,14 @@ export interface BackupCodeTwoFactorAuthAccountConfig extends GeneralTwoFactorAu export type TwoFactorAuthAccountConfig = TotpTwoFactorAuthAccountConfig | SmsTwoFactorAuthAccountConfig | EmailTwoFactorAuthAccountConfig | BackupCodeTwoFactorAuthAccountConfig; - export interface AccountTwoFaSettings { - configs: {TwoFactorAuthProviderType: TwoFactorAuthAccountConfig}; + configs: AccountTwoFaSettingProviders; } +export type AccountTwoFaSettingProviders = { + [key in TwoFactorAuthProviderType]?: TwoFactorAuthAccountConfig; +}; + export interface TwoFaProviderInfo { type: TwoFactorAuthProviderType; default: boolean;