This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch 3.8.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 7687eda5c4fd8a3977a962b1dd1909978ae99538 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 | 21 +++++++++----- .../org/apache/james/jwt/OidcJwtTokenVerifier.java | 5 +++- .../apache/james/jwt/OidcJwtTokenVerifierTest.java | 33 ++++++++++++++++++++++ 3 files changed, 51 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 c5bc4ff9b8..66daee29a2 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 @@ -25,6 +25,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; @@ -67,16 +68,22 @@ public class JwtTokenVerifier { .findFirst(); } + public Optional<Claims> verify(String token) { + 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.parseClaimsJws(token); - T claim = jws - .getBody() - .get(claimName, returnType); - if (claim == null) { - throw new MalformedJwtException("'" + claimName + "' field in token is mandatory"); - } - return Optional.of(claim); + return Optional.of(jws.getBody()); } catch (JwtException e) { 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 6f0be04e56..b1ca932fe8 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 @@ -82,7 +82,10 @@ public class OidcJwtTokenVerifier { .map(kidValue -> JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL(), kidValue)) .orElse(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL())); return new JwtTokenVerifier(jwksPublicKeyProvider) - .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))); } private <T> Optional<T> getClaimWithoutSignatureVerification(String token, String claimName) { 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 0aee8af43c..1e1e09a33b 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 @@ -68,6 +68,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]
