Browse Source

Validate mobile app callback url scheme

pull/16102/head
Viacheslav Klimov 6 days ago
parent
commit
ee64fa1938
Failed to extract signature
  1. 12
      application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java
  2. 76
      application/src/test/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactoryTest.java

12
application/src/main/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactory.java

@ -29,7 +29,10 @@ import org.thingsboard.server.common.data.StringUtils;
import java.util.Base64; import java.util.Base64;
import java.util.Date; import java.util.Date;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Component @Component
@Slf4j @Slf4j
@ -39,6 +42,9 @@ public class OAuth2AppTokenFactory {
private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5); private static final long MAX_EXPIRATION_TIME_DIFF_MS = TimeUnit.MINUTES.toMillis(5);
private static final Pattern CALLBACK_URL_SCHEME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9+.-]*");
private static final Set<String> FORBIDDEN_CALLBACK_URL_SCHEMES = Set.of("http", "https", "javascript", "data", "file", "vbscript");
public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) { public String validateTokenAndGetCallbackUrlScheme(String appPackage, String appToken, String appSecret) {
Jws<Claims> jwsClaims; Jws<Claims> jwsClaims;
try { try {
@ -65,6 +71,12 @@ public class OAuth2AppTokenFactory {
if (StringUtils.isEmpty(callbackUrlScheme)) { if (StringUtils.isEmpty(callbackUrlScheme)) {
throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme"); throw new IllegalArgumentException("Application token doesn't have callbackUrlScheme");
} }
// the redirect carrying the access token is built as callbackUrlScheme + ":", so only a mobile app scheme
// may pass: a web scheme would send the token to whatever host follows it
if (!CALLBACK_URL_SCHEME_PATTERN.matcher(callbackUrlScheme).matches()
|| FORBIDDEN_CALLBACK_URL_SCHEMES.contains(callbackUrlScheme.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("Application token has invalid callbackUrlScheme");
}
return callbackUrlScheme; return callbackUrlScheme;
} }

76
application/src/test/java/org/thingsboard/server/service/security/model/token/OAuth2AppTokenFactoryTest.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2026 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.Jwts;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import javax.crypto.SecretKey;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
public class OAuth2AppTokenFactoryTest {
private static final String APP_PACKAGE = "org.thingsboard.demo.app";
private static final byte[] KEY_BYTES = "yjNyylzT1TmiVE2jV3YTnUpZzwLLLdPDJKmhLNyXDPnLtVCLcJIjIGmDPKHNoDMK".getBytes();
private final OAuth2AppTokenFactory tokenFactory = new OAuth2AppTokenFactory();
@Test
public void testMobileAppSchemeIsAccepted() {
assertThat(validate("tb-mobile.app1")).isEqualTo("tb-mobile.app1");
}
@ParameterizedTest
@ValueSource(strings = {
"https://evil.com",
"http://evil.com",
"https",
"HTTPS",
"javascript",
"data",
"//evil.com",
"tbmobile/evil.com",
"tbmobile:evil.com",
"tbmobile evil",
"1tbmobile",
"tbmobile@evil.com"
})
public void testInvalidCallbackUrlSchemeIsRejected(String callbackUrlScheme) {
assertThatThrownBy(() -> validate(callbackUrlScheme))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("callbackUrlScheme");
}
private String validate(String callbackUrlScheme) {
SecretKey key = Keys.hmacShaKeyFor(KEY_BYTES);
String appToken = Jwts.builder()
.issuer(APP_PACKAGE)
.expiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(1)))
.claim("callbackUrlScheme", callbackUrlScheme)
.signWith(key)
.compact();
return tokenFactory.validateTokenAndGetCallbackUrlScheme(APP_PACKAGE, appToken, Base64.getEncoder().encodeToString(KEY_BYTES));
}
}
Loading…
Cancel
Save