This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch debu-oidc in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 661e91d7d77b034e3c0bb82a09ab0e16403940d2 Author: Benoit TELLIER <[email protected]> AuthorDate: Thu Oct 1 23:55:22 2026 +0200 [ENHANCEMENT] Add OIDC validation failure info in logs --- .../org/apache/james/jwt/OidcJwtTokenVerifier.java | 56 +++++++++++++++++----- 1 file changed, 45 insertions(+), 11 deletions(-) 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 5ddf16be1f..35579560a0 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 @@ -24,13 +24,13 @@ import java.util.Optional; import org.apache.james.core.Username; import org.apache.james.jwt.introspection.IntrospectionEndpoint; -import org.apache.james.jwt.introspection.TokenIntrospectionResponse; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import reactor.core.publisher.Mono; @@ -75,9 +75,8 @@ public class OidcJwtTokenVerifier { try { return new JwtTokenVerifier(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL())) .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))); + .filter(this::hasExpectedAudience) + .flatMap(this::extractConfiguredClaim); } catch (JwtException e) { LOGGER.info("Failed Jwt verification", e); return Optional.empty(); @@ -89,10 +88,14 @@ public class OidcJwtTokenVerifier { return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken)) .flatMap(optional -> optional.map(Mono::just).orElseGet(Mono::empty)) .flatMap(claimResult -> Mono.from(CHECK_TOKEN_CLIENT.introspect(introspectionEndpoint, jwtToken)) - .filter(TokenIntrospectionResponse::active) - .filter(tokenIntrospectionResponse -> tokenIntrospectionResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()) - .map(claim -> claim.equals(claimResult)) - .orElse(false)) + .filter(tokenIntrospectionResponse -> { + if (!tokenIntrospectionResponse.active()) { + LOGGER.info("OIDC token rejected: introspection endpoint reported the token as inactive"); + return false; + } + return true; + }) + .filter(tokenIntrospectionResponse -> claimMatches("introspection", tokenIntrospectionResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()), claimResult)) .map(activeResponse -> claimResult)); } @@ -101,9 +104,40 @@ public class OidcJwtTokenVerifier { return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken)) .flatMap(optional -> optional.map(Mono::just).orElseGet(Mono::empty)) .flatMap(claimResult -> Mono.from(CHECK_TOKEN_CLIENT.userInfo(userinfoEndpoint, jwtToken)) - .filter(userinfoResponse -> userinfoResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()) - .map(claim -> claim.equals(claimResult)) - .orElse(false)) + .filter(userinfoResponse -> claimMatches("userinfo", userinfoResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()), claimResult)) .map(userinfoResponse -> claimResult)); } + + private boolean hasExpectedAudience(Claims claims) { + return oidcSASLConfiguration.getAud() + .map(expectedAud -> { + boolean matches = claims.getAudience() != null && claims.getAudience().contains(expectedAud); + if (!matches) { + LOGGER.info("OIDC token rejected: expected audience '{}' but token audience is {}", expectedAud, claims.getAudience()); + } + return matches; + }) + .orElse(true); // true if no aud is configured + } + + private Optional<String> extractConfiguredClaim(Claims claims) { + Optional<String> claim = Optional.ofNullable(claims.get(oidcSASLConfiguration.getClaim(), String.class)); + if (claim.isEmpty()) { + LOGGER.info("OIDC token rejected: claim '{}' is missing from the token", oidcSASLConfiguration.getClaim()); + } + return claim; + } + + private boolean claimMatches(String source, Optional<String> remoteClaim, String tokenClaim) { + if (remoteClaim.isEmpty()) { + LOGGER.info("OIDC token rejected: claim '{}' is missing from the {} response", oidcSASLConfiguration.getClaim(), source); + return false; + } + if (!remoteClaim.get().equals(tokenClaim)) { + LOGGER.info("OIDC token rejected: claim '{}' from the {} response ({}) does not match the token ({})", + oidcSASLConfiguration.getClaim(), source, remoteClaim.get(), tokenClaim); + return false; + } + return true; + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
