This is an automated email from the ASF dual-hosted git repository. github-actions[bot] pushed a commit to branch cherry-pick-25bfe375-to-branch-1.3 in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit 94143cfb76c62db13e7dddc9f9642c980b1dbb31 Author: MaSai <[email protected]> AuthorDate: Tue Oct 20 16:27:37 2026 +0800 [#13971] fix(auth): preserve UnauthorizedException from PrincipalMapper in JWKS validation (#13972) ### What changes were proposed in this pull request? `JwksTokenValidator.validateToken` now rethrows `UnauthorizedException` from `principalMapper.map` as-is, instead of wrapping every failure as `"JWKS JWT validation error"`. ### Why are the changes needed? Custom `PrincipalMapper` implementations may throw `UnauthorizedException` with a deliberate client message (for example rejecting a disabled identity). On the JWKS path that message was overwritten. The static-key path already preserves `UnauthorizedException` via `OAuth2TokenAuthenticator`. Fix: #13971 ### Does this PR introduce _any_ user-facing change? When a configured `PrincipalMapper` throws `UnauthorizedException`, JWKS OAuth authentication returns that exception's message unchanged. ### How was this patch tested? - `./gradlew :server-common:test --tests org.apache.gravitino.server.authentication.TestJwksTokenValidator.testValidateTokenPreservesUnauthorizedExceptionFromPrincipalMapper` --------- Co-authored-by: Cursor <[email protected]> --- .../server/authentication/JwksTokenValidator.java | 2 + .../authentication/TestJwksTokenValidator.java | 55 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) 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 32e47b84e1..3605f016c9 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 @@ -170,6 +170,8 @@ public class JwksTokenValidator implements OAuthTokenValidator { } catch (ExpiredJWTException e) { throw new TokenExpiredException(e, "Authentication token is expired"); + } catch (UnauthorizedException e) { + throw e; } catch (Exception e) { LOG.warn( "JWKS JWT validation failed for principal [{}]: {}", 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 2fb1c1f2f7..db0b56eea5 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 @@ -46,8 +46,10 @@ import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; +import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.gravitino.Config; import org.apache.gravitino.UserPrincipal; +import org.apache.gravitino.auth.PrincipalMapper; import org.apache.gravitino.exceptions.TokenExpiredException; import org.apache.gravitino.exceptions.UnauthorizedException; import org.junit.jupiter.api.BeforeEach; @@ -771,4 +773,57 @@ public class TestJwksTokenValidator { assertTrue(exception.getMessage().contains("expired")); } } + + @Test + public void testValidateTokenPreservesUnauthorizedExceptionFromPrincipalMapper() + throws Exception { + RSAKey rsaKey = + new RSAKeyGenerator(2048).keyID("test-key-id").algorithm(JWSAlgorithm.RS256).generate(); + + JWTClaimsSet claimsSet = + new JWTClaimsSet.Builder() + .subject("disabled-user") + .audience("test-service") + .issuer("https://test-issuer.com") + .expirationTime(Date.from(Instant.now().plusSeconds(3600))) + .issueTime(Date.from(Instant.now())) + .build(); + + SignedJWT signedJWT = + new SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.RS256).keyID("test-key-id").build(), claimsSet); + signedJWT.sign(new RSASSASigner(rsaKey)); + String tokenString = signedJWT.serialize(); + + 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)); + + 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.principalMapper", "regex"); + config.put("gravitino.authenticator.oauth.principalMapper.regex.pattern", "(.*)"); + config.put("gravitino.authenticator.oauth.allowSkewSecs", "120"); + + validator.initialize(createConfig(config)); + + PrincipalMapper rejectingMapper = mock(PrincipalMapper.class); + when(rejectingMapper.map(any())).thenThrow(new UnauthorizedException("Invalid access token")); + FieldUtils.writeField(validator, "principalMapper", rejectingMapper, true); + + UnauthorizedException exception = + assertThrows( + UnauthorizedException.class, + () -> validator.validateToken(tokenString, "test-service")); + assertEquals("Invalid access token", exception.getMessage()); + } + } }
