Introduce mobile app oauth2 request authentication using application token signed by application secret.
This commit is contained in:
parent
dbc3ef95ce
commit
1ed624d30b
@ -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)
|
||||
);
|
||||
|
||||
@ -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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Claims> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -42,5 +42,5 @@ public interface OAuth2Service {
|
||||
|
||||
List<OAuth2Registration> findAllRegistrations();
|
||||
|
||||
String findCallbackUrlScheme(UUID registrationId, String pkgName);
|
||||
String findAppSecret(UUID registrationId, String pkgName);
|
||||
}
|
||||
|
||||
@ -31,12 +31,12 @@ public class OAuth2Mobile extends BaseData<OAuth2MobileId> {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,5 +30,5 @@ import lombok.ToString;
|
||||
@Builder
|
||||
public class OAuth2MobileInfo {
|
||||
private String pkgName;
|
||||
private String callbackUrlScheme;
|
||||
private String appSecret;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -40,8 +40,8 @@ public class OAuth2MobileEntity extends BaseSqlEntity<OAuth2Mobile> {
|
||||
@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<OAuth2Mobile> {
|
||||
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<OAuth2Mobile> {
|
||||
mobile.setCreatedTime(createdTime);
|
||||
mobile.setOauth2ParamsId(new OAuth2ParamsId(oauth2ParamsId));
|
||||
mobile.setPkgName(pkgName);
|
||||
mobile.setCallbackUrlScheme(callbackUrlScheme);
|
||||
mobile.setAppSecret(appSecret);
|
||||
return mobile;
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,6 @@ public interface OAuth2RegistrationDao extends Dao<OAuth2Registration> {
|
||||
|
||||
List<OAuth2Registration> findByOAuth2ParamsId(UUID oauth2ParamsId);
|
||||
|
||||
String findCallbackUrlScheme(UUID id, String pkgName);
|
||||
String findAppSecret(UUID id, String pkgName);
|
||||
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -57,8 +57,8 @@ public class JpaOAuth2RegistrationDao extends JpaAbstractDao<OAuth2RegistrationE
|
||||
}
|
||||
|
||||
@Override
|
||||
public String findCallbackUrlScheme(UUID id, String pkgName) {
|
||||
return repository.findCallbackUrlScheme(id, pkgName);
|
||||
public String findAppSecret(UUID id, String pkgName) {
|
||||
return repository.findAppSecret(id, pkgName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -42,12 +42,12 @@ public interface OAuth2RegistrationRepository extends CrudRepository<OAuth2Regis
|
||||
|
||||
List<OAuth2RegistrationEntity> 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);
|
||||
|
||||
}
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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)
|
||||
);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -90,23 +90,26 @@
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.oauth2.redirect-uri-template</mat-label>
|
||||
<input matInput [value]="redirectURI(domainInfo)" readonly>
|
||||
<button mat-icon-button color="primary" matSuffix type="button"
|
||||
ngxClipboard cbContent="{{ redirectURI(domainInfo) }}"
|
||||
matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||
</button>
|
||||
<tb-copy-button
|
||||
matSuffix
|
||||
color="primary"
|
||||
[copyText]="redirectURI(domainInfo)"
|
||||
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
|
||||
tooltipPosition="above"
|
||||
mdiIcon="mdi:clipboard-arrow-left">
|
||||
</tb-copy-button>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field fxFlex *ngIf="domainInfo.get('scheme').value === 'MIXED'" class="mat-block">
|
||||
<mat-label></mat-label>
|
||||
<input matInput [value]="redirectURIMixed(domainInfo)" readonly>
|
||||
<button mat-icon-button color="primary" matSuffix type="button"
|
||||
ngxClipboard cbContent="{{ redirectURIMixed(domainInfo) }}"
|
||||
matTooltip="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
|
||||
matTooltipPosition="above">
|
||||
<mat-icon class="material-icons" svgIcon="mdi:clipboard-arrow-left"></mat-icon>
|
||||
</button>
|
||||
<tb-copy-button
|
||||
matSuffix
|
||||
color="primary"
|
||||
[copyText]="redirectURIMixed(domainInfo)"
|
||||
tooltipText="{{ 'admin.oauth2.copy-redirect-uri' | translate }}"
|
||||
tooltipPosition="above"
|
||||
mdiIcon="mdi:clipboard-arrow-left">
|
||||
</tb-copy-button>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
@ -144,18 +147,32 @@
|
||||
<div [formGroupName]="n" fxLayout="row" fxLayoutGap="8px">
|
||||
<div fxFlex fxLayout="row" fxLayout.xs="column" fxLayoutGap="8px">
|
||||
<div fxFlex fxLayout="column">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-form-field fxFlex class="mat-block" floatLabel="always">
|
||||
<mat-label translate>admin.oauth2.mobile-package</mat-label>
|
||||
<input matInput formControlName="pkgName" required>
|
||||
<input matInput formControlName="pkgName" placeholder="{{ 'admin.oauth2.mobile-package-placeholder' | translate }}" required>
|
||||
<mat-hint translate>admin.oauth2.mobile-package-hint</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-error *ngIf="mobileInfo.hasError('unique')">
|
||||
{{ 'admin.oauth2.mobile-package-unique' | translate }}
|
||||
</mat-error>
|
||||
</div>
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.oauth2.mobile-callback-url-scheme</mat-label>
|
||||
<input matInput formControlName="callbackUrlScheme" required>
|
||||
</mat-form-field>
|
||||
<div fxFlex fxLayout="row">
|
||||
<mat-form-field fxFlex class="mat-block">
|
||||
<mat-label translate>admin.oauth2.mobile-app-secret</mat-label>
|
||||
<textarea matInput formControlName="appSecret" rows="1" required></textarea>
|
||||
<tb-copy-button
|
||||
matSuffix
|
||||
color="primary"
|
||||
[copyText]="mobileInfo.get('appSecret').value"
|
||||
tooltipText="{{ 'admin.oauth2.copy-mobile-app-secret' | translate }}"
|
||||
tooltipPosition="above"
|
||||
mdiIcon="mdi:clipboard-arrow-left">
|
||||
</tb-copy-button>
|
||||
<mat-error *ngIf="mobileInfo.get('appSecret').invalid">
|
||||
{{ 'admin.oauth2.invalid-mobile-app-secret' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div fxLayout="column" fxLayoutAlign="center start">
|
||||
<button type="button" mat-icon-button color="primary"
|
||||
|
||||
@ -42,7 +42,7 @@ import { WINDOW } from '@core/services/window.service';
|
||||
import { forkJoin, Subscription } from 'rxjs';
|
||||
import { DialogService } from '@core/services/dialog.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { isDefined, isDefinedAndNotNull } from '@core/utils';
|
||||
import { isDefined, isDefinedAndNotNull, randomAlphanumeric } from '@core/utils';
|
||||
import { OAuth2Service } from '@core/http/oauth2.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@ -275,7 +275,8 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
|
||||
private buildMobileInfoForm(mobileInfo?: OAuth2MobileInfo): FormGroup {
|
||||
return this.fb.group({
|
||||
pkgName: [mobileInfo?.pkgName, [Validators.required]],
|
||||
callbackUrlScheme: [mobileInfo?.callbackUrlScheme, [Validators.required]],
|
||||
appSecret: [mobileInfo?.appSecret, [Validators.required, Validators.minLength(16), Validators.maxLength(2048),
|
||||
Validators.pattern(/^[A-Za-z0-9]+$/)]],
|
||||
}, {validators: this.uniquePkgNameValidator});
|
||||
}
|
||||
|
||||
@ -529,7 +530,7 @@ export class OAuth2SettingsComponent extends PageComponent implements OnInit, Ha
|
||||
addMobileInfo(control: AbstractControl): void {
|
||||
this.mobileInfos(control).push(this.buildMobileInfoForm({
|
||||
pkgName: '',
|
||||
callbackUrlScheme: ''
|
||||
appSecret: randomAlphanumeric(24)
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -16,11 +16,16 @@
|
||||
|
||||
-->
|
||||
<button mat-icon-button
|
||||
type="button"
|
||||
[color]="color"
|
||||
[disabled]="disabled"
|
||||
[matTooltip]="matTooltipText"
|
||||
[matTooltipPosition]="matTooltipPosition"
|
||||
(click)="copy($event)">
|
||||
<mat-icon [svgIcon]="mdiIconSymbol" [ngStyle]="style" [ngClass]="{'copied': copied}">
|
||||
{{ iconSymbol }}
|
||||
<mat-icon [svgIcon]="mdiIcon" [ngStyle]="style" *ngIf="!copied; else copiedTemplate">
|
||||
{{ icon }}
|
||||
</mat-icon>
|
||||
<ng-template #copiedTemplate>
|
||||
<mat-icon [ngStyle]="style" class="copied">done</mat-icon>
|
||||
</ng-template>
|
||||
</button>
|
||||
|
||||
@ -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<string>();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export interface OAuth2DomainInfo {
|
||||
|
||||
export interface OAuth2MobileInfo {
|
||||
pkgName: string;
|
||||
callbackUrlScheme: string;
|
||||
appSecret: string;
|
||||
}
|
||||
|
||||
export enum DomainSchema{
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user