Skip to content

Commit

Permalink
Merge pull request thingsboard#10881 from YevhenBondarenko/feature/ap…
Browse files Browse the repository at this point in the history
…p-secret-upgrade

[Mobile App] added upgrade script for update mobile secretApp
  • Loading branch information
ViacheslavKlimov authored May 29, 2024
2 parents bbc6b9a + 7cb87e8 commit f8c0414
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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")
Expand All @@ -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;

Expand Down Expand Up @@ -269,29 +273,28 @@ 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;

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;
}
Expand All @@ -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<OAuth2Mobile> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public interface SystemDataLoaderService {

void createRandomJwtSettings() throws Exception;

void updateJwtSettings() throws Exception;
void updateSecuritySettings() throws Exception;

void createOAuth2Templates() throws Exception;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
<div fxFlex fxLayout="column">
<mat-form-field fxFlex class="mat-block" floatLabel="always">
<mat-form-field fxFlex class="mat-block" floatLabel="always" subscriptSizing="dynamic">
<mat-label translate>admin.oauth2.mobile-package</mat-label>
<input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required>
<mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint>
Expand All @@ -166,9 +166,9 @@
</mat-error>
</div>
<div fxFlex fxLayout="row">
<mat-form-field fxFlex class="mat-block">
<mat-form-field fxFlex class="mat-block" subscriptSizing="dynamic">
<mat-label translate>admin.oauth2.mobile-app-secret</mat-label>
<textarea matInput formControlName="appSecret" rows="1" required></textarea>
<input matInput formControlName="appSecret" required>
<tb-copy-button
matSuffix
miniButton="false"
Expand All @@ -178,8 +178,15 @@
tooltipPosition="above"
icon="mdi:clipboard-arrow-left">
</tb-copy-button>
<mat-error *ngIf="mobileInfo.get('appSecret').invalid">
{{ 'admin.oauth2.invalid-mobile-app-secret' | translate }}
<mat-hint translate>admin.oauth2.mobile-app-secret-hint</mat-hint>
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('required')">
{{ 'admin.oauth2.mobile-app-secret-required' | translate }}
</mat-error>
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('base64')">
{{ 'admin.oauth2.mobile-app-secret-min-length' | translate }}
</mat-error>
<mat-error *ngIf="mobileInfo.get('appSecret').hasError('minLength')">
{{ 'admin.oauth2.mobile-app-secret-base64' | translate }}
</mat-error>
</mat-form-field>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import {
AbstractControl,
FormControl,
UntypedFormArray,
UntypedFormBuilder,
UntypedFormGroup,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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))
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions ui-ngx/src/assets/locale/locale.constant-en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit f8c0414

Please sign in to comment.