Commit 1ed624d30bf67c7f893e21bb1d643e480b43e551
1 parent
dbc3ef95
Introduce mobile app oauth2 request authentication using application token signe…
…d by application secret.
Showing
23 changed files
with
226 additions
and
84 deletions
... | ... | @@ -136,7 +136,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( |
136 | 136 | oauth2_params_id uuid NOT NULL, |
137 | 137 | created_time bigint NOT NULL, |
138 | 138 | pkg_name varchar(255), |
139 | - callback_url_scheme varchar(255), | |
139 | + app_secret varchar(2048), | |
140 | 140 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
141 | 141 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) |
142 | 142 | ); | ... | ... |
... | ... | @@ -39,6 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder; |
39 | 39 | import org.thingsboard.server.dao.oauth2.OAuth2Configuration; |
40 | 40 | import org.thingsboard.server.dao.oauth2.OAuth2Service; |
41 | 41 | import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; |
42 | +import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; | |
42 | 43 | import org.thingsboard.server.utils.MiscUtils; |
43 | 44 | |
44 | 45 | import javax.servlet.http.HttpServletRequest; |
... | ... | @@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza |
69 | 70 | @Autowired |
70 | 71 | private OAuth2Service oAuth2Service; |
71 | 72 | |
73 | + @Autowired | |
74 | + private OAuth2AppTokenFactory oAuth2AppTokenFactory; | |
75 | + | |
72 | 76 | @Autowired(required = false) |
73 | 77 | private OAuth2Configuration oauth2Configuration; |
74 | 78 | |
... | ... | @@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza |
78 | 82 | String registrationId = this.resolveRegistrationId(request); |
79 | 83 | String redirectUriAction = getAction(request, "login"); |
80 | 84 | String appPackage = getAppPackage(request); |
81 | - return resolve(request, registrationId, redirectUriAction, appPackage); | |
85 | + String appToken = getAppToken(request); | |
86 | + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); | |
82 | 87 | } |
83 | 88 | |
84 | 89 | @Override |
... | ... | @@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza |
88 | 93 | } |
89 | 94 | String redirectUriAction = getAction(request, "authorize"); |
90 | 95 | String appPackage = getAppPackage(request); |
91 | - return resolve(request, registrationId, redirectUriAction, appPackage); | |
96 | + String appToken = getAppToken(request); | |
97 | + return resolve(request, registrationId, redirectUriAction, appPackage, appToken); | |
92 | 98 | } |
93 | 99 | |
94 | 100 | private String getAction(HttpServletRequest request, String defaultAction) { |
... | ... | @@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza |
103 | 109 | return request.getParameter("pkg"); |
104 | 110 | } |
105 | 111 | |
112 | + private String getAppToken(HttpServletRequest request) { | |
113 | + return request.getParameter("appToken"); | |
114 | + } | |
115 | + | |
106 | 116 | @SuppressWarnings("deprecation") |
107 | - private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage) { | |
117 | + private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction, String appPackage, String appToken) { | |
108 | 118 | if (registrationId == null) { |
109 | 119 | return null; |
110 | 120 | } |
... | ... | @@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza |
117 | 127 | Map<String, Object> attributes = new HashMap<>(); |
118 | 128 | attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); |
119 | 129 | if (!StringUtils.isEmpty(appPackage)) { |
120 | - String callbackUrlScheme = this.oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), appPackage); | |
121 | - if (StringUtils.isEmpty(callbackUrlScheme)) { | |
122 | - throw new IllegalArgumentException("Invalid package: " + appPackage + ". No package info found for Client Registration."); | |
130 | + if (StringUtils.isEmpty(appToken)) { | |
131 | + throw new IllegalArgumentException("Invalid application token."); | |
123 | 132 | } else { |
133 | + String appSecret = this.oAuth2Service.findAppSecret(UUID.fromString(registrationId), appPackage); | |
134 | + if (StringUtils.isEmpty(appSecret)) { | |
135 | + throw new IllegalArgumentException("Invalid package: " + appPackage + ". No application secret found for Client Registration with given application package."); | |
136 | + } | |
137 | + String callbackUrlScheme = this.oAuth2AppTokenFactory.validateTokenAndGetCallbackUrlScheme(appPackage, appToken, appSecret); | |
124 | 138 | attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme); |
125 | 139 | } |
126 | 140 | } | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2021 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.service.security.model.token; | |
17 | + | |
18 | +import io.jsonwebtoken.Claims; | |
19 | +import io.jsonwebtoken.ExpiredJwtException; | |
20 | +import io.jsonwebtoken.Jws; | |
21 | +import io.jsonwebtoken.Jwts; | |
22 | +import io.jsonwebtoken.MalformedJwtException; | |
23 | +import io.jsonwebtoken.SignatureException; | |
24 | +import io.jsonwebtoken.UnsupportedJwtException; | |
25 | +import io.micrometer.core.instrument.util.StringUtils; | |
26 | +import lombok.extern.slf4j.Slf4j; | |
27 | +import org.springframework.stereotype.Component; | |
28 | + | |
29 | +import java.util.Date; | |
30 | +import java.util.concurrent.TimeUnit; | |
31 | + | |
32 | +@Component | |
33 | +@Slf4j | |
34 | +public class OAuth2AppTokenFactory { | |
35 | + | |
36 | + private static final String CALLBACK_URL_SCHEME = "callbackUrlScheme"; | |
37 | + | |
38 | + private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5); | |
39 | + | |
40 | + public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { | |
41 | + Jws<Claims> jwsClaims; | |
42 | + try { | |
43 | + jwsClaims = Jwts.parser().setSigningKey(appSecret).parseClaimsJws(appToken); | |
44 | + } | |
45 | + catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException | SignatureException ex) { | |
46 | + throw new IllegalArgumentException("Invalid Application token: ", ex); | |
47 | + } catch (ExpiredJwtException expiredEx) { | |
48 | + throw new IllegalArgumentException("Application token expired", expiredEx); | |
49 | + } | |
50 | + Claims claims = jwsClaims.getBody(); | |
51 | + Date expiration = claims.getExpiration(); | |
52 | + if (expiration == null) { | |
53 | + throw new IllegalArgumentException("Application token must have expiration date"); | |
54 | + } | |
55 | + long timeDiff = expiration.getTime() - System.currentTimeMillis(); | |
56 | + if (timeDiff > MAX_EXPIRATION_TIME_DIFF_MS) { | |
57 | + throw new IllegalArgumentException("Application token expiration time can't be longer than 5 minutes"); | |
58 | + } | |
59 | + if (!claims.getIssuer().equals(appPackage)) { | |
60 | + throw new IllegalArgumentException("Application token issuer doesn't match application package"); | |
61 | + } | |
62 | + String callbackUrlScheme = claims.get(CALLBACK_URL_SCHEME, String.class); | |
63 | + if (StringUtils.isEmpty(callbackUrlScheme)) { | |
64 | + throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme"); | |
65 | + } | |
66 | + return callbackUrlScheme; | |
67 | + } | |
68 | + | |
69 | +} | ... | ... |
... | ... | @@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> { |
31 | 31 | |
32 | 32 | private OAuth2ParamsId oauth2ParamsId; |
33 | 33 | private String pkgName; |
34 | - private String callbackUrlScheme; | |
34 | + private String appSecret; | |
35 | 35 | |
36 | 36 | public OAuth2Mobile(OAuth2Mobile mobile) { |
37 | 37 | super(mobile); |
38 | 38 | this.oauth2ParamsId = mobile.oauth2ParamsId; |
39 | 39 | this.pkgName = mobile.pkgName; |
40 | - this.callbackUrlScheme = mobile.callbackUrlScheme; | |
40 | + this.appSecret = mobile.appSecret; | |
41 | 41 | } |
42 | 42 | } | ... | ... |
... | ... | @@ -418,7 +418,7 @@ public class ModelConstants { |
418 | 418 | public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile"; |
419 | 419 | public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; |
420 | 420 | public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name"; |
421 | - public static final String OAUTH2_CALLBACK_URL_SCHEME_PROPERTY = "callback_url_scheme"; | |
421 | + public static final String OAUTH2_APP_SECRET_PROPERTY = "app_secret"; | |
422 | 422 | |
423 | 423 | public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; |
424 | 424 | public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; | ... | ... |
... | ... | @@ -40,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { |
40 | 40 | @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) |
41 | 41 | private String pkgName; |
42 | 42 | |
43 | - @Column(name = ModelConstants.OAUTH2_CALLBACK_URL_SCHEME_PROPERTY) | |
44 | - private String callbackUrlScheme; | |
43 | + @Column(name = ModelConstants.OAUTH2_APP_SECRET_PROPERTY) | |
44 | + private String appSecret; | |
45 | 45 | |
46 | 46 | public OAuth2MobileEntity() { |
47 | 47 | super(); |
... | ... | @@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { |
56 | 56 | this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); |
57 | 57 | } |
58 | 58 | this.pkgName = mobile.getPkgName(); |
59 | - this.callbackUrlScheme = mobile.getCallbackUrlScheme(); | |
59 | + this.appSecret = mobile.getAppSecret(); | |
60 | 60 | } |
61 | 61 | |
62 | 62 | @Override |
... | ... | @@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { |
66 | 66 | mobile.setCreatedTime(createdTime); |
67 | 67 | mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); |
68 | 68 | mobile.setPkgName(pkgName); |
69 | - mobile.setCallbackUrlScheme(callbackUrlScheme); | |
69 | + mobile.setAppSecret(appSecret); | |
70 | 70 | return mobile; |
71 | 71 | } |
72 | 72 | } | ... | ... |
... | ... | @@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> { |
29 | 29 | |
30 | 30 | List<OAuth2Registration> findByOAuth2ParamsId(UUID oauth2ParamsId); |
31 | 31 | |
32 | - String findCallbackUrlScheme(UUID id, String pkgName); | |
32 | + String findAppSecret(UUID id, String pkgName); | |
33 | 33 | |
34 | 34 | } | ... | ... |
... | ... | @@ -21,7 +21,23 @@ import org.springframework.stereotype.Service; |
21 | 21 | import org.springframework.util.StringUtils; |
22 | 22 | import org.thingsboard.server.common.data.BaseData; |
23 | 23 | import org.thingsboard.server.common.data.id.TenantId; |
24 | -import org.thingsboard.server.common.data.oauth2.*; | |
24 | +import org.thingsboard.server.common.data.oauth2.MapperType; | |
25 | +import org.thingsboard.server.common.data.oauth2.OAuth2BasicMapperConfig; | |
26 | +import org.thingsboard.server.common.data.oauth2.OAuth2ClientInfo; | |
27 | +import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; | |
28 | +import org.thingsboard.server.common.data.oauth2.OAuth2Domain; | |
29 | +import org.thingsboard.server.common.data.oauth2.OAuth2DomainInfo; | |
30 | +import org.thingsboard.server.common.data.oauth2.OAuth2Info; | |
31 | +import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig; | |
32 | +import org.thingsboard.server.common.data.oauth2.OAuth2Mobile; | |
33 | +import org.thingsboard.server.common.data.oauth2.OAuth2MobileInfo; | |
34 | +import org.thingsboard.server.common.data.oauth2.OAuth2Params; | |
35 | +import org.thingsboard.server.common.data.oauth2.OAuth2ParamsInfo; | |
36 | +import org.thingsboard.server.common.data.oauth2.OAuth2Registration; | |
37 | +import org.thingsboard.server.common.data.oauth2.OAuth2RegistrationInfo; | |
38 | +import org.thingsboard.server.common.data.oauth2.PlatformType; | |
39 | +import org.thingsboard.server.common.data.oauth2.SchemeType; | |
40 | +import org.thingsboard.server.common.data.oauth2.TenantNameStrategyType; | |
25 | 41 | import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; |
26 | 42 | import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; |
27 | 43 | import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; |
... | ... | @@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; |
36 | 52 | import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; |
37 | 53 | |
38 | 54 | import javax.transaction.Transactional; |
39 | -import java.util.*; | |
55 | +import java.util.ArrayList; | |
56 | +import java.util.Arrays; | |
57 | +import java.util.Comparator; | |
58 | +import java.util.List; | |
59 | +import java.util.UUID; | |
40 | 60 | import java.util.function.Consumer; |
41 | 61 | import java.util.stream.Collectors; |
42 | 62 | |
... | ... | @@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se |
164 | 184 | } |
165 | 185 | |
166 | 186 | @Override |
167 | - public String findCallbackUrlScheme(UUID id, String pkgName) { | |
168 | - log.trace("Executing findCallbackUrlScheme [{}][{}]", id, pkgName); | |
187 | + public String findAppSecret(UUID id, String pkgName) { | |
188 | + log.trace("Executing findAppSecret [{}][{}]", id, pkgName); | |
169 | 189 | validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); |
170 | 190 | validateString(pkgName, "Incorrect package name"); |
171 | - return oauth2RegistrationDao.findCallbackUrlScheme(id, pkgName); | |
191 | + return oauth2RegistrationDao.findAppSecret(id, pkgName); | |
172 | 192 | } |
173 | 193 | |
174 | 194 | |
... | ... | @@ -323,8 +343,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se |
323 | 343 | if (StringUtils.isEmpty(mobileInfo.getPkgName())) { |
324 | 344 | throw new DataValidationException("Package should be specified!"); |
325 | 345 | } |
326 | - if (StringUtils.isEmpty(mobileInfo.getCallbackUrlScheme())) { | |
327 | - throw new DataValidationException("Callback URL scheme should be specified!"); | |
346 | + if (StringUtils.isEmpty(mobileInfo.getAppSecret())) { | |
347 | + throw new DataValidationException("Application secret should be specified!"); | |
348 | + } | |
349 | + if (mobileInfo.getAppSecret().length() < 16) { | |
350 | + throw new DataValidationException("Application secret should be at least 16 characters!"); | |
328 | 351 | } |
329 | 352 | } |
330 | 353 | oauth2Params.getMobileInfos().stream() | ... | ... |
... | ... | @@ -148,7 +148,7 @@ public class OAuth2Utils { |
148 | 148 | public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { |
149 | 149 | return OAuth2MobileInfo.builder() |
150 | 150 | .pkgName(mobile.getPkgName()) |
151 | - .callbackUrlScheme(mobile.getCallbackUrlScheme()) | |
151 | + .appSecret(mobile.getAppSecret()) | |
152 | 152 | .build(); |
153 | 153 | } |
154 | 154 | |
... | ... | @@ -191,7 +191,7 @@ public class OAuth2Utils { |
191 | 191 | OAuth2Mobile mobile = new OAuth2Mobile(); |
192 | 192 | mobile.setOauth2ParamsId(oauth2ParamsId); |
193 | 193 | mobile.setPkgName(mobileInfo.getPkgName()); |
194 | - mobile.setCallbackUrlScheme(mobileInfo.getCallbackUrlScheme()); | |
194 | + mobile.setAppSecret(mobileInfo.getAppSecret()); | |
195 | 195 | return mobile; |
196 | 196 | } |
197 | 197 | ... | ... |
... | ... | @@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE |
57 | 57 | } |
58 | 58 | |
59 | 59 | @Override |
60 | - public String findCallbackUrlScheme(UUID id, String pkgName) { | |
61 | - return repository.findCallbackUrlScheme(id, pkgName); | |
60 | + public String findAppSecret(UUID id, String pkgName) { | |
61 | + return repository.findAppSecret(id, pkgName); | |
62 | 62 | } |
63 | 63 | |
64 | 64 | } | ... | ... |
... | ... | @@ -42,12 +42,12 @@ public interface OAuth2RegistrationRepository extends CrudRepository<OAuth2Regis |
42 | 42 | |
43 | 43 | List<OAuth2RegistrationEntity> findByOauth2ParamsId(UUID oauth2ParamsId); |
44 | 44 | |
45 | - @Query("SELECT mobile.callbackUrlScheme " + | |
45 | + @Query("SELECT mobile.appSecret " + | |
46 | 46 | "FROM OAuth2MobileEntity mobile " + |
47 | 47 | "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + |
48 | 48 | "WHERE reg.id = :registrationId " + |
49 | 49 | "AND mobile.pkgName = :pkgName") |
50 | - String findCallbackUrlScheme(@Param("registrationId") UUID id, | |
51 | - @Param("pkgName") String pkgName); | |
50 | + String findAppSecret(@Param("registrationId") UUID id, | |
51 | + @Param("pkgName") String pkgName); | |
52 | 52 | |
53 | 53 | } | ... | ... |
... | ... | @@ -431,7 +431,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( |
431 | 431 | oauth2_params_id uuid NOT NULL, |
432 | 432 | created_time bigint NOT NULL, |
433 | 433 | pkg_name varchar(255), |
434 | - callback_url_scheme varchar(255), | |
434 | + app_secret varchar(2048), | |
435 | 435 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
436 | 436 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) |
437 | 437 | ); | ... | ... |
... | ... | @@ -468,7 +468,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( |
468 | 468 | oauth2_params_id uuid NOT NULL, |
469 | 469 | created_time bigint NOT NULL, |
470 | 470 | pkg_name varchar(255), |
471 | - callback_url_scheme varchar(255), | |
471 | + app_secret varchar(2048), | |
472 | 472 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
473 | 473 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) |
474 | 474 | ); | ... | ... |
... | ... | @@ -15,8 +15,8 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.dao.service; |
17 | 17 | |
18 | -import com.fasterxml.jackson.databind.node.ObjectNode; | |
19 | 18 | import com.google.common.collect.Lists; |
19 | +import org.apache.commons.lang3.RandomStringUtils; | |
20 | 20 | import org.junit.After; |
21 | 21 | import org.junit.Assert; |
22 | 22 | import org.junit.Before; |
... | ... | @@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { |
487 | 487 | } |
488 | 488 | |
489 | 489 | @Test |
490 | - public void testFindCallbackUrlScheme() { | |
490 | + public void testFindAppSecret() { | |
491 | 491 | OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( |
492 | 492 | OAuth2ParamsInfo.builder() |
493 | 493 | .domainInfos(Lists.newArrayList( |
... | ... | @@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { |
496 | 496 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() |
497 | 497 | )) |
498 | 498 | .mobileInfos(Lists.newArrayList( |
499 | - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(), | |
500 | - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build() | |
499 | + validMobileInfo("com.test.pkg1", "testPkg1AppSecret"), | |
500 | + validMobileInfo("com.test.pkg2", "testPkg2AppSecret") | |
501 | 501 | )) |
502 | 502 | .clientRegistrations(Lists.newArrayList( |
503 | 503 | validRegistrationInfo(), |
... | ... | @@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { |
527 | 527 | for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { |
528 | 528 | String[] segments = clientInfo.getUrl().split("/"); |
529 | 529 | String registrationId = segments[segments.length-1]; |
530 | - String callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg1"); | |
531 | - Assert.assertNotNull(callbackUrlScheme); | |
532 | - Assert.assertEquals("testPkg1Callback", callbackUrlScheme); | |
533 | - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg2"); | |
534 | - Assert.assertNotNull(callbackUrlScheme); | |
535 | - Assert.assertEquals("testPkg2Callback", callbackUrlScheme); | |
536 | - callbackUrlScheme = oAuth2Service.findCallbackUrlScheme(UUID.fromString(registrationId), "com.test.pkg3"); | |
537 | - Assert.assertNull(callbackUrlScheme); | |
530 | + String appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg1"); | |
531 | + Assert.assertNotNull(appSecret); | |
532 | + Assert.assertEquals("testPkg1AppSecret", appSecret); | |
533 | + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg2"); | |
534 | + Assert.assertNotNull(appSecret); | |
535 | + Assert.assertEquals("testPkg2AppSecret", appSecret); | |
536 | + appSecret = oAuth2Service.findAppSecret(UUID.fromString(registrationId), "com.test.pkg3"); | |
537 | + Assert.assertNull(appSecret); | |
538 | 538 | } |
539 | 539 | } |
540 | 540 | |
... | ... | @@ -548,8 +548,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { |
548 | 548 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() |
549 | 549 | )) |
550 | 550 | .mobileInfos(Lists.newArrayList( |
551 | - OAuth2MobileInfo.builder().pkgName("com.test.pkg1").callbackUrlScheme("testPkg1Callback").build(), | |
552 | - OAuth2MobileInfo.builder().pkgName("com.test.pkg2").callbackUrlScheme("testPkg2Callback").build() | |
551 | + validMobileInfo("com.test.pkg1", "testPkg1Callback"), | |
552 | + validMobileInfo("com.test.pkg2", "testPkg2Callback") | |
553 | 553 | )) |
554 | 554 | .clientRegistrations(Lists.newArrayList( |
555 | 555 | validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), |
... | ... | @@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { |
651 | 651 | ) |
652 | 652 | .build(); |
653 | 653 | } |
654 | + | |
655 | + private OAuth2MobileInfo validMobileInfo(String pkgName, String appSecret) { | |
656 | + return OAuth2MobileInfo.builder().pkgName(pkgName) | |
657 | + .appSecret(appSecret != null ? appSecret : RandomStringUtils.randomAlphanumeric(24)) | |
658 | + .build(); | |
659 | + } | |
654 | 660 | } | ... | ... |
... | ... | @@ -445,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean { |
445 | 445 | export function isMobileApp(): boolean { |
446 | 446 | return isDefined((window as any).flutter_inappwebview); |
447 | 447 | } |
448 | + | |
449 | +const alphanumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
450 | +const alphanumericCharactersLength = alphanumericCharacters.length; | |
451 | + | |
452 | +export function randomAlphanumeric(length: number): string { | |
453 | + let result = ''; | |
454 | + for ( let i = 0; i < length; i++ ) { | |
455 | + result += alphanumericCharacters.charAt(Math.floor(Math.random() * alphanumericCharactersLength)); | |
456 | + } | |
457 | + return result; | |
458 | +} | ... | ... |
... | ... | @@ -90,23 +90,26 @@ |
90 | 90 | <mat-form-field fxFlex class="mat-block"> |
91 | 91 | <mat-label translate>admin.oauth2.redirect-uri-template</mat-label> |
92 | 92 | <input matInput [value]="redirectURI(domainInfo)" readonly> |
93 | - <button mat-icon-button color="primary" matSuffix type="button" | |
94 | - ngxClipboard cbContent="{{ redirectURI(domainInfo) }}" | |
95 | - matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" | |
96 | - matTooltipPosition="above"> | |
97 | - <mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon> | |
98 | - </button> | |
93 | + <tb-copy-button | |
94 | + matSuffix | |
95 | + color="primary" | |
96 | + [copyText]="redirectURI(domainInfo)" | |
97 | + tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" | |
98 | + tooltipPosition="above" | |
99 | + mdiIcon="mdi:clipboard-arrow-left"> | |
100 | + </tb-copy-button> | |
99 | 101 | </mat-form-field> |
100 | - | |
101 | 102 | <mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block"> |
102 | 103 | <mat-label></mat-label> |
103 | 104 | <input matInput [value]="redirectURIMixed(domainInfo)" readonly> |
104 | - <button mat-icon-button color="primary" matSuffix type="button" | |
105 | - ngxClipboard cbContent="{{ redirectURIMixed(domainInfo) }}" | |
106 | - matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" | |
107 | - matTooltipPosition="above"> | |
108 | - <mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon> | |
109 | - </button> | |
105 | + <tb-copy-button | |
106 | + matSuffix | |
107 | + color="primary" | |
108 | + [copyText]="redirectURIMixed(domainInfo)" | |
109 | + tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}" | |
110 | + tooltipPosition="above" | |
111 | + mdiIcon="mdi:clipboard-arrow-left"> | |
112 | + </tb-copy-button> | |
110 | 113 | </mat-form-field> |
111 | 114 | </div> |
112 | 115 | </div> |
... | ... | @@ -144,18 +147,32 @@ |
144 | 147 | <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> |
145 | 148 | <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> |
146 | 149 | <div fxFlex fxLayout="column"> |
147 | - <mat-form-field fxFlex class="mat-block"> | |
150 | + <mat-form-field fxFlex class="mat-block" floatLabel="always"> | |
148 | 151 | <mat-label translate>admin.oauth2.mobile-package</mat-label> |
149 | - <input matInput formControlName="pkgName" required> | |
152 | + <input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required> | |
153 | + <mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint> | |
150 | 154 | </mat-form-field> |
151 | 155 | <mat-error *ngIf="mobileInfo.hasError('unique')"> |
152 | 156 | {{ 'admin.oauth2.mobile-package-unique' | translate }} |
153 | 157 | </mat-error> |
154 | 158 | </div> |
155 | - <mat-form-field fxFlex class="mat-block"> | |
156 | - <mat-label translate>admin.oauth2.mobile-callback-url-scheme</mat-label> | |
157 | - <input matInput formControlName="callbackUrlScheme" required> | |
158 | - </mat-form-field> | |
159 | + <div fxFlex fxLayout="row"> | |
160 | + <mat-form-field fxFlex class="mat-block"> | |
161 | + <mat-label translate>admin.oauth2.mobile-app-secret</mat-label> | |
162 | + <textarea matInput formControlName="appSecret" rows="1" required></textarea> | |
163 | + <tb-copy-button | |
164 | + matSuffix | |
165 | + color="primary" | |
166 | + [copyText]="mobileInfo.get('appSecret').value" | |
167 | + tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}" | |
168 | + tooltipPosition="above" | |
169 | + mdiIcon="mdi:clipboard-arrow-left"> | |
170 | + </tb-copy-button> | |
171 | + <mat-error *ngIf="mobileInfo.get('appSecret').invalid"> | |
172 | + {{ 'admin.oauth2.invalid-mobile-app-secret' | translate }} | |
173 | + </mat-error> | |
174 | + </mat-form-field> | |
175 | + </div> | |
159 | 176 | </div> |
160 | 177 | <div fxLayout="column" fxLayoutAlign="center start"> |
161 | 178 | <button type="button" mat-icon-button color="primary" | ... | ... |
... | ... | @@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service'; |
42 | 42 | import { forkJoin, Subscription } from 'rxjs'; |
43 | 43 | import { DialogService } from '@core/services/dialog.service'; |
44 | 44 | import { TranslateService } from '@ngx-translate/core'; |
45 | -import { isDefined, isDefinedAndNotNull } from '@core/utils'; | |
45 | +import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; | |
46 | 46 | import { OAuth2Service } from '@core/http/oauth2.service'; |
47 | 47 | import { ActivatedRoute } from '@angular/router'; |
48 | 48 | |
... | ... | @@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
275 | 275 | private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup { |
276 | 276 | return this.fb.group({ |
277 | 277 | pkgName: [mobileInfo?.pkgName, [Validators.required]], |
278 | - callbackUrlScheme: [mobileInfo?.callbackUrlScheme, [Validators.required]], | |
278 | + appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048), | |
279 | + Validators.pattern(/^[A-Za-z0-9]+$/)]], | |
279 | 280 | }, {validators: this.uniquePkgNameValidator}); |
280 | 281 | } |
281 | 282 | |
... | ... | @@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha |
529 | 530 | addMobileInfo(control: AbstractControl): void { |
530 | 531 | this.mobileInfos(control).push(this.buildMobileInfoForm({ |
531 | 532 | pkgName: '', |
532 | - callbackUrlScheme: '' | |
533 | + appSecret: randomAlphanumeric(24) | |
533 | 534 | })); |
534 | 535 | } |
535 | 536 | ... | ... |
... | ... | @@ -16,11 +16,16 @@ |
16 | 16 | |
17 | 17 | --> |
18 | 18 | <button mat-icon-button |
19 | + type="button" | |
20 | + [color]="color" | |
19 | 21 | [disabled]="disabled" |
20 | 22 | [matTooltip]="matTooltipText" |
21 | 23 | [matTooltipPosition]="matTooltipPosition" |
22 | 24 | (click)="copy($event)"> |
23 | - <mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}"> | |
24 | - {{ iconSymbol }} | |
25 | + <mat-icon [svgIcon]="mdiIcon" [ngStyle]="style" *ngIf="!copied; else copiedTemplate"> | |
26 | + {{ icon }} | |
25 | 27 | </mat-icon> |
28 | + <ng-template #copiedTemplate> | |
29 | + <mat-icon [ngStyle]="style" class="copied">done</mat-icon> | |
30 | + </ng-template> | |
26 | 31 | </button> | ... | ... |
... | ... | @@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core'; |
26 | 26 | }) |
27 | 27 | export class CopyButtonComponent { |
28 | 28 | |
29 | - private copedIcon = ''; | |
30 | 29 | private timer; |
31 | 30 | |
32 | 31 | copied = false; |
... | ... | @@ -52,6 +51,9 @@ export class CopyButtonComponent { |
52 | 51 | @Input() |
53 | 52 | style: {[key: string]: any} = {}; |
54 | 53 | |
54 | + @Input() | |
55 | + color: string; | |
56 | + | |
55 | 57 | @Output() |
56 | 58 | successCopied = new EventEmitter<string>(); |
57 | 59 | |
... | ... | @@ -67,23 +69,13 @@ export class CopyButtonComponent { |
67 | 69 | } |
68 | 70 | this.clipboardService.copy(this.copyText); |
69 | 71 | this.successCopied.emit(this.copyText); |
70 | - this.copedIcon = 'done'; | |
71 | 72 | this.copied = true; |
72 | 73 | this.timer = setTimeout(() => { |
73 | - this.copedIcon = null; | |
74 | 74 | this.copied = false; |
75 | 75 | this.cd.detectChanges(); |
76 | 76 | }, 1500); |
77 | 77 | } |
78 | 78 | |
79 | - get iconSymbol(): string { | |
80 | - return this.copedIcon || this.icon; | |
81 | - } | |
82 | - | |
83 | - get mdiIconSymbol(): string { | |
84 | - return this.copedIcon ? '' : this.mdiIcon; | |
85 | - } | |
86 | - | |
87 | 79 | get matTooltipText(): string { |
88 | 80 | return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; |
89 | 81 | } | ... | ... |
... | ... | @@ -224,8 +224,12 @@ |
224 | 224 | "mobile-apps": "Mobile applications", |
225 | 225 | "no-mobile-apps": "No applications configured", |
226 | 226 | "mobile-package": "Application package", |
227 | + "mobile-package-placeholder": "Ex.: my.example.app", | |
228 | + "mobile-package-hint": "For Android: your own unique Application ID. For iOS: Product bundle identifier.", | |
227 | 229 | "mobile-package-unique": "Application package must be unique.", |
228 | - "mobile-callback-url-scheme": "Callback URL scheme", | |
230 | + "mobile-app-secret": "Application secret", | |
231 | + "invalid-mobile-app-secret": "Application secret must contain only alphanumeric characters and must be between 16 and 2048 characters long.", | |
232 | + "copy-mobile-app-secret": "Copy application secret", | |
229 | 233 | "add-mobile-app": "Add application", |
230 | 234 | "delete-mobile-app": "Delete application info", |
231 | 235 | "providers": "Providers", | ... | ... |