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