diff --git a/application/src/main/data/upgrade/3.2.2/schema_update.sql b/application/src/main/data/upgrade/3.2.2/schema_update.sql index 30bc80a458..d57668de0d 100644 --- a/application/src/main/data/upgrade/3.2.2/schema_update.sql +++ b/application/src/main/data/upgrade/3.2.2/schema_update.sql @@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, pkg_name varchar(255), - callback_url_scheme varchar(255), + app_secret varchar(2048), CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) ); diff --git a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java index bc93253a73..26c230455b 100644 --- a/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java +++ b/application/src/main/java/org/thingsboard/server/config/CustomOAuth2AuthorizationRequestResolver.java @@ -39,6 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder; import org.thingsboard.server.dao.oauth2.OAuth2Configuration; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; +import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; import org.thingsboard.server.utils.MiscUtils; import javax.servlet.http.HttpServletRequest; @@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza @Autowired private OAuth2Service oAuth2Service; + @Autowired + private OAuth2AppTokenFactory oAuth2AppTokenFactory; + @Autowired(required = false) private OAuth2Configuration oauth2Configuration; @@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza String registrationId = this.resolveRegistrationId(request); String redirectUriAction = getAction(request, "login"); String appPackage = getAppPackage(request); - return resolve(request, registrationId, redirectUriAction, appPackage); + String appToken = getAppToken(request); + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); } @Override @@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza } String redirectUriAction = getAction(request, "authorize"); String appPackage = getAppPackage(request); - return resolve(request, registrationId, redirectUriAction, appPackage); + String appToken = getAppToken(request); + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); } private String getAction(HttpServletRequest request, String defaultAction) { @@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza return request.getParameter("pkg"); } + private String getAppToken(HttpServletRequest request) { + return request.getParameter("appToken"); + } + @SuppressWarnings("deprecation") - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage) { + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) { if (registrationId == null) { return null; } @@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza Map attributes = new HashMap<>(); attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); if (!StringUtils.isEmpty(appPackage)) { - String callbackUrlScheme = this.oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), appPackage); - if (StringUtils.isEmpty(callbackUrlScheme)) { - throw new IllegalArgumentException("Invalid package: " + appPackage + ". No package info found for Client Registration."); + if (StringUtils.isEmpty(appToken)) { + throw new IllegalArgumentException("Invalid application token."); } else { + String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage); + if (StringUtils.isEmpty(appSecret)) { + throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package."); + } + String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret); attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java new file mode 100644 index 0000000000..fba1598a7f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java @@ -0,0 +1,69 @@ +/** + * Copyright © 2016-2021 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. + */ +package org.thingsboard.server.service.security.model.token; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.micrometer.core.instrument.util.StringUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +@Component +@Slf4j +public class OAuth2AppTokenFactory { + + private static final String CALLBACK_URL_SCHEME = "callbackUrlScheme"; + + private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5); + + public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { + Jws jwsClaims; + try { + jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); + } + catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { + throw new IllegalArgumentException("Invalid Application token: ", ex); + } catch (ExpiredJwtException expiredEx) { + throw new IllegalArgumentException("Application token expired", expiredEx); + } + Claims claims = jwsClaims.getBody(); + Date expiration = claims.getExpiration(); + if (expiration == null) { + throw new IllegalArgumentException("Application token must have expiration date"); + } + long timeDiff = expiration.getTime() - System.currentTimeMillis(); + if (timeDiff > MAX_EXPIRATION_TIME_DIFF_MS) { + throw new IllegalArgumentException("Application token expiration time can't be longer than 5 minutes"); + } + if (!claims.getIssuer().equals(appPackage)) { + throw new IllegalArgumentException("Application token issuer doesn't match application package"); + } + String callbackUrlScheme = claims.get(CALLBACK_URL_SCHEME, String.class); + if (StringUtils.isEmpty(callbackUrlScheme)) { + throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme"); + } + return callbackUrlScheme; + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java index 0e9a36c05e..9764137cf2 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Service.java @@ -42,5 +42,5 @@ public interface OAuth2Service { List findAllRegistrations(); - String findCallbackUrlScheme(UUID registrationId, String pkgName); + String findAppSecret(UUID registrationId, String pkgName); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java index b3249a1ed5..53a6d41e5c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2Mobile.java @@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData { private OAuth2ParamsId oauth2ParamsId; private String pkgName; - private String callbackUrlScheme; + private String appSecret; public OAuth2Mobile(OAuth2Mobile mobile) { super(mobile); this.oauth2ParamsId = mobile.oauth2ParamsId; this.pkgName = mobile.pkgName; - this.callbackUrlScheme = mobile.callbackUrlScheme; + this.appSecret = mobile.appSecret; } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java index 18fbd49dcd..c04a1a9fd1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/oauth2/OAuth2MobileInfo.java @@ -30,5 +30,5 @@ import lombok.ToString; @Builder public class OAuth2MobileInfo { private String pkgName; - private String callbackUrlScheme; + private String appSecret; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 32b5afc9aa..87cee53991 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -418,7 +418,7 @@ public class ModelConstants { public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile"; public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name"; - public static final String OAUTH2_CALLBACK_URL_SCHEME_PROPERTY = "callback_url_scheme"; + public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret"; public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java index 403f7af958..a6517011b0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/OAuth2MobileEntity.java @@ -40,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity { @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) private String pkgName; - @Column(name = ModelConstants.OAUTH2_CALLBACK_URL_SCHEME_PROPERTY) - private String callbackUrlScheme; + @Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY) + private String appSecret; public OAuth2MobileEntity() { super(); @@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity { this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); } this.pkgName = mobile.getPkgName(); - this.callbackUrlScheme = mobile.getCallbackUrlScheme(); + this.appSecret = mobile.getAppSecret(); } @Override @@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity { mobile.setCreatedTime(createdTime); mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); mobile.setPkgName(pkgName); - mobile.setCallbackUrlScheme(callbackUrlScheme); + mobile.setAppSecret(appSecret); return mobile; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java index de80334577..87f0d56460 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2RegistrationDao.java @@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao { List findByOAuth2ParamsId(UUID oauth2ParamsId); - String findCallbackUrlScheme(UUID id, String pkgName); + String findAppSecret(UUID id, String pkgName); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java index 6c2126a005..83a134ea36 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2ServiceImpl.java @@ -21,7 +21,23 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.oauth2.*; +import org.thingsboard.server.common.data.oauth2.MapperType; +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Info; +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Params; +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; +import org.thingsboard.server.common.data.oauth2.PlatformType; +import org.thingsboard.server.common.data.oauth2.SchemeType; +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; @@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; import javax.transaction.Transactional; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se } @Override - public String findCallbackUrlScheme(UUID id, String pkgName) { - log.trace("Executing findCallbackUrlScheme [{}][{}]", id, pkgName); + public String findAppSecret(UUID id, String pkgName) { + log.trace("Executing findAppSecret [{}][{}]", id, pkgName); validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); validateString(pkgName, "Incorrect package name"); - return oauth2RegistrationDao.findCallbackUrlScheme(id, pkgName); + return oauth2RegistrationDao.findAppSecret(id, pkgName); } @@ -323,8 +343,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se if (StringUtils.isEmpty(mobileInfo.getPkgName())) { throw new DataValidationException("Package should be specified!"); } - if (StringUtils.isEmpty(mobileInfo.getCallbackUrlScheme())) { - throw new DataValidationException("Callback URL scheme should be specified!"); + if (StringUtils.isEmpty(mobileInfo.getAppSecret())) { + throw new DataValidationException("Application secret should be specified!"); + } + if (mobileInfo.getAppSecret().length() < 16) { + throw new DataValidationException("Application secret should be at least 16 characters!"); } } oauth2Params.getMobileInfos().stream() diff --git a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java index 896c619c82..c34875d014 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java +++ b/dao/src/main/java/org/thingsboard/server/dao/oauth2/OAuth2Utils.java @@ -148,7 +148,7 @@ public class OAuth2Utils { public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { return OAuth2MobileInfo.builder() .pkgName(mobile.getPkgName()) - .callbackUrlScheme(mobile.getCallbackUrlScheme()) + .appSecret(mobile.getAppSecret()) .build(); } @@ -191,7 +191,7 @@ public class OAuth2Utils { OAuth2Mobile mobile = new OAuth2Mobile(); mobile.setOauth2ParamsId(oauth2ParamsId); mobile.setPkgName(mobileInfo.getPkgName()); - mobile.setCallbackUrlScheme(mobileInfo.getCallbackUrlScheme()); + mobile.setAppSecret(mobileInfo.getAppSecret()); return mobile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java index 0b09f7ddea..ef35371b6e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/oauth2/JpaOAuth2RegistrationDao.java @@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao findByOauth2ParamsId(UUID oauth2ParamsId); - @Query("SELECT mobile.callbackUrlScheme " + + @Query("SELECT mobile.appSecret " + "FROM OAuth2MobileEntity mobile " + "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + "WHERE reg.id = :registrationId " + "AND mobile.pkgName = :pkgName") - String findCallbackUrlScheme(@Param("registrationId") UUID id, - @Param("pkgName") String pkgName); + String findAppSecret(@Param("registrationId") UUID id, + @Param("pkgName") String pkgName); } diff --git a/dao/src/main/resources/sql/schema-entities-hsql.sql b/dao/src/main/resources/sql/schema-entities-hsql.sql index ad3a023a41..16522becde 100644 --- a/dao/src/main/resources/sql/schema-entities-hsql.sql +++ b/dao/src/main/resources/sql/schema-entities-hsql.sql @@ -431,7 +431,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, pkg_name varchar(255), - callback_url_scheme varchar(255), + app_secret varchar(2048), CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) ); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 890d639a01..88386a11dd 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -468,7 +468,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( oauth2_params_id uuid NOT NULL, created_time bigint NOT NULL, pkg_name varchar(255), - callback_url_scheme varchar(255), + app_secret varchar(2048), CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) ); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index ae2ff054ec..4077bb2176 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -15,8 +15,8 @@ */ package org.thingsboard.server.dao.service; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { } @Test - public void testFindCallbackUrlScheme() { + public void testFindAppSecret() { OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() .domainInfos(Lists.newArrayList( @@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .mobileInfos(Lists.newArrayList( - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(), - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build() + validMobileInfo("com.test.pkg1", "testPkg1AppSecret"), + validMobileInfo("com.test.pkg2", "testPkg2AppSecret") )) .clientRegistrations(Lists.newArrayList( validRegistrationInfo(), @@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { String[] segments = clientInfo.getUrl().split("/"); String registrationId = segments[segments.length-1]; - String callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg1"); - Assert.assertNotNull(callbackUrlScheme); - Assert.assertEquals("testPkg1Callback", callbackUrlScheme); - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg2"); - Assert.assertNotNull(callbackUrlScheme); - Assert.assertEquals("testPkg2Callback", callbackUrlScheme); - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg3"); - Assert.assertNull(callbackUrlScheme); + String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1"); + Assert.assertNotNull(appSecret); + Assert.assertEquals("testPkg1AppSecret", appSecret); + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2"); + Assert.assertNotNull(appSecret); + Assert.assertEquals("testPkg2AppSecret", appSecret); + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3"); + Assert.assertNull(appSecret); } } @@ -548,8 +548,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() )) .mobileInfos(Lists.newArrayList( - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(), - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build() + validMobileInfo("com.test.pkg1", "testPkg1Callback"), + validMobileInfo("com.test.pkg2", "testPkg2Callback") )) .clientRegistrations(Lists.newArrayList( validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), @@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { ) .build(); } + + private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) { + return OAuth2MobileInfo.builder().pkgName(pkgName) + .appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24)) + .build(); + } } diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index a287802ec2..aa52c55cbe 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -445,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean { export function isMobileApp(): boolean { return isDefined((window as any).flutter_inappwebview); } + +const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; +const alphanumericCharactersLength = alphanumericCharacters.length; + +export function randomAlphanumeric(length: number): string { + let result = ''; + for ( let i = 0; i < length; i++ ) { + result += alphanumericCharacters.charAt(Math.floor(Math.random() * alphanumericCharactersLength)); + } + return result; +} 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 c56dc6a793..a983266434 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 @@ -90,23 +90,26 @@ admin.oauth2.redirect-uri-template - + + - - + + @@ -144,18 +147,32 @@
- + admin.oauth2.mobile-package - + + admin.oauth2.mobile-package-hint {{ 'admin.oauth2.mobile-package-unique' | translate }}
- - admin.oauth2.mobile-callback-url-scheme - - +
+ + admin.oauth2.mobile-app-secret + + + + + {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }} + + +
diff --git a/ui-ngx/src/app/shared/components/button/copy-button.component.ts b/ui-ngx/src/app/shared/components/button/copy-button.component.ts index 8d5d72efe3..60f5db12e0 100644 --- a/ui-ngx/src/app/shared/components/button/copy-button.component.ts +++ b/ui-ngx/src/app/shared/components/button/copy-button.component.ts @@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core'; }) export class CopyButtonComponent { - private copedIcon = ''; private timer; copied = false; @@ -52,6 +51,9 @@ export class CopyButtonComponent { @Input() style: {[key: string]: any} = {}; + @Input() + color: string; + @Output() successCopied = new EventEmitter(); @@ -67,23 +69,13 @@ export class CopyButtonComponent { } this.clipboardService.copy(this.copyText); this.successCopied.emit(this.copyText); - this.copedIcon = 'done'; this.copied = true; this.timer = setTimeout(() => { - this.copedIcon = null; this.copied = false; this.cd.detectChanges(); }, 1500); } - get iconSymbol(): string { - return this.copedIcon || this.icon; - } - - get mdiIconSymbol(): string { - return this.copedIcon ? '' : this.mdiIcon; - } - get matTooltipText(): string { return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; } diff --git a/ui-ngx/src/app/shared/models/oauth2.models.ts b/ui-ngx/src/app/shared/models/oauth2.models.ts index d334e89bdd..b434302a9a 100644 --- a/ui-ngx/src/app/shared/models/oauth2.models.ts +++ b/ui-ngx/src/app/shared/models/oauth2.models.ts @@ -34,7 +34,7 @@ export interface OAuth2DomainInfo { export interface OAuth2MobileInfo { pkgName: string; - callbackUrlScheme: string; + appSecret: string; } export enum DomainSchema{ 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 abe4cba802..f6112713f4 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -224,8 +224,12 @@ "mobile-apps": "Mobile applications", "no-mobile-apps": "No applications configured", "mobile-package": "Application package", + "mobile-package-placeholder": "Ex.: my.example.app", + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", "mobile-package-unique": "Application package must be unique.", - "mobile-callback-url-scheme": "Callback URL scheme", + "mobile-app-secret": "Application secret", + "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", "delete-mobile-app": "Delete application info", "providers": "Providers",