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"));
   }
 }

Reply via email to