diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java index 33e2ddee01f..4bef0d420ad 100644 --- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java +++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java @@ -136,7 +136,7 @@ public void performInstall() { dataUpdateService.updateData("3.6.4"); entityDatabaseSchemaService.createCustomerTitleUniqueConstraintIfNotExists(); systemDataLoaderService.updateDefaultNotificationConfigs(false); - systemDataLoaderService.updateJwtSettings(); + systemDataLoaderService.updateSecuritySettings(); //TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache break; default: diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java index bdd0494443e..7588726a73f 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java @@ -26,6 +26,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -68,6 +69,7 @@ import org.thingsboard.server.common.data.kv.BooleanDataEntry; import org.thingsboard.server.common.data.kv.DoubleDataEntry; import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.BooleanFilterPredicate; @@ -98,6 +100,7 @@ import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationTargetService; +import org.thingsboard.server.dao.oauth2.OAuth2MobileDao; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.rule.RuleChainService; import org.thingsboard.server.dao.settings.AdminSettingsService; @@ -120,7 +123,7 @@ import static org.thingsboard.server.common.data.DataConstants.DEFAULT_DEVICE_TYPE; import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.isSigningKeyDefault; -import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateTokenSigningKeyLength; +import static org.thingsboard.server.service.security.auth.jwt.settings.DefaultJwtSettingsService.validateKeyLength; @Service @Profile("install") @@ -146,6 +149,7 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService { private final DeviceConnectivityConfiguration connectivityConfiguration; private final QueueService queueService; private final JwtSettingsService jwtSettingsService; + private final OAuth2MobileDao oAuth2MobileDao; private final NotificationSettingsService notificationSettingsService; private final NotificationTargetService notificationTargetService; @@ -269,21 +273,20 @@ public void createAdminSettings() throws Exception { @Override public void createRandomJwtSettings() throws Exception { - if (jwtSettingsService.getJwtSettings() == null) { - log.info("Creating JWT admin settings..."); - var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); - if (isSigningKeyDefault(jwtSettings) || !validateTokenSigningKeyLength(jwtSettings)) { - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); - } - jwtSettingsService.saveJwtSettings(jwtSettings); - } else { - log.info("Skip creating JWT admin settings because they already exist."); + if (jwtSettingsService.getJwtSettings() == null) { + log.info("Creating JWT admin settings..."); + var jwtSettings = new JwtSettings(this.tokenExpirationTime, this.refreshTokenExpTime, this.tokenIssuer, this.tokenSigningKey); + if (isSigningKeyDefault(jwtSettings) || !validateKeyLength(jwtSettings.getTokenSigningKey())) { + jwtSettings.setTokenSigningKey(generateRandomKey()); } + jwtSettingsService.saveJwtSettings(jwtSettings); + } else { + log.info("Skip creating JWT admin settings because they already exist."); + } } @Override - public void updateJwtSettings() { + public void updateSecuritySettings() { JwtSettings jwtSettings = jwtSettingsService.getJwtSettings(); boolean invalidSignKey = false; String warningMessage = null; @@ -291,7 +294,7 @@ public void updateJwtSettings() { if (isSigningKeyDefault(jwtSettings)) { warningMessage = "The platform is using the default JWT Signing Key, which is a security risk."; invalidSignKey = true; - } else if (!validateTokenSigningKeyLength(jwtSettings)) { + } else if (!validateKeyLength(jwtSettings.getTokenSigningKey())) { warningMessage = "The JWT Signing Key is shorter than 512 bits, which is a security risk."; invalidSignKey = true; } @@ -301,10 +304,28 @@ public void updateJwtSettings() { "You can change the JWT Signing Key using the Web UI: " + "Navigate to \"System settings -> Security settings\" while logged in as a System Administrator.", warningMessage); - jwtSettings.setTokenSigningKey(Base64.getEncoder().encodeToString( - RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8))); + jwtSettings.setTokenSigningKey(generateRandomKey()); jwtSettingsService.saveJwtSettings(jwtSettings); } + + List mobiles = oAuth2MobileDao.find(TenantId.SYS_TENANT_ID); + if (CollectionUtils.isNotEmpty(mobiles)) { + mobiles.stream() + .filter(config -> !validateKeyLength(config.getAppSecret())) + .forEach(config -> { + log.warn("WARNING: The App secret is shorter than 512 bits, which is a security risk. " + + "A new Application Secret has been added automatically for Mobile Application [{}]. " + + "You can change the Application Secret using the Web UI: " + + "Navigate to \"Security settings -> OAuth2 -> Mobile applications\" while logged in as a System Administrator.", config.getPkgName()); + config.setAppSecret(generateRandomKey()); + oAuth2MobileDao.save(TenantId.SYS_TENANT_ID, config); + }); + } + } + + private String generateRandomKey() { + return Base64.getEncoder().encodeToString( + RandomStringUtils.randomAlphanumeric(64).getBytes(StandardCharsets.UTF_8)); } @Override diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java index 71c829ee117..2f4b5fa8856 100644 --- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java +++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java @@ -25,7 +25,7 @@ public interface SystemDataLoaderService { void createRandomJwtSettings() throws Exception; - void updateJwtSettings() throws Exception; + void updateSecuritySettings() throws Exception; void createOAuth2Templates() throws Exception; diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java index c5c04ac3121..aba7c5b9305 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/settings/DefaultJwtSettingsService.java @@ -17,8 +17,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.cluster.TbClusterService; @@ -111,8 +109,8 @@ public static boolean isSigningKeyDefault(JwtSettings settings) { return TOKEN_SIGNING_KEY_DEFAULT.equals(settings.getTokenSigningKey()); } - public static boolean validateTokenSigningKeyLength(JwtSettings settings) { - return Base64.getDecoder().decode(settings.getTokenSigningKey()).length * Byte.SIZE >= KEY_LENGTH; + public static boolean validateKeyLength(String key) { + return Base64.getDecoder().decode(key).length * Byte.SIZE >= KEY_LENGTH; } } diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html index 2e85f8b54ba..2ea96f63876 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.html @@ -156,7 +156,7 @@
- + admin.oauth2.mobile-package admin.oauth2.mobile-package-hint @@ -166,9 +166,9 @@
- + admin.oauth2.mobile-app-secret - + - - {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }} + admin.oauth2.mobile-app-secret-hint + + {{ 'admin.oauth2.mobile-app-secret-required' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-min-length' | translate }} + + + {{ 'admin.oauth2.mobile-app-secret-base64' | translate }}
diff --git a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts index f6f8340550c..f1eabf2d0c0 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/oauth2-settings.component.ts @@ -17,6 +17,7 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, + FormControl, UntypedFormArray, UntypedFormBuilder, UntypedFormGroup, @@ -215,7 +216,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha this.oauth2SettingsForm.get('edgeEnabled').patchValue(false); this.oauth2SettingsForm.get('edgeEnabled').disable(); } - })) + })); } private initOAuth2Settings(oauth2Info: OAuth2Info): void { @@ -302,11 +303,25 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): UntypedFormGroup { return this.fb.group({ pkgName: [mobileInfo?.pkgName, [Validators.required]], - appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048), - Validators.pattern(/^[A-Za-z0-9]+$/)]], + appSecret: [mobileInfo?.appSecret, [Validators.required, this.base64Format]], }, {validators: this.uniquePkgNameValidator}); } + private base64Format(control: FormControl): { [key: string]: boolean } | null { + if (control.value === '') { + return null; + } + try { + const value = atob(control.value); + if (value.length < 64) { + return {minLength: true}; + } + return null; + } catch (e) { + return {base64: true}; + } + } + private buildRegistrationForm(registration?: OAuth2RegistrationInfo): UntypedFormGroup { let additionalInfo = null; if (isDefinedAndNotNull(registration?.additionalInfo)) { @@ -556,7 +571,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha addMobileInfo(control: AbstractControl): void { this.mobileInfos(control).push(this.buildMobileInfoForm({ pkgName: '', - appSecret: randomAlphanumeric(24) + appSecret: btoa(randomAlphanumeric(64)) })); } diff --git a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts index 17dda4a0c9f..c7ac4897535 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/security-settings.component.ts @@ -201,7 +201,7 @@ export class SecuritySettingsComponent extends PageComponent implements HasConfi } try { const value = atob(control.value); - if (value.length < 32) { + if (value.length < 64) { return {minLength: true}; } return null; 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 e376a20e313..c958c3a99e0 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -292,6 +292,10 @@ "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", "mobile-package-unique": "Application package must be unique.", "mobile-app-secret": "Application secret", + "mobile-app-secret-hint": "Base64 encoded string representing at least 512 bits of data.", + "mobile-app-secret-required": "Application secret is required.", + "mobile-app-secret-min-length": "Application secret must be at least 512 bits of data.", + "mobile-app-secret-base64": "Application secret must be base64 format.", "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", "copy-mobile-app-secret": "Copy application secret", "add-mobile-app": "Add application",