This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch 3.9.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit a64d96fa0af89bd115623e55f3154a4aa012dbc1 Author: Benoit TELLIER <[email protected]> AuthorDate: Fri Jan 16 17:52:36 2026 +0100 [ENHANCEMENT] Validate aud without introspect --- .../org/apache/james/jwt/JwtTokenVerifier.java | 27 +++++++++++++----- .../org/apache/james/jwt/OidcJwtTokenVerifier.java | 5 +++- .../apache/james/jwt/OidcJwtTokenVerifierTest.java | 33 ++++++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java index ae197d8e68..f5b79927ec 100644 --- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java +++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenVerifier.java @@ -29,6 +29,7 @@ import java.util.Optional; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.github.fge.lambdas.Throwing; import com.google.common.collect.ImmutableList; import io.jsonwebtoken.Claims; @@ -132,16 +133,28 @@ public class JwtTokenVerifier { } } + public Optional<Claims> verify(String token) { + try { + // if the token contains a kid, verify only with the corresponding key (or fail) + return retrieveClaims(token, kidJwtParser); + } catch (NullPointerException npe) { // our own key locator throws NPE when there is no kid + // if token does not specify kid, fallback to trying all keys + return jwtParsers.stream() + .flatMap(parser -> retrieveClaims(token, parser).stream()) + .findFirst(); + } + } + private <T> Optional<T> verifyAndExtractClaim(String token, String claimName, Class<T> returnType, JwtParser parser) { + return retrieveClaims(token, parser) + .map(Throwing.function(claims -> Optional.ofNullable(claims.get(claimName, returnType)) + .orElseThrow(() -> new MalformedJwtException("'" + claimName + "' field in token is mandatory")))); + } + + private Optional<Claims> retrieveClaims(String token, JwtParser parser) { try { Jws<Claims> jws = parser.parseSignedClaims(token); - T claim = jws - .getPayload() - .get(claimName, returnType); - if (claim == null) { - throw new MalformedJwtException("'" + claimName + "' field in token is mandatory"); - } - return Optional.of(claim); + return Optional.of(jws.getPayload()); } catch (JwtException e) { // also if kid was given but our locator didn't find the corresponding key LOGGER.info("Failed Jwt verification", e); return Optional.empty(); diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java index 7f87132bb7..d158fd2299 100644 --- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java +++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java @@ -73,7 +73,10 @@ public class OidcJwtTokenVerifier { @VisibleForTesting Optional<String> verifySignatureAndExtractClaim(String jwtToken) { return new JwtTokenVerifier(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL())) - .verifyAndExtractClaim(jwtToken, oidcSASLConfiguration.getClaim(), String.class); + .verify(jwtToken) + .filter(claims -> oidcSASLConfiguration.getAud().map(expectedAud -> claims.getAudience().contains(expectedAud)) + .orElse(true)) // true if no aud is configured + .flatMap(claims -> Optional.ofNullable(claims.get(oidcSASLConfiguration.getClaim(), String.class))); } @VisibleForTesting diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java index 3755d2fb2d..5480dd53b1 100644 --- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java +++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwtTokenVerifierTest.java @@ -78,6 +78,39 @@ class OidcJwtTokenVerifierTest { } } + @Test + void verifyAndClaimShouldAcceptValidAud() throws Exception { + Optional<String> emailAddress = new OidcJwtTokenVerifier( + OidcSASLConfiguration.builder() + .jwksURL(getJwksURL()) + .scope("email") + .oidcConfigurationURL(new URL("https://whatever.nte")) + .claim("email_address") + .aud("account") + .build()) + .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(emailAddress.isPresent()).isTrue(); + softly.assertThat(emailAddress.get()).isEqualTo("[email protected]"); + }); + } + + @Test + void verifyAndClaimShouldRejectInvalidAud() throws Exception { + Optional<String> emailAddress = new OidcJwtTokenVerifier( + OidcSASLConfiguration.builder() + .jwksURL(getJwksURL()) + .scope("email") + .oidcConfigurationURL(new URL("https://whatever.nte")) + .claim("email_address") + .aud("other") + .build()) + .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN); + + assertThat(emailAddress).isEmpty(); + } + @Test void verifyAndClaimShouldReturnClaimValueWhenValidTokenHasKid() { Optional<String> emailAddress = new OidcJwtTokenVerifier(configForClaim("email_address")).verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
