This is an automated email from the ASF dual-hosted git repository.
jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 86efd7ba2b [MINOR] feat(oauth): Support principal field to accept
comma separated values to fallback (#8049)
86efd7ba2b is described below
commit 86efd7ba2b9d4b0cc4eadbbdd71871a30bd7ccfd
Author: Bharath Krishna <[email protected]>
AuthorDate: Thu Aug 14 23:36:10 2025 -0700
[MINOR] feat(oauth): Support principal field to accept comma separated
values to fallback (#8049)
### What changes were proposed in this pull request?
Support comma-separated values in principal field.
### Why are the changes needed?
Currently, the fallback values are hardcoded in the code. But better
approach is to accept comma-separated values in config and use it in the
order to fallback one by one.
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
Unit tests
---
.../server/authentication/JwksTokenValidator.java | 31 +++++++++-------------
.../server/authentication/OAuthConfig.java | 11 +++++---
.../authentication/TestJwksTokenValidator.java | 25 ++++++++---------
3 files changed, 33 insertions(+), 34 deletions(-)
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
b/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
index 9c91c54871..cb47d61848 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authentication/JwksTokenValidator.java
@@ -31,6 +31,7 @@ import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import java.net.URL;
import java.security.Principal;
+import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Config;
import org.apache.gravitino.UserPrincipal;
@@ -50,14 +51,14 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
private String jwksUri;
private String expectedIssuer;
- private String principalField;
+ private List<String> principalFields;
private long allowSkewSeconds;
@Override
public void initialize(Config config) {
this.jwksUri = config.get(OAuthConfig.JWKS_URI);
this.expectedIssuer = config.get(OAuthConfig.AUTHORITY);
- this.principalField = config.get(OAuthConfig.PRINCIPAL_FIELD);
+ this.principalFields = config.get(OAuthConfig.PRINCIPAL_FIELDS);
this.allowSkewSeconds = config.get(OAuthConfig.ALLOW_SKEW_SECONDS);
LOG.info("Initializing JWKS token validator");
@@ -147,23 +148,17 @@ public class JwksTokenValidator implements
OAuthTokenValidator {
}
}
- /** Extracts the principal from the validated JWT claims using configured
field or fallbacks. */
+ /** Extracts the principal from the validated JWT claims using configured
field(s). */
private String extractPrincipal(JWTClaimsSet validatedClaims) {
- // Try the configured principal field first
- if (StringUtils.isNotBlank(principalField)) {
- String principal = (String) validatedClaims.getClaim(principalField);
- if (principal != null) {
- return principal;
- }
- }
-
- // Fallback to common user identity fields
- // TODO: Consider making this configurable
- String[] fallbackFields = {"unique_name", "upn", "email",
"preferred_username", "sub"};
- for (String field : fallbackFields) {
- String principal = (String) validatedClaims.getClaim(field);
- if (principal != null) {
- return principal;
+ // Try the principal field(s) one by one in order
+ if (principalFields != null && !principalFields.isEmpty()) {
+ for (String field : principalFields) {
+ if (StringUtils.isNotBlank(field)) {
+ String principal = (String) validatedClaims.getClaim(field);
+ if (principal != null) {
+ return principal;
+ }
+ }
}
}
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
b/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
index 9086d269f6..4f7078bde5 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authentication/OAuthConfig.java
@@ -20,6 +20,7 @@
package org.apache.gravitino.server.authentication;
import io.jsonwebtoken.SignatureAlgorithm;
+import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.config.ConfigBuilder;
import org.apache.gravitino.config.ConfigConstants;
@@ -128,12 +129,14 @@ public interface OAuthConfig {
.stringConf()
.create();
- ConfigEntry<String> PRINCIPAL_FIELD =
- new ConfigBuilder(OAUTH_CONFIG_PREFIX + "principalField")
- .doc("JWT claim field to use as principal identity (e.g., 'sub',
'client_id', 'appid')")
+ ConfigEntry<List<String>> PRINCIPAL_FIELDS =
+ new ConfigBuilder(OAUTH_CONFIG_PREFIX + "principalFields")
+ .doc(
+ "JWT claim field(s) to use as principal identity.
Comma-separated list for fallback in order (e.g.,
'preferred_username,email,sub').")
.version(ConfigConstants.VERSION_1_0_0)
.stringConf()
- .createWithDefault("sub");
+ .toSequence()
+ .createWithDefault(java.util.Arrays.asList("sub"));
ConfigEntry<String> TOKEN_VALIDATOR_CLASS =
new ConfigBuilder(OAUTH_CONFIG_PREFIX + "tokenValidatorClass")
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
index 20c3e587d3..2eaf800415 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.server.authentication;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
@@ -74,7 +75,7 @@ public class TestJwksTokenValidator {
Map<String, String> properties = new HashMap<>();
properties.put("gravitino.authenticator.oauth.jwksUri", validJwksUri);
properties.put("gravitino.authenticator.oauth.authority",
"https://login.microsoftonline.com");
- properties.put("gravitino.authenticator.oauth.principalField", "sub");
+ properties.put("gravitino.authenticator.oauth.principalFields", "sub");
properties.put("gravitino.authenticator.oauth.allowSkewSecs", "60");
validator.initialize(createConfig(properties));
@@ -116,7 +117,7 @@ public class TestJwksTokenValidator {
}
@Test
- public void testCustomPrincipalField() throws Exception {
+ public void testCustomPrincipalFields() throws Exception {
// Generate a test RSA key pair
RSAKey rsaKey =
new
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
@@ -161,7 +162,7 @@ public class TestJwksTokenValidator {
"gravitino.authenticator.oauth.jwksUri",
"https://test-jwks.com/.well-known/jwks.json");
config.put("gravitino.authenticator.oauth.authority",
"https://test-issuer.com");
config.put(
- "gravitino.authenticator.oauth.principalField",
+ "gravitino.authenticator.oauth.principalFields",
"client_id"); // Use client_id instead of default 'sub'
config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
@@ -180,11 +181,13 @@ public class TestJwksTokenValidator {
RSAKey rsaKey =
new
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
- // Create a signed JWT token without the configured principal field, but
with fallback fields
+ // Create a signed JWT token without the first configured principal field,
but with other
+ // configured fields
JWTClaimsSet claimsSet =
new JWTClaimsSet.Builder()
.subject("fallback-subject")
- .claim("email", "[email protected]") // This should be used as
fallback
+ .claim(
+ "email", "[email protected]") // This should be used as
fallback from configuration
.audience("test-service")
.issuer("https://test-issuer.com")
.expirationTime(Date.from(Instant.now().plusSeconds(3600)))
@@ -213,19 +216,17 @@ public class TestJwksTokenValidator {
// Configure the mock to return our test key
when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
- // Initialize validator with principal field that doesn't exist in token
Map<String, String> config = new HashMap<>();
config.put(
"gravitino.authenticator.oauth.jwksUri",
"https://test-jwks.com/.well-known/jwks.json");
config.put("gravitino.authenticator.oauth.authority",
"https://test-issuer.com");
config.put(
- "gravitino.authenticator.oauth.principalField",
- "non_existent_field"); // This field doesn't exist
+ "gravitino.authenticator.oauth.principalFields",
+ "non_existent_field,email"); // Should fall back to email since
first field doesn't exist
config.put("gravitino.authenticator.oauth.allowSkewSecs", "120");
validator.initialize(createConfig(config));
- // Test token validation - should fall back to 'email' field (which is
in fallback list)
Principal result = validator.validateToken(tokenString, "test-service");
assertNotNull(result);
@@ -277,7 +278,7 @@ public class TestJwksTokenValidator {
config.put(
"gravitino.authenticator.oauth.jwksUri",
"https://test-jwks.com/.well-known/jwks.json");
config.put("gravitino.authenticator.oauth.authority",
"https://test-issuer.com");
- config.put("gravitino.authenticator.oauth.principalField", "sub");
+ config.put("gravitino.authenticator.oauth.principalFields", "sub");
config.put("gravitino.authenticator.oauth.allowSkewSecs", "60");
validator.initialize(createConfig(config));
@@ -317,7 +318,7 @@ public class TestJwksTokenValidator {
Map<String, String> properties = new HashMap<>();
properties.put("gravitino.authenticator.oauth.jwksUri", validJwksUri);
properties.put("gravitino.authenticator.oauth.authority",
"https://login.microsoftonline.com");
- properties.put("gravitino.authenticator.oauth.principalField",
"custom_field");
+ properties.put("gravitino.authenticator.oauth.principalFields",
"custom_field");
properties.put("gravitino.authenticator.oauth.allowSkewSecs", "300");
validator.initialize(createConfig(properties));
@@ -332,6 +333,6 @@ public class TestJwksTokenValidator {
// The exception should indicate JWT parsing/validation error, not
initialization error
assertNotNull(exception.getMessage());
// The message should not contain "not initialized" or similar
- assertEquals(true, exception.getMessage().toLowerCase().contains("jwt"));
+ assertTrue(exception.getMessage().toLowerCase().contains("jwt"));
}
}