From c78958502bf4592f2b11310ac8c6427dd4d04f7a Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Tue, 18 Nov 2025 18:14:37 +0200 Subject: [PATCH 1/5] Fix 2FA enforcement when admin deletes used 2FA method --- .../mfa/config/DefaultTwoFaConfigManager.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 ad04d3e962..3f7d711378 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 @@ -16,7 +16,6 @@ package org.thingsboard.server.service.security.auth.mfa.config; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; @@ -49,12 +48,11 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { private final UserAuthSettingsDao userAuthSettingsDao; private final AdminSettingsService adminSettingsService; private final AdminSettingsDao adminSettingsDao; - @Autowired @Lazy - private TwoFactorAuthService twoFactorAuthService; + @Lazy + private final TwoFactorAuthService twoFactorAuthService; protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings"; - @Override public Optional getAccountTwoFaSettings(TenantId tenantId, User user) { PlatformTwoFaSettings platformTwoFaSettings = getPlatformTwoFaSettings(tenantId, true).orElse(null); @@ -87,11 +85,6 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { } protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) { - if (settings.getConfigs().isEmpty()) { - if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) { - throw new DataValidationException("At least one 2FA provider is required"); - } - } UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(user.getId())) .orElseGet(() -> { UserAuthSettings newUserAuthSettings = new UserAuthSettings(); @@ -105,7 +98,6 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { return settings; } - @Override public Optional getTwoFaAccountConfig(TenantId tenantId, User user, TwoFaProviderType providerType) { return getAccountTwoFaSettings(tenantId, user) @@ -134,6 +126,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) { configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true)); } + checkAccountTwoFaSettings(tenantId, user, settings); return saveAccountTwoFaSettings(tenantId, user, settings); } @@ -151,6 +144,7 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { .min(Comparator.comparing(TwoFaAccountConfig::getProviderType)) .ifPresent(config -> config.setUseByDefault(true)); } + checkAccountTwoFaSettings(tenantId, user, settings); return saveAccountTwoFaSettings(tenantId, user, settings); } @@ -203,4 +197,12 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager { .ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId())); } + private void checkAccountTwoFaSettings(TenantId tenantId, User user, AccountTwoFaSettings settings) { + if (settings.getConfigs().isEmpty()) { + if (twoFactorAuthService.isEnforceTwoFaEnabled(tenantId, user)) { + throw new DataValidationException("At least one 2FA provider is required"); + } + } + } + } From 5dd8608086b7f1e8420c1d6259a5f147ca3da722 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 19 Nov 2025 18:05:31 +0200 Subject: [PATCH 2/5] UI: Add validation for twoFA settings --- .../pages/admin/two-factor-auth-settings.component.html | 5 ++++- .../home/pages/admin/two-factor-auth-settings.component.ts | 7 +++++++ ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html index d1192cace4..c7ad6f373a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.html @@ -90,6 +90,9 @@
admin.2fa.available-providers
+ @if (!atListOneProvider) { + + }
@@ -243,7 +246,7 @@
diff --git a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts index 2770f904bb..ecd564632a 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/two-factor-auth-settings.component.ts @@ -184,6 +184,13 @@ export class TwoFactorAuthSettingsComponent extends PageComponent implements OnI }); } + get atListOneProvider():boolean { + if (this.twoFaFormGroup.get('enforceTwoFa').value) { + return this.providersForm.value.some(value => value.enable); + } + return true; + } + private setAuthConfigFormValue(settings: TwoFactorAuthSettings) { const [checkRateLimitNumber, checkRateLimitTime] = this.splitRateLimit(settings?.verificationCodeCheckRateLimit); const allowProvidersConfig = settings?.providers.map(provider => provider.providerType) || []; diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 60e279d7e6..8e152eca4b 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -485,6 +485,7 @@ "2fa": { "2fa": "Two-factor authentication", "available-providers": "Available providers", + "available-providers-required": "At least one 2FA provider should be configured.", "issuer-name": "Issuer name", "issuer-name-required": "Issuer name is required.", "max-verification-failures-before-user-lockout": "Max verification failures before user lockout", From 0a352839d31b208d10e65a8b69c090ec9e082c70 Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 19 Nov 2025 18:07:11 +0200 Subject: [PATCH 3/5] UI: Add error msg to send code card for twoFA enforce --- ...force-two-factor-auth-login.component.html | 15 +++++-- .../force-two-factor-auth-login.component.ts | 42 ++++++++++++------- .../assets/locale/locale.constant-en_US.json | 2 + 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 39236ef8e2..94bd0b2cf0 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -262,9 +262,18 @@ inputmode="numeric" pattern="[0-9]*" autocomplete="off" placeholder="{{ 'login.verification-code' | translate }}"> - - {{ 'login.verification-code-invalid' | translate }} - + @if (configForm.get('verificationCode').getError('required') || + configForm.get('verificationCode').getError('minlength') || + configForm.get('verificationCode').getError('maxlength') || + configForm.get('verificationCode').getError('pattern')) { + {{ 'login.verification-code-invalid' | translate }} + } + @if (configForm.get('verificationCode').getError('incorrectCode')) { + {{ 'login.verification-code-incorrect' | translate }} + } + @if (configForm.get('verificationCode').getError('tooManyRequest')) { + {{ 'login.verification-code-many-request' | translate }} + }