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,7 +136,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( | ||
136 | oauth2_params_id uuid NOT NULL, | 136 | oauth2_params_id uuid NOT NULL, |
137 | created_time bigint NOT NULL, | 137 | created_time bigint NOT NULL, |
138 | pkg_name varchar(255), | 138 | pkg_name varchar(255), |
139 | - callback_url_scheme varchar(255), | 139 | + app_secret varchar(2048), |
140 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, | 140 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
141 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) | 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,6 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder; | ||
39 | import org.thingsboard.server.dao.oauth2.OAuth2Configuration; | 39 | import org.thingsboard.server.dao.oauth2.OAuth2Configuration; |
40 | import org.thingsboard.server.dao.oauth2.OAuth2Service; | 40 | import org.thingsboard.server.dao.oauth2.OAuth2Service; |
41 | import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; | 41 | import org.thingsboard.server.service.security.auth.oauth2.TbOAuth2ParameterNames; |
42 | +import org.thingsboard.server.service.security.model.token.OAuth2AppTokenFactory; | ||
42 | import org.thingsboard.server.utils.MiscUtils; | 43 | import org.thingsboard.server.utils.MiscUtils; |
43 | 44 | ||
44 | import javax.servlet.http.HttpServletRequest; | 45 | import javax.servlet.http.HttpServletRequest; |
@@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | @@ -69,6 +70,9 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | ||
69 | @Autowired | 70 | @Autowired |
70 | private OAuth2Service oAuth2Service; | 71 | private OAuth2Service oAuth2Service; |
71 | 72 | ||
73 | + @Autowired | ||
74 | + private OAuth2AppTokenFactory oAuth2AppTokenFactory; | ||
75 | + | ||
72 | @Autowired(required = false) | 76 | @Autowired(required = false) |
73 | private OAuth2Configuration oauth2Configuration; | 77 | private OAuth2Configuration oauth2Configuration; |
74 | 78 | ||
@@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | @@ -78,7 +82,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | ||
78 | String registrationId = this.resolveRegistrationId(request); | 82 | String registrationId = this.resolveRegistrationId(request); |
79 | String redirectUriAction = getAction(request, "login"); | 83 | String redirectUriAction = getAction(request, "login"); |
80 | String appPackage = getAppPackage(request); | 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 | @Override | 89 | @Override |
@@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | @@ -88,7 +93,8 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | ||
88 | } | 93 | } |
89 | String redirectUriAction = getAction(request, "authorize"); | 94 | String redirectUriAction = getAction(request, "authorize"); |
90 | String appPackage = getAppPackage(request); | 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 | private String getAction(HttpServletRequest request, String defaultAction) { | 100 | private String getAction(HttpServletRequest request, String defaultAction) { |
@@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | @@ -103,8 +109,12 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | ||
103 | return request.getParameter("pkg"); | 109 | return request.getParameter("pkg"); |
104 | } | 110 | } |
105 | 111 | ||
112 | + private String getAppToken(HttpServletRequest request) { | ||
113 | + return request.getParameter("appToken"); | ||
114 | + } | ||
115 | + | ||
106 | @SuppressWarnings("deprecation") | 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 | if (registrationId == null) { | 118 | if (registrationId == null) { |
109 | return null; | 119 | return null; |
110 | } | 120 | } |
@@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | @@ -117,10 +127,14 @@ public class CustomOAuth2AuthorizationRequestResolver implements OAuth2Authoriza | ||
117 | Map<String, Object> attributes = new HashMap<>(); | 127 | Map<String, Object> attributes = new HashMap<>(); |
118 | attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); | 128 | attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); |
119 | if (!StringUtils.isEmpty(appPackage)) { | 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 | } else { | 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 | attributes.put(TbOAuth2ParameterNames.CALLBACK_URL_SCHEME, callbackUrlScheme); | 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 | +} |
@@ -42,5 +42,5 @@ public interface OAuth2Service { | @@ -42,5 +42,5 @@ public interface OAuth2Service { | ||
42 | 42 | ||
43 | List<OAuth2Registration> findAllRegistrations(); | 43 | List<OAuth2Registration> findAllRegistrations(); |
44 | 44 | ||
45 | - String findCallbackUrlScheme(UUID registrationId, String pkgName); | 45 | + String findAppSecret(UUID registrationId, String pkgName); |
46 | } | 46 | } |
@@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> { | @@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> { | ||
31 | 31 | ||
32 | private OAuth2ParamsId oauth2ParamsId; | 32 | private OAuth2ParamsId oauth2ParamsId; |
33 | private String pkgName; | 33 | private String pkgName; |
34 | - private String callbackUrlScheme; | 34 | + private String appSecret; |
35 | 35 | ||
36 | public OAuth2Mobile(OAuth2Mobile mobile) { | 36 | public OAuth2Mobile(OAuth2Mobile mobile) { |
37 | super(mobile); | 37 | super(mobile); |
38 | this.oauth2ParamsId = mobile.oauth2ParamsId; | 38 | this.oauth2ParamsId = mobile.oauth2ParamsId; |
39 | this.pkgName = mobile.pkgName; | 39 | this.pkgName = mobile.pkgName; |
40 | - this.callbackUrlScheme = mobile.callbackUrlScheme; | 40 | + this.appSecret = mobile.appSecret; |
41 | } | 41 | } |
42 | } | 42 | } |
@@ -30,5 +30,5 @@ import lombok.ToString; | @@ -30,5 +30,5 @@ import lombok.ToString; | ||
30 | @Builder | 30 | @Builder |
31 | public class OAuth2MobileInfo { | 31 | public class OAuth2MobileInfo { |
32 | private String pkgName; | 32 | private String pkgName; |
33 | - private String callbackUrlScheme; | 33 | + private String appSecret; |
34 | } | 34 | } |
@@ -418,7 +418,7 @@ public class ModelConstants { | @@ -418,7 +418,7 @@ public class ModelConstants { | ||
418 | public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile"; | 418 | public static final String OAUTH2_MOBILE_COLUMN_FAMILY_NAME = "oauth2_mobile"; |
419 | public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; | 419 | public static final String OAUTH2_PARAMS_ID_PROPERTY = "oauth2_params_id"; |
420 | public static final String OAUTH2_PKG_NAME_PROPERTY = "pkg_name"; | 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 | public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; | 423 | public static final String OAUTH2_CLIENT_REGISTRATION_INFO_COLUMN_FAMILY_NAME = "oauth2_client_registration_info"; |
424 | public static final String OAUTH2_CLIENT_REGISTRATION_COLUMN_FAMILY_NAME = "oauth2_client_registration"; | 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,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { | ||
40 | @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) | 40 | @Column(name = ModelConstants.OAUTH2_PKG_NAME_PROPERTY) |
41 | private String pkgName; | 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 | public OAuth2MobileEntity() { | 46 | public OAuth2MobileEntity() { |
47 | super(); | 47 | super(); |
@@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { | @@ -56,7 +56,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { | ||
56 | this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); | 56 | this.oauth2ParamsId = mobile.getOauth2ParamsId().getId(); |
57 | } | 57 | } |
58 | this.pkgName = mobile.getPkgName(); | 58 | this.pkgName = mobile.getPkgName(); |
59 | - this.callbackUrlScheme = mobile.getCallbackUrlScheme(); | 59 | + this.appSecret = mobile.getAppSecret(); |
60 | } | 60 | } |
61 | 61 | ||
62 | @Override | 62 | @Override |
@@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { | @@ -66,7 +66,7 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> { | ||
66 | mobile.setCreatedTime(createdTime); | 66 | mobile.setCreatedTime(createdTime); |
67 | mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); | 67 | mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId)); |
68 | mobile.setPkgName(pkgName); | 68 | mobile.setPkgName(pkgName); |
69 | - mobile.setCallbackUrlScheme(callbackUrlScheme); | 69 | + mobile.setAppSecret(appSecret); |
70 | return mobile; | 70 | return mobile; |
71 | } | 71 | } |
72 | } | 72 | } |
@@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> { | @@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> { | ||
29 | 29 | ||
30 | List<OAuth2Registration> findByOAuth2ParamsId(UUID oauth2ParamsId); | 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,7 +21,23 @@ import org.springframework.stereotype.Service; | ||
21 | import org.springframework.util.StringUtils; | 21 | import org.springframework.util.StringUtils; |
22 | import org.thingsboard.server.common.data.BaseData; | 22 | import org.thingsboard.server.common.data.BaseData; |
23 | import org.thingsboard.server.common.data.id.TenantId; | 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 | import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; | 41 | import org.thingsboard.server.common.data.oauth2.deprecated.ClientRegistrationDto; |
26 | import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; | 42 | import org.thingsboard.server.common.data.oauth2.deprecated.DomainInfo; |
27 | import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; | 43 | import org.thingsboard.server.common.data.oauth2.deprecated.ExtendedOAuth2ClientRegistrationInfo; |
@@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; | @@ -36,7 +52,11 @@ import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationDao; | ||
36 | import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; | 52 | import org.thingsboard.server.dao.oauth2.deprecated.OAuth2ClientRegistrationInfoDao; |
37 | 53 | ||
38 | import javax.transaction.Transactional; | 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 | import java.util.function.Consumer; | 60 | import java.util.function.Consumer; |
41 | import java.util.stream.Collectors; | 61 | import java.util.stream.Collectors; |
42 | 62 | ||
@@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se | @@ -164,11 +184,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se | ||
164 | } | 184 | } |
165 | 185 | ||
166 | @Override | 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 | validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); | 189 | validateId(id, INCORRECT_CLIENT_REGISTRATION_ID + id); |
170 | validateString(pkgName, "Incorrect package name"); | 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,8 +343,11 @@ public class OAuth2ServiceImpl extends AbstractEntityService implements OAuth2Se | ||
323 | if (StringUtils.isEmpty(mobileInfo.getPkgName())) { | 343 | if (StringUtils.isEmpty(mobileInfo.getPkgName())) { |
324 | throw new DataValidationException("Package should be specified!"); | 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 | oauth2Params.getMobileInfos().stream() | 353 | oauth2Params.getMobileInfos().stream() |
@@ -148,7 +148,7 @@ public class OAuth2Utils { | @@ -148,7 +148,7 @@ public class OAuth2Utils { | ||
148 | public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { | 148 | public static OAuth2MobileInfo toOAuth2MobileInfo(OAuth2Mobile mobile) { |
149 | return OAuth2MobileInfo.builder() | 149 | return OAuth2MobileInfo.builder() |
150 | .pkgName(mobile.getPkgName()) | 150 | .pkgName(mobile.getPkgName()) |
151 | - .callbackUrlScheme(mobile.getCallbackUrlScheme()) | 151 | + .appSecret(mobile.getAppSecret()) |
152 | .build(); | 152 | .build(); |
153 | } | 153 | } |
154 | 154 | ||
@@ -191,7 +191,7 @@ public class OAuth2Utils { | @@ -191,7 +191,7 @@ public class OAuth2Utils { | ||
191 | OAuth2Mobile mobile = new OAuth2Mobile(); | 191 | OAuth2Mobile mobile = new OAuth2Mobile(); |
192 | mobile.setOauth2ParamsId(oauth2ParamsId); | 192 | mobile.setOauth2ParamsId(oauth2ParamsId); |
193 | mobile.setPkgName(mobileInfo.getPkgName()); | 193 | mobile.setPkgName(mobileInfo.getPkgName()); |
194 | - mobile.setCallbackUrlScheme(mobileInfo.getCallbackUrlScheme()); | 194 | + mobile.setAppSecret(mobileInfo.getAppSecret()); |
195 | return mobile; | 195 | return mobile; |
196 | } | 196 | } |
197 | 197 |
@@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE | @@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE | ||
57 | } | 57 | } |
58 | 58 | ||
59 | @Override | 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,12 +42,12 @@ public interface OAuth2RegistrationRepository extends CrudRepository<OAuth2Regis | ||
42 | 42 | ||
43 | List<OAuth2RegistrationEntity> findByOauth2ParamsId(UUID oauth2ParamsId); | 43 | List<OAuth2RegistrationEntity> findByOauth2ParamsId(UUID oauth2ParamsId); |
44 | 44 | ||
45 | - @Query("SELECT mobile.callbackUrlScheme " + | 45 | + @Query("SELECT mobile.appSecret " + |
46 | "FROM OAuth2MobileEntity mobile " + | 46 | "FROM OAuth2MobileEntity mobile " + |
47 | "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + | 47 | "LEFT JOIN OAuth2RegistrationEntity reg on mobile.oauth2ParamsId = reg.oauth2ParamsId " + |
48 | "WHERE reg.id = :registrationId " + | 48 | "WHERE reg.id = :registrationId " + |
49 | "AND mobile.pkgName = :pkgName") | 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,7 +431,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( | ||
431 | oauth2_params_id uuid NOT NULL, | 431 | oauth2_params_id uuid NOT NULL, |
432 | created_time bigint NOT NULL, | 432 | created_time bigint NOT NULL, |
433 | pkg_name varchar(255), | 433 | pkg_name varchar(255), |
434 | - callback_url_scheme varchar(255), | 434 | + app_secret varchar(2048), |
435 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, | 435 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
436 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) | 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,7 +468,7 @@ CREATE TABLE IF NOT EXISTS oauth2_mobile ( | ||
468 | oauth2_params_id uuid NOT NULL, | 468 | oauth2_params_id uuid NOT NULL, |
469 | created_time bigint NOT NULL, | 469 | created_time bigint NOT NULL, |
470 | pkg_name varchar(255), | 470 | pkg_name varchar(255), |
471 | - callback_url_scheme varchar(255), | 471 | + app_secret varchar(2048), |
472 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, | 472 | CONSTRAINT fk_mobile_oauth2_params FOREIGN KEY (oauth2_params_id) REFERENCES oauth2_params(id) ON DELETE CASCADE, |
473 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) | 473 | CONSTRAINT oauth2_mobile_unq_key UNIQUE (oauth2_params_id, pkg_name) |
474 | ); | 474 | ); |
@@ -15,8 +15,8 @@ | @@ -15,8 +15,8 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.dao.service; | 16 | package org.thingsboard.server.dao.service; |
17 | 17 | ||
18 | -import com.fasterxml.jackson.databind.node.ObjectNode; | ||
19 | import com.google.common.collect.Lists; | 18 | import com.google.common.collect.Lists; |
19 | +import org.apache.commons.lang3.RandomStringUtils; | ||
20 | import org.junit.After; | 20 | import org.junit.After; |
21 | import org.junit.Assert; | 21 | import org.junit.Assert; |
22 | import org.junit.Before; | 22 | import org.junit.Before; |
@@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | @@ -487,7 +487,7 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | ||
487 | } | 487 | } |
488 | 488 | ||
489 | @Test | 489 | @Test |
490 | - public void testFindCallbackUrlScheme() { | 490 | + public void testFindAppSecret() { |
491 | OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( | 491 | OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( |
492 | OAuth2ParamsInfo.builder() | 492 | OAuth2ParamsInfo.builder() |
493 | .domainInfos(Lists.newArrayList( | 493 | .domainInfos(Lists.newArrayList( |
@@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | @@ -496,8 +496,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | ||
496 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() | 496 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() |
497 | )) | 497 | )) |
498 | .mobileInfos(Lists.newArrayList( | 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 | .clientRegistrations(Lists.newArrayList( | 502 | .clientRegistrations(Lists.newArrayList( |
503 | validRegistrationInfo(), | 503 | validRegistrationInfo(), |
@@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | @@ -527,14 +527,14 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | ||
527 | for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { | 527 | for (OAuth2ClientInfo clientInfo : firstDomainHttpClients) { |
528 | String[] segments = clientInfo.getUrl().split("/"); | 528 | String[] segments = clientInfo.getUrl().split("/"); |
529 | String registrationId = segments[segments.length-1]; | 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,8 +548,8 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | ||
548 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() | 548 | OAuth2DomainInfo.builder().name("third-domain").scheme(SchemeType.HTTPS).build() |
549 | )) | 549 | )) |
550 | .mobileInfos(Lists.newArrayList( | 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 | .clientRegistrations(Lists.newArrayList( | 554 | .clientRegistrations(Lists.newArrayList( |
555 | validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), | 555 | validRegistrationInfo("Google", Arrays.asList(PlatformType.WEB, PlatformType.ANDROID)), |
@@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | @@ -651,4 +651,10 @@ public class BaseOAuth2ServiceTest extends AbstractServiceTest { | ||
651 | ) | 651 | ) |
652 | .build(); | 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,3 +445,14 @@ export function validateEntityId(entityId: EntityId | null): boolean { | ||
445 | export function isMobileApp(): boolean { | 445 | export function isMobileApp(): boolean { |
446 | return isDefined((window as any).flutter_inappwebview); | 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,23 +90,26 @@ | ||
90 | <mat-form-field fxFlex class="mat-block"> | 90 | <mat-form-field fxFlex class="mat-block"> |
91 | <mat-label translate>admin.oauth2.redirect-uri-template</mat-label> | 91 | <mat-label translate>admin.oauth2.redirect-uri-template</mat-label> |
92 | <input matInput [value]="redirectURI(domainInfo)" readonly> | 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 | </mat-form-field> | 101 | </mat-form-field> |
100 | - | ||
101 | <mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block"> | 102 | <mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block"> |
102 | <mat-label></mat-label> | 103 | <mat-label></mat-label> |
103 | <input matInput [value]="redirectURIMixed(domainInfo)" readonly> | 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 | </mat-form-field> | 113 | </mat-form-field> |
111 | </div> | 114 | </div> |
112 | </div> | 115 | </div> |
@@ -144,18 +147,32 @@ | @@ -144,18 +147,32 @@ | ||
144 | <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> | 147 | <div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px"> |
145 | <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> | 148 | <div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px"> |
146 | <div fxFlex fxLayout="column"> | 149 | <div fxFlex fxLayout="column"> |
147 | - <mat-form-field fxFlex class="mat-block"> | 150 | + <mat-form-field fxFlex class="mat-block" floatLabel="always"> |
148 | <mat-label translate>admin.oauth2.mobile-package</mat-label> | 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 | </mat-form-field> | 154 | </mat-form-field> |
151 | <mat-error *ngIf="mobileInfo.hasError('unique')"> | 155 | <mat-error *ngIf="mobileInfo.hasError('unique')"> |
152 | {{ 'admin.oauth2.mobile-package-unique' | translate }} | 156 | {{ 'admin.oauth2.mobile-package-unique' | translate }} |
153 | </mat-error> | 157 | </mat-error> |
154 | </div> | 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 | </div> | 176 | </div> |
160 | <div fxLayout="column" fxLayoutAlign="center start"> | 177 | <div fxLayout="column" fxLayoutAlign="center start"> |
161 | <button type="button" mat-icon-button color="primary" | 178 | <button type="button" mat-icon-button color="primary" |
@@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service'; | @@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service'; | ||
42 | import { forkJoin, Subscription } from 'rxjs'; | 42 | import { forkJoin, Subscription } from 'rxjs'; |
43 | import { DialogService } from '@core/services/dialog.service'; | 43 | import { DialogService } from '@core/services/dialog.service'; |
44 | import { TranslateService } from '@ngx-translate/core'; | 44 | import { TranslateService } from '@ngx-translate/core'; |
45 | -import { isDefined, isDefinedAndNotNull } from '@core/utils'; | 45 | +import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils'; |
46 | import { OAuth2Service } from '@core/http/oauth2.service'; | 46 | import { OAuth2Service } from '@core/http/oauth2.service'; |
47 | import { ActivatedRoute } from '@angular/router'; | 47 | import { ActivatedRoute } from '@angular/router'; |
48 | 48 | ||
@@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha | @@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha | ||
275 | private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup { | 275 | private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup { |
276 | return this.fb.group({ | 276 | return this.fb.group({ |
277 | pkgName: [mobileInfo?.pkgName, [Validators.required]], | 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 | }, {validators: this.uniquePkgNameValidator}); | 280 | }, {validators: this.uniquePkgNameValidator}); |
280 | } | 281 | } |
281 | 282 | ||
@@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha | @@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha | ||
529 | addMobileInfo(control: AbstractControl): void { | 530 | addMobileInfo(control: AbstractControl): void { |
530 | this.mobileInfos(control).push(this.buildMobileInfoForm({ | 531 | this.mobileInfos(control).push(this.buildMobileInfoForm({ |
531 | pkgName: '', | 532 | pkgName: '', |
532 | - callbackUrlScheme: '' | 533 | + appSecret: randomAlphanumeric(24) |
533 | })); | 534 | })); |
534 | } | 535 | } |
535 | 536 |
@@ -16,11 +16,16 @@ | @@ -16,11 +16,16 @@ | ||
16 | 16 | ||
17 | --> | 17 | --> |
18 | <button mat-icon-button | 18 | <button mat-icon-button |
19 | + type="button" | ||
20 | + [color]="color" | ||
19 | [disabled]="disabled" | 21 | [disabled]="disabled" |
20 | [matTooltip]="matTooltipText" | 22 | [matTooltip]="matTooltipText" |
21 | [matTooltipPosition]="matTooltipPosition" | 23 | [matTooltipPosition]="matTooltipPosition" |
22 | (click)="copy($event)"> | 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 | </mat-icon> | 27 | </mat-icon> |
28 | + <ng-template #copiedTemplate> | ||
29 | + <mat-icon [ngStyle]="style" class="copied">done</mat-icon> | ||
30 | + </ng-template> | ||
26 | </button> | 31 | </button> |
@@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core'; | @@ -26,7 +26,6 @@ import { TranslateService } from '@ngx-translate/core'; | ||
26 | }) | 26 | }) |
27 | export class CopyButtonComponent { | 27 | export class CopyButtonComponent { |
28 | 28 | ||
29 | - private copedIcon = ''; | ||
30 | private timer; | 29 | private timer; |
31 | 30 | ||
32 | copied = false; | 31 | copied = false; |
@@ -52,6 +51,9 @@ export class CopyButtonComponent { | @@ -52,6 +51,9 @@ export class CopyButtonComponent { | ||
52 | @Input() | 51 | @Input() |
53 | style: {[key: string]: any} = {}; | 52 | style: {[key: string]: any} = {}; |
54 | 53 | ||
54 | + @Input() | ||
55 | + color: string; | ||
56 | + | ||
55 | @Output() | 57 | @Output() |
56 | successCopied = new EventEmitter<string>(); | 58 | successCopied = new EventEmitter<string>(); |
57 | 59 | ||
@@ -67,23 +69,13 @@ export class CopyButtonComponent { | @@ -67,23 +69,13 @@ export class CopyButtonComponent { | ||
67 | } | 69 | } |
68 | this.clipboardService.copy(this.copyText); | 70 | this.clipboardService.copy(this.copyText); |
69 | this.successCopied.emit(this.copyText); | 71 | this.successCopied.emit(this.copyText); |
70 | - this.copedIcon = 'done'; | ||
71 | this.copied = true; | 72 | this.copied = true; |
72 | this.timer = setTimeout(() => { | 73 | this.timer = setTimeout(() => { |
73 | - this.copedIcon = null; | ||
74 | this.copied = false; | 74 | this.copied = false; |
75 | this.cd.detectChanges(); | 75 | this.cd.detectChanges(); |
76 | }, 1500); | 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 | get matTooltipText(): string { | 79 | get matTooltipText(): string { |
88 | return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; | 80 | return this.copied ? this.translate.instant('ota-update.copied') : this.tooltipText; |
89 | } | 81 | } |
@@ -34,7 +34,7 @@ export interface OAuth2DomainInfo { | @@ -34,7 +34,7 @@ export interface OAuth2DomainInfo { | ||
34 | 34 | ||
35 | export interface OAuth2MobileInfo { | 35 | export interface OAuth2MobileInfo { |
36 | pkgName: string; | 36 | pkgName: string; |
37 | - callbackUrlScheme: string; | 37 | + appSecret: string; |
38 | } | 38 | } |
39 | 39 | ||
40 | export enum DomainSchema{ | 40 | export enum DomainSchema{ |
@@ -224,8 +224,12 @@ | @@ -224,8 +224,12 @@ | ||
224 | "mobile-apps": "Mobile applications", | 224 | "mobile-apps": "Mobile applications", |
225 | "no-mobile-apps": "No applications configured", | 225 | "no-mobile-apps": "No applications configured", |
226 | "mobile-package": "Application package", | 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 | "mobile-package-unique": "Application package must be unique.", | 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 | "add-mobile-app": "Add application", | 233 | "add-mobile-app": "Add application", |
230 | "delete-mobile-app": "Delete application info", | 234 | "delete-mobile-app": "Delete application info", |
231 | "providers": "Providers", | 235 | "providers": "Providers", |