You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iotdb.apache.org by ha...@apache.org on 2026/03/17 02:19:42 UTC
(iotdb) 02/02: Validate OpenID issuer and audience claims
This is an automated email from the ASF dual-hosted git repository.
haonan pushed a commit to branch fix_openid
in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit df788e0773f6a0ac268eb680f6d619b8c2af1e85
Author: HTHou <ha...@apache.org>
AuthorDate: Tue Mar 17 10:19:06 2026 +0800
Validate OpenID issuer and audience claims
---
.../conf/iotdb-system.properties.template | 6 ++
.../commons/auth/authorizer/OpenIdAuthorizer.java | 101 +++++++++++++++++--
.../apache/iotdb/commons/conf/CommonConfig.java | 9 ++
.../iotdb/commons/conf/CommonDescriptor.java | 2 +
.../auth/authorizer/OpenIdAuthorizerTest.java | 112 +++++++++++++++++++++
5 files changed, 221 insertions(+), 9 deletions(-)
diff --git a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
index 3f1c4ab41ff..5d5cce4123f 100644
--- a/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
+++ b/iotdb-core/node-commons/src/assembly/resources/conf/iotdb-system.properties.template
@@ -1753,6 +1753,12 @@ authorizer_provider_class=org.apache.iotdb.commons.auth.authorizer.LocalFileAuth
# Privilege: SECURITY
openID_url=
+# If OpenIdAuthorizer is enabled, then openID_audience must contain the IoTDB client ID
+# or a comma-separated allowlist of accepted audiences.
+# effectiveMode: restart
+# Privilege: SECURITY
+openID_audience=
+
# encryption provider class
# effectiveMode: first_start
iotdb_server_encrypt_decrypt_provider=org.apache.iotdb.commons.security.encrypt.MessageDigestEncrypt
diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java
index f5f74a87b39..2ff888fa4ae 100644
--- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java
+++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizer.java
@@ -45,11 +45,16 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
+import java.util.Set;
import java.util.UUID;
+import java.util.stream.Collectors;
/** Uses an OpenID Connect provider for Authorization / Authentication. */
public class OpenIdAuthorizer extends BasicAuthorizer {
@@ -62,6 +67,8 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
private static final CommonConfig config = CommonDescriptor.getInstance().getConfig();
private final RSAPublicKey providerKey;
+ private final String expectedIssuer;
+ private final Set<String> acceptedAudiences;
/** Stores all claims to the respective user */
private final Map<String, Claims> loggedClaims = new HashMap<>();
@@ -71,6 +78,11 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
}
public OpenIdAuthorizer(JSONObject jwk) throws AuthException {
+ this(jwk, null, Collections.emptySet());
+ }
+
+ public OpenIdAuthorizer(JSONObject jwk, String expectedIssuer, Set<String> acceptedAudiences)
+ throws AuthException {
super(
new LocalFileUserManager(config.getUserFolder()),
new LocalFileRoleManager(config.getRoleFolder()));
@@ -80,15 +92,21 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
throw new AuthException(
TSStatusCode.INIT_AUTH_ERROR, "Unable to get OIDC Provider Key from JWK " + jwk, e);
}
+ this.expectedIssuer = expectedIssuer;
+ this.acceptedAudiences = Collections.unmodifiableSet(new HashSet<>(acceptedAudiences));
logger.info("Initialized with providerKey: {}", providerKey);
}
public OpenIdAuthorizer(String providerUrl)
throws AuthException, URISyntaxException, ParseException, IOException {
- this(getJwkFromProvider(providerUrl));
+ this(loadProviderContext(providerUrl));
}
- private static JSONObject getJwkFromProvider(String providerUrl)
+ private OpenIdAuthorizer(ProviderContext providerContext) throws AuthException {
+ this(providerContext.jwk, providerContext.issuer, providerContext.acceptedAudiences);
+ }
+
+ private static ProviderContext loadProviderContext(String providerUrl)
throws URISyntaxException, IOException, ParseException, AuthException {
if (providerUrl == null) {
throw new IllegalArgumentException("OpenID Connect Provider URI must be given!");
@@ -99,10 +117,24 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
logger.debug("Using Provider Metadata: {}", providerMetadata);
+ Set<String> acceptedAudiences = parseAudiences(config.getOpenIdAudience());
+ if (acceptedAudiences.isEmpty()) {
+ throw new AuthException(
+ TSStatusCode.INIT_AUTH_ERROR,
+ "openID_audience must be configured when OpenIdAuthorizer is enabled");
+ }
+
+ String issuer =
+ providerMetadata.getIssuer() == null ? null : providerMetadata.getIssuer().getValue();
+ if (issuer == null || issuer.isEmpty()) {
+ throw new AuthException(
+ TSStatusCode.INIT_AUTH_ERROR, "OIDC provider metadata does not contain an issuer");
+ }
+
try {
URL url = new URI(providerMetadata.getJWKSetURI().toString()).toURL();
logger.debug("Using url {}", url);
- return getProviderRsaJwk(url.openStream());
+ return new ProviderContext(getProviderRsaJwk(url.openStream()), issuer, acceptedAudiences);
} catch (IOException e) {
throw new AuthException(TSStatusCode.INIT_AUTH_ERROR, "Unable to start the Auth", e);
}
@@ -194,12 +226,51 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
}
private Claims validateToken(String token) {
- return Jwts.parser()
- .clockSkewSeconds(MAX_CLOCK_SKEW_SECONDS)
- .verifyWith(providerKey)
- .build()
- .parseSignedClaims(token)
- .getPayload();
+ Claims claims =
+ Jwts.parser()
+ .clockSkewSeconds(MAX_CLOCK_SKEW_SECONDS)
+ .verifyWith(providerKey)
+ .build()
+ .parseSignedClaims(token)
+ .getPayload();
+ validateClaims(claims);
+ return claims;
+ }
+
+ private void validateClaims(Claims claims) {
+ if (expectedIssuer != null && !expectedIssuer.equals(claims.getIssuer())) {
+ throw new JwtException(
+ String.format("Unexpected issuer %s, expected %s", claims.getIssuer(), expectedIssuer));
+ }
+ if (!acceptedAudiences.isEmpty() && !hasAcceptedAudience(claims.get("aud"))) {
+ throw new JwtException(
+ String.format(
+ "Unexpected audience %s, expected one of %s", claims.get("aud"), acceptedAudiences));
+ }
+ }
+
+ private boolean hasAcceptedAudience(Object audienceClaim) {
+ if (audienceClaim instanceof String) {
+ return acceptedAudiences.contains(audienceClaim);
+ }
+ if (audienceClaim instanceof List<?>) {
+ for (Object audience : (List<?>) audienceClaim) {
+ if (audience instanceof String && acceptedAudiences.contains(audience)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static Set<String> parseAudiences(String configuredAudiences) {
+ if (configuredAudiences == null || configuredAudiences.trim().isEmpty()) {
+ return Collections.emptySet();
+ }
+ return Arrays.stream(configuredAudiences.split(","))
+ .map(String::trim)
+ .filter(audience -> !audience.isEmpty())
+ .collect(Collectors.toCollection(HashSet::new));
}
private String getUsername(Claims claims) {
@@ -258,6 +329,18 @@ public class OpenIdAuthorizer extends BasicAuthorizer {
return isAdmin(userName);
}
+ private static class ProviderContext {
+ private final JSONObject jwk;
+ private final String issuer;
+ private final Set<String> acceptedAudiences;
+
+ private ProviderContext(JSONObject jwk, String issuer, Set<String> acceptedAudiences) {
+ this.jwk = jwk;
+ this.issuer = issuer;
+ this.acceptedAudiences = acceptedAudiences;
+ }
+ }
+
@Override
public void updateUserPassword(String userName, String newPassword) {
throwUnsupportedOperationException();
diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
index a490107ded3..760cccb973b 100644
--- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
+++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonConfig.java
@@ -60,6 +60,7 @@ public class CommonConfig {
// Open ID Secret
private String openIdProviderUrl = "";
+ private String openIdAudience = "";
// The authorizer provider class which extends BasicAuthorizer
private String authorizerProvider =
@@ -543,6 +544,14 @@ public class CommonConfig {
this.openIdProviderUrl = openIdProviderUrl;
}
+ public String getOpenIdAudience() {
+ return openIdAudience;
+ }
+
+ public void setOpenIdAudience(String openIdAudience) {
+ this.openIdAudience = openIdAudience;
+ }
+
public String getAuthorizerProvider() {
return authorizerProvider;
}
diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
index 8483d1425cf..004b147938c 100644
--- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
+++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/conf/CommonDescriptor.java
@@ -80,6 +80,8 @@ public class CommonDescriptor {
// if using org.apache.iotdb.db.auth.authorizer.OpenIdAuthorizer, openID_url is needed.
config.setOpenIdProviderUrl(
properties.getProperty("openID_url", config.getOpenIdProviderUrl()).trim());
+ config.setOpenIdAudience(
+ properties.getProperty("openID_audience", config.getOpenIdAudience()).trim());
config.setEncryptDecryptProvider(
properties
.getProperty(
diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java
index f41645998fe..b0f2b797f35 100644
--- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java
+++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/auth/authorizer/OpenIdAuthorizerTest.java
@@ -24,6 +24,8 @@ import org.apache.iotdb.commons.conf.CommonDescriptor;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
import io.jsonwebtoken.Jwts;
import net.minidev.json.JSONObject;
import org.junit.After;
@@ -32,6 +34,8 @@ import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
@@ -48,12 +52,14 @@ public class OpenIdAuthorizerTest {
private String originalUserFolder;
private String originalRoleFolder;
+ private String originalOpenIdAudience;
private Path baseDir;
@Before
public void setUp() throws IOException {
originalUserFolder = config.getUserFolder();
originalRoleFolder = config.getRoleFolder();
+ originalOpenIdAudience = config.getOpenIdAudience();
baseDir = Files.createTempDirectory("openid-authorizer-test-");
config.setUserFolder(Files.createDirectories(baseDir.resolve("users")).toString());
@@ -64,6 +70,7 @@ public class OpenIdAuthorizerTest {
public void tearDown() throws IOException {
config.setUserFolder(originalUserFolder);
config.setRoleFolder(originalRoleFolder);
+ config.setOpenIdAudience(originalOpenIdAudience);
if (baseDir != null) {
try (java.util.stream.Stream<Path> stream = Files.walk(baseDir)) {
@@ -103,6 +110,111 @@ public class OpenIdAuthorizerTest {
Assert.assertFalse(authorizer.isAdmin(expiredToken));
}
+ @Test
+ public void testWrongIssuerRejected() throws Exception {
+ config.setOpenIdAudience("iotdb");
+ KeyPair keyPair = generateKeyPair();
+ HttpServer server = startProviderServer(keyPair);
+ String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/";
+
+ try {
+ OpenIdAuthorizer authorizer = new OpenIdAuthorizer(issuer);
+ String token =
+ Jwts.builder()
+ .subject("attacker")
+ .issuer("https://evil.example/issuer")
+ .claim("aud", "iotdb")
+ .expiration(Date.from(Instant.now().plusSeconds(3600)))
+ .claim(
+ "realm_access",
+ Collections.singletonMap(
+ "roles", Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME)))
+ .signWith(keyPair.getPrivate(), Jwts.SIG.RS256)
+ .compact();
+
+ Assert.assertFalse(authorizer.login(token, "", false));
+ Assert.assertFalse(authorizer.isAdmin(token));
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ @Test
+ public void testWrongAudienceRejected() throws Exception {
+ config.setOpenIdAudience("iotdb");
+ KeyPair keyPair = generateKeyPair();
+ HttpServer server = startProviderServer(keyPair);
+ String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/";
+
+ try {
+ OpenIdAuthorizer authorizer = new OpenIdAuthorizer(issuer);
+ String token =
+ Jwts.builder()
+ .subject("attacker")
+ .issuer(issuer)
+ .claim("aud", "unrelated-client")
+ .expiration(Date.from(Instant.now().plusSeconds(3600)))
+ .claim(
+ "realm_access",
+ Collections.singletonMap(
+ "roles", Collections.singletonList(OpenIdAuthorizer.IOTDB_ADMIN_ROLE_NAME)))
+ .signWith(keyPair.getPrivate(), Jwts.SIG.RS256)
+ .compact();
+
+ Assert.assertFalse(authorizer.login(token, "", false));
+ Assert.assertFalse(authorizer.isAdmin(token));
+ } finally {
+ server.stop(0);
+ }
+ }
+
+ private KeyPair generateKeyPair() throws Exception {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+ keyPairGenerator.initialize(2048);
+ return keyPairGenerator.generateKeyPair();
+ }
+
+ private HttpServer startProviderServer(KeyPair keyPair) throws Exception {
+ JSONObject publicJwk =
+ new JSONObject(
+ new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
+ .keyUse(KeyUse.SIGNATURE)
+ .keyID("openid-provider-test-key")
+ .build()
+ .toJSONObject());
+
+ HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ String issuer = "http://127.0.0.1:" + server.getAddress().getPort() + "/";
+ String metadata =
+ "{"
+ + "\"issuer\":\""
+ + issuer
+ + "\","
+ + "\"jwks_uri\":\""
+ + issuer
+ + "jwks.json\","
+ + "\"subject_types_supported\":[\"public\"],"
+ + "\"response_types_supported\":[\"code\"],"
+ + "\"id_token_signing_alg_values_supported\":[\"RS256\"]"
+ + "}";
+ String jwks = "{\"keys\":[" + publicJwk.toJSONString() + "]}";
+
+ server.createContext(
+ "/.well-known/openid-configuration", exchange -> writeJson(exchange, metadata));
+ server.createContext("/jwks.json", exchange -> writeJson(exchange, jwks));
+ server.start();
+ return server;
+ }
+
+ private void writeJson(HttpExchange exchange, String json) throws IOException {
+ byte[] response = json.getBytes(java.nio.charset.StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, response.length);
+ try (OutputStream outputStream = exchange.getResponseBody()) {
+ outputStream.write(response);
+ }
+ }
+
private void deleteIfExists(Path path) {
try {
Files.deleteIfExists(path);