This is an automated email from the ASF dual-hosted git repository.

roryqi 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 6d6d0083cd [#9733]feat(oauth): Support tokens with multiple audiences 
in JWKS validator (#9734)
6d6d0083cd is described below

commit 6d6d0083cd460d150915e299c3796350806213cd
Author: Bharath Krishna <[email protected]>
AuthorDate: Fri Jan 16 21:31:23 2026 -0800

    [#9733]feat(oauth): Support tokens with multiple audiences in JWKS 
validator (#9734)
    
    ### What changes were proposed in this pull request?
    Modified `JwksTokenValidator` to use Nimbus's `acceptedAudiences` Set
    parameter instead of `exactMatchClaims` for audience validation. This
    enables proper RFC 7519 compliant multi-audience token support.
    StaticSignKeyValidator already supports multiple audiences.
    
    ### Why are the changes needed?
    
    JwksTokenValidator currently rejects valid JWT tokens that contain
    multiple audiences (e.g., `["service-a", "service-b", "service-c"]`),
    even when the configured service audience is present in the list. This
    is because the validator uses `exactMatchClaims` which requires exact
    array equality rather than "at-least-one match" semantics defined in RFC
    7519.
    
    Fix: #9733
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes - JWT tokens with multiple audiences in the `aud` claim are now
    properly validated when the configured service audience is present in
    the token's audience list. Previously only single-audience tokens were
    supported.
    
    
    ### How was this patch tested?
    
    Added `testValidateTokenWithMultipleAudiences()` that verifies:
    1. Token with `["other-service", "test-service", "another-service"]`
    validates successfully against `"test-service"`
    2. Same token fails validation against `"incorrect-service"`
    
    All existing OAuth authentication tests pass.
---
 .../server/authentication/JwksTokenValidator.java  | 24 +++++-----
 .../authentication/TestJwksTokenValidator.java     | 54 ++++++++++++++++++++++
 2 files changed, 67 insertions(+), 11 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 cb47d61848..5bd0681d47 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,7 +31,9 @@ import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
 import com.nimbusds.jwt.proc.DefaultJWTProcessor;
 import java.net.URL;
 import java.security.Principal;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.UserPrincipal;
@@ -101,27 +103,27 @@ public class JwksTokenValidator implements 
OAuthTokenValidator {
       DefaultJWTProcessor<SecurityContext> jwtProcessor = new 
DefaultJWTProcessor<>();
       jwtProcessor.setJWSKeySelector(keySelector);
 
-      // Configure claims verification
-      JWTClaimsSet.Builder expectedClaimsBuilder = new JWTClaimsSet.Builder();
-
-      // Set expected issuer if configured
-      if (StringUtils.isNotBlank(expectedIssuer)) {
-        expectedClaimsBuilder.issuer(expectedIssuer);
+      // Audience validation per RFC 7519 (at-least-one match)
+      Set<String> acceptedAudiences = null;
+      if (StringUtils.isNotBlank(serviceAudience)) {
+        acceptedAudiences = Collections.singleton(serviceAudience);
       }
 
-      // Set expected audience if provided
-      if (StringUtils.isNotBlank(serviceAudience)) {
-        expectedClaimsBuilder.audience(serviceAudience);
+      // Build exact match claims for issuer validation
+      JWTClaimsSet.Builder exactMatchBuilder = new JWTClaimsSet.Builder();
+      if (StringUtils.isNotBlank(expectedIssuer)) {
+        exactMatchBuilder.issuer(expectedIssuer);
       }
+      JWTClaimsSet exactMatchClaims = exactMatchBuilder.build();
 
       DefaultJWTClaimsVerifier<SecurityContext> claimsVerifier =
-          new 
DefaultJWTClaimsVerifier<SecurityContext>(expectedClaimsBuilder.build(), null);
+          new DefaultJWTClaimsVerifier<>(acceptedAudiences, exactMatchClaims, 
null, null);
 
       // Set clock skew tolerance
       claimsVerifier.setMaxClockSkew((int) allowSkewSeconds);
       jwtProcessor.setJWTClaimsSetVerifier(claimsVerifier);
 
-      // Process and validate the token
+      // Validate token signature and claims
       JWTClaimsSet validatedClaims = jwtProcessor.process(signedJWT, null);
 
       String principal = extractPrincipal(validatedClaims);
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 2eaf800415..a2f2362d87 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
@@ -291,6 +291,60 @@ public class TestJwksTokenValidator {
     }
   }
 
+  @Test
+  public void testValidateTokenWithMultipleAudiences() throws Exception {
+    // Generate a test RSA key pair
+    RSAKey rsaKey =
+        new 
RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate();
+
+    // Mock the JWKSourceBuilder to return our test key
+    try (MockedStatic<JWKSourceBuilder> mockedBuilder = 
mockStatic(JWKSourceBuilder.class)) {
+      @SuppressWarnings("unchecked")
+      JWKSource<SecurityContext> mockJwkSource = mock(JWKSource.class);
+      @SuppressWarnings("unchecked")
+      JWKSourceBuilder<SecurityContext> mockBuilder = 
mock(JWKSourceBuilder.class);
+
+      mockedBuilder.when(() -> 
JWKSourceBuilder.create(any(URL.class))).thenReturn(mockBuilder);
+      when(mockBuilder.build()).thenReturn(mockJwkSource);
+      when(mockJwkSource.get(any(), any())).thenReturn(Arrays.asList(rsaKey));
+
+      // Initialize validator
+      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.principalFields", "sub");
+      config.put("gravitino.authenticator.oauth.allowSkewSecs", "60");
+
+      validator.initialize(createConfig(config));
+
+      // Test 1: Token with multiple audiences including our service - should 
succeed
+      JWTClaimsSet validClaimsSet =
+          new JWTClaimsSet.Builder()
+              .subject("test-user")
+              .audience(Arrays.asList("other-service", "test-service", 
"another-service"))
+              .issuer("https://test-issuer.com";)
+              .expirationTime(Date.from(Instant.now().plusSeconds(3600)))
+              .issueTime(Date.from(Instant.now()))
+              .build();
+
+      SignedJWT validToken =
+          new SignedJWT(
+              new 
JWSHeader.Builder(JWSAlgorithm.RS256).keyID("test-key-id").build(),
+              validClaimsSet);
+      validToken.sign(new RSASSASigner(rsaKey));
+
+      Principal result = validator.validateToken(validToken.serialize(), 
"test-service");
+      assertNotNull(result);
+      assertEquals("test-user", result.getName());
+
+      // Test 2: Same token, different service name - should fail
+      assertThrows(
+          UnauthorizedException.class,
+          () -> validator.validateToken(validToken.serialize(), 
"incorrect-service"));
+    }
+  }
+
   @Test
   public void testValidateTokenWithInvalidToken() {
     Map<String, String> config = new HashMap<>();

Reply via email to