This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch 3.8.x in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 547a3ccf87baa41503d46e06ca7f311601c18daa Author: Benoit TELLIER <[email protected]> AuthorDate: Mon Dec 8 14:11:29 2025 +0100 [ENHANCEMENT] OIDC SASL code refactoring - Mutualize code for choosing how code shall be validated between IMAP and SMTP - Turn OidcJwtTokenVerifier as a class - Have SASL configuration as a member in order to limit count of arguments --- protocols/api/pom.xml | 4 + .../apache/james/imap/api/process/ImapSession.java | 2 +- .../apache/james/imap/encode/FakeImapSession.java | 2 +- .../imap/processor/AuthenticateProcessor.java | 39 +-------- .../protocols/lmtp/LMTPConfigurationImpl.java | 2 +- .../james/protocols/smtp/SMTPConfiguration.java | 2 +- .../protocols/smtp/SMTPConfigurationImpl.java | 2 +- .../protocols/smtp/core/esmtp/AuthCmdHandler.java | 2 +- .../apache/james/protocols/smtp/hook/AuthHook.java | 2 +- server/protocols/jwt/pom.xml | 4 + .../org/apache/james/jwt/OidcJwtTokenVerifier.java | 55 +++++++++++-- .../apache/james/jwt}/OidcSASLConfiguration.java | 96 +++++++++++++++++++++- .../apache/james/jwt/OidcJwtTokenVerifierTest.java | 85 ++++++++++++------- server/protocols/protocols-imap4/pom.xml | 2 +- .../apache/james/imapserver/netty/IMAPServer.java | 2 +- .../james/imapserver/netty/NettyImapSession.java | 2 +- .../apache/james/lmtpserver/netty/LMTPServer.java | 2 +- server/protocols/protocols-smtp/pom.xml | 2 +- .../james/smtpserver/UsersRepositoryAuthHook.java | 40 +-------- .../apache/james/smtpserver/netty/SMTPServer.java | 2 +- 20 files changed, 222 insertions(+), 127 deletions(-) diff --git a/protocols/api/pom.xml b/protocols/api/pom.xml index 0b33366bea..03cb72c367 100644 --- a/protocols/api/pom.xml +++ b/protocols/api/pom.xml @@ -37,6 +37,10 @@ <groupId>${james.groupId}</groupId> <artifactId>james-core</artifactId> </dependency> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-server-jwt</artifactId> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>testing-base</artifactId> diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java index 81a93d0194..c3da9c5841 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java @@ -28,9 +28,9 @@ import javax.net.ssl.SSLSession; import org.apache.commons.text.RandomStringGenerator; import org.apache.james.core.Username; import org.apache.james.imap.api.ImapSessionState; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.mailbox.MailboxSession; import org.apache.james.protocols.api.CommandDetectionSession; -import org.apache.james.protocols.api.OidcSASLConfiguration; import reactor.core.publisher.Mono; diff --git a/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java b/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java index 44eb77d5ed..2fcb506b8e 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java @@ -33,7 +33,7 @@ import org.apache.james.imap.api.ImapSessionState; import org.apache.james.imap.api.process.ImapLineHandler; import org.apache.james.imap.api.process.ImapSession; import org.apache.james.imap.api.process.SelectedMailbox; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.util.concurrent.NamedThreadFactory; import reactor.core.publisher.Mono; diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java index 40de07069c..937cf8fd11 100644 --- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java +++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; import javax.inject.Inject; @@ -40,11 +39,10 @@ import org.apache.james.imap.message.request.AuthenticateRequest; import org.apache.james.imap.message.request.IRAuthenticateRequest; import org.apache.james.imap.message.response.AuthenticateResponse; import org.apache.james.jwt.OidcJwtTokenVerifier; -import org.apache.james.jwt.introspection.IntrospectionEndpoint; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.mailbox.MailboxManager; import org.apache.james.metrics.api.MetricFactory; import org.apache.james.protocols.api.OIDCSASLParser; -import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.util.MDCBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,8 +50,6 @@ import org.slf4j.LoggerFactory; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import reactor.core.publisher.Mono; - /** * Processor which handles the AUTHENTICATE command. Only authtype of PLAIN is supported ATM. */ @@ -210,7 +206,7 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq private void doOAuth(OIDCSASLParser.OIDCInitialResponse oidcInitialResponse, OidcSASLConfiguration oidcSASLConfiguration, ImapSession session, ImapRequest request, Responder responder) { - validateToken(oidcSASLConfiguration, oidcInitialResponse.getToken()) + new OidcJwtTokenVerifier(oidcSASLConfiguration).validateToken(oidcInitialResponse.getToken()) .ifPresentOrElse(authenticatedUser -> { Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser()); if (!associatedUser.equals(authenticatedUser)) { @@ -224,37 +220,6 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq }, () -> manageFailureCount(session, request, responder)); } - private Optional<Username> validateToken(OidcSASLConfiguration oidcSASLConfiguration, String token) { - if (oidcSASLConfiguration.isCheckTokenByIntrospectionEndpoint()) { - return validTokenWithIntrospection(oidcSASLConfiguration, token); - } else if (oidcSASLConfiguration.isCheckTokenByUserinfoEndpoint()) { - return validTokenWithUserInfo(oidcSASLConfiguration, token); - } else { - return OidcJwtTokenVerifier.verifySignatureAndExtractClaim(token, oidcSASLConfiguration.getJwksURL(), oidcSASLConfiguration.getClaim()) - .map(Username::of); - } - } - - private static Optional<Username> validTokenWithUserInfo(OidcSASLConfiguration oidcSASLConfiguration, String token) { - return Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(token, - oidcSASLConfiguration.getJwksURL(), - oidcSASLConfiguration.getClaim(), - oidcSASLConfiguration.getUserInfoEndpoint().orElseThrow())) - .blockOptional() - .map(Username::of); - } - - private static Optional<Username> validTokenWithIntrospection(OidcSASLConfiguration oidcSASLConfiguration, String token) { - return Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(token, - oidcSASLConfiguration.getJwksURL(), - oidcSASLConfiguration.getClaim(), - oidcSASLConfiguration.getIntrospectionEndpoint() - .map(endpoint -> new IntrospectionEndpoint(endpoint, oidcSASLConfiguration.getIntrospectionEndpointAuthorization())) - .orElseThrow())) - .blockOptional() - .map(Username::of); - } - private static String extractInitialClientResponse(byte[] data) { // cut of the CRLF return new String(data, 0, data.length - 2, StandardCharsets.US_ASCII); diff --git a/protocols/lmtp/src/test/java/org/apache/james/protocols/lmtp/LMTPConfigurationImpl.java b/protocols/lmtp/src/test/java/org/apache/james/protocols/lmtp/LMTPConfigurationImpl.java index 9202c26404..f7eb23442a 100644 --- a/protocols/lmtp/src/test/java/org/apache/james/protocols/lmtp/LMTPConfigurationImpl.java +++ b/protocols/lmtp/src/test/java/org/apache/james/protocols/lmtp/LMTPConfigurationImpl.java @@ -20,7 +20,7 @@ package org.apache.james.protocols.lmtp; import java.util.Optional; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; public class LMTPConfigurationImpl extends LMTPConfiguration { diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfiguration.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfiguration.java index acbf2dc604..f66106d482 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfiguration.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfiguration.java @@ -23,7 +23,7 @@ package org.apache.james.protocols.smtp; import java.util.Optional; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolConfiguration; diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfigurationImpl.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfigurationImpl.java index 1fb081f546..12e4f4bbee 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfigurationImpl.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPConfigurationImpl.java @@ -22,7 +22,7 @@ package org.apache.james.protocols.smtp; import java.util.Optional; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolConfigurationImpl; /** diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java index b3f1915a32..2285f615de 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandler.java @@ -35,7 +35,7 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.james.core.Username; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.Request; import org.apache.james.protocols.api.Response; import org.apache.james.protocols.api.handler.CommandHandler; diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/AuthHook.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/AuthHook.java index 8351eb2367..3a7234356c 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/AuthHook.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/hook/AuthHook.java @@ -19,7 +19,7 @@ package org.apache.james.protocols.smtp.hook; import org.apache.james.core.Username; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.smtp.SMTPSession; /** diff --git a/server/protocols/jwt/pom.xml b/server/protocols/jwt/pom.xml index 6e1c300b10..17d59d1135 100644 --- a/server/protocols/jwt/pom.xml +++ b/server/protocols/jwt/pom.xml @@ -32,6 +32,10 @@ <name>Apache James :: Server :: JWT</name> <dependencies> + <dependency> + <groupId>${james.groupId}</groupId> + <artifactId>james-core</artifactId> + </dependency> <dependency> <groupId>${james.groupId}</groupId> <artifactId>testing-base</artifactId> 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 35ba364dca..83bd172846 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 @@ -22,10 +22,13 @@ package org.apache.james.jwt; import java.net.URL; 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 com.google.common.annotations.VisibleForTesting; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; @@ -62,22 +65,62 @@ public class OidcJwtTokenVerifier { } } - public static Publisher<String> verifyWithIntrospection(String jwtToken, URL jwksURL, String claimName, IntrospectionEndpoint introspectionEndpoint) { - return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken, jwksURL, claimName)) + private final OidcSASLConfiguration oidcSASLConfiguration; + + public OidcJwtTokenVerifier(OidcSASLConfiguration oidcSASLConfiguration) { + this.oidcSASLConfiguration = oidcSASLConfiguration; + } + + public Optional<Username> validateToken(String token) { + if (oidcSASLConfiguration.isCheckTokenByIntrospectionEndpoint()) { + return validTokenWithIntrospection(token); + } else if (oidcSASLConfiguration.isCheckTokenByUserinfoEndpoint()) { + return validTokenWithUserInfo(token); + } else { + return verifySignatureAndExtractClaim(token) + .map(Username::of); + } + } + + private Optional<Username> validTokenWithUserInfo(String token) { + return Mono.from(verifyWithUserinfo(token, oidcSASLConfiguration.getUserInfoEndpoint().orElseThrow())) + .blockOptional() + .map(Username::of); + } + + private Optional<Username> validTokenWithIntrospection(String token) { + return Mono.from(verifyWithIntrospection(token, + oidcSASLConfiguration.getIntrospectionEndpoint() + .map(endpoint -> new IntrospectionEndpoint(endpoint, oidcSASLConfiguration.getIntrospectionEndpointAuthorization())) + .orElseThrow())) + .blockOptional() + .map(Username::of); + } + + @VisibleForTesting + Optional<String> verifySignatureAndExtractClaim(String jwtToken) { + return new JwtTokenVerifier(JwksPublicKeyProvider.of(oidcSASLConfiguration.getJwksURL())) + .verifyAndExtractClaim(jwtToken, oidcSASLConfiguration.getClaim(), String.class); + } + + @VisibleForTesting + Publisher<String> verifyWithIntrospection(String jwtToken, IntrospectionEndpoint introspectionEndpoint) { + 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(claimName) + .filter(tokenIntrospectionResponse -> tokenIntrospectionResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()) .map(claim -> claim.equals(claimResult)) .orElse(false)) .map(activeResponse -> claimResult)); } - public static Publisher<String> verifyWithUserinfo(String jwtToken, URL jwksURL, String claimName, URL userinfoEndpoint) { - return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken, jwksURL, claimName)) + @VisibleForTesting + Publisher<String> verifyWithUserinfo(String jwtToken, URL userinfoEndpoint) { + 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(claimName) + .filter(userinfoResponse -> userinfoResponse.claimByPropertyName(oidcSASLConfiguration.getClaim()) .map(claim -> claim.equals(claimResult)) .orElse(false)) .map(userinfoResponse -> claimResult)); diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java similarity index 56% rename from protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java rename to server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java index 232ca8afcd..403a33f966 100644 --- a/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java +++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java @@ -17,7 +17,7 @@ * under the License. * ****************************************************************/ -package org.apache.james.protocols.api; +package org.apache.james.jwt; import java.net.MalformedURLException; import java.net.URL; @@ -25,11 +25,95 @@ import java.util.Optional; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.tree.ImmutableNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.github.fge.lambdas.Throwing; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; public class OidcSASLConfiguration { + private static final Logger LOGGER = LoggerFactory.getLogger(OidcSASLConfiguration.class); + + private static final boolean FORCE_INTROSPECT = Boolean.parseBoolean(System.getProperty("james.sasl.oidc.force.introspect", "true")); + + @VisibleForTesting + static Builder builder() { + return new Builder(); + } + + public static class Builder { + private URL jwksURL; + private String claim; + private URL oidcConfigurationURL; + private String scope; + private Optional<URL> introspectionEndpoint = Optional.empty(); + private Optional<String> introspectionEndpointAuthorization = Optional.empty(); + private Optional<URL> userInfoEndpoint = Optional.empty(); + + private Builder() { + } + + public Builder jwksURL(URL jwksURL) { + this.jwksURL = jwksURL; + return this; + } + + public Builder claim(String claim) { + this.claim = claim; + return this; + } + + public Builder oidcConfigurationURL(URL oidcConfigurationURL) { + this.oidcConfigurationURL = oidcConfigurationURL; + return this; + } + + public Builder scope(String scope) { + this.scope = scope; + return this; + } + + public Builder introspectionEndpoint(Optional<URL> introspectionEndpoint) { + this.introspectionEndpoint = introspectionEndpoint; + return this; + } + + public Builder introspectionEndpoint(URL introspectionEndpoint) { + this.introspectionEndpoint = Optional.ofNullable(introspectionEndpoint); + return this; + } + + public Builder introspectionEndpointAuthorization(Optional<String> introspectionEndpointAuthorization) { + this.introspectionEndpointAuthorization = introspectionEndpointAuthorization; + return this; + } + + public Builder introspectionEndpointAuthorization(String introspectionEndpointAuthorization) { + this.introspectionEndpointAuthorization = Optional.ofNullable(introspectionEndpointAuthorization); + return this; + } + + public Builder userInfoEndpoint(Optional<URL> userInfoEndpoint) { + this.userInfoEndpoint = userInfoEndpoint; + return this; + } + + public Builder userInfoEndpoint(URL userInfoEndpoint) { + this.userInfoEndpoint = Optional.ofNullable(userInfoEndpoint); + return this; + } + + public OidcSASLConfiguration build() { + Preconditions.checkNotNull(jwksURL, "jwksURL is mandatory"); + Preconditions.checkNotNull(claim, "claim is mandatory"); + Preconditions.checkNotNull(oidcConfigurationURL, "oidcConfigurationURL is mandatory"); + Preconditions.checkNotNull(scope, "scope is mandatory"); + + return new OidcSASLConfiguration(jwksURL, claim, oidcConfigurationURL, scope, + introspectionEndpoint, introspectionEndpointAuthorization, userInfoEndpoint); + } + } public static OidcSASLConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws MalformedURLException { String jwksURL = configuration.getString("jwksURL", null); @@ -45,6 +129,14 @@ public class OidcSASLConfiguration { String introspectionUrl = configuration.getString("introspection.url", null); String userInfoUrl = configuration.getString("userinfo.url", null); + if (introspectionUrl == null) { + if (FORCE_INTROSPECT) { + throw new IllegalArgumentException("'introspection.url' is mandatory for secure set up. Disable this check with -Djames.sasl.oidc.force.introspect=false."); + } else { + LOGGER.warn("'introspection.url' is mandatory for secure set up. This check was disabled with -Djames.sasl.oidc.force.introspect=false."); + } + } + return new OidcSASLConfiguration(new URL(jwksURL), claim, new URL(oidcConfigurationURL), scope, Optional.ofNullable(introspectionUrl) .map(Throwing.function(URL::new)), Optional.ofNullable(configuration.getString("introspection.auth", null)), Optional.ofNullable(userInfoUrl).map(Throwing.function(URL::new))); @@ -58,7 +150,7 @@ public class OidcSASLConfiguration { private final Optional<String> introspectionEndpointAuthorization; private final Optional<URL> userInfoEndpoint; - public OidcSASLConfiguration(URL jwksURL, + private OidcSASLConfiguration(URL jwksURL, String claim, URL oidcConfigurationURL, String scope, 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 ff18fcf56a..03674929b8 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 @@ -70,38 +70,43 @@ class OidcJwtTokenVerifierTest { @Test void verifyAndClaimShouldReturnClaimValueWhenValidTokenHasKid() { - Optional<String> email_address = OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address"); + Optional<String> emailAddress = new OidcJwtTokenVerifier(configForClaim("email_address")).verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN); + SoftAssertions.assertSoftly(softly -> { - softly.assertThat(email_address.isPresent()).isTrue(); - softly.assertThat(email_address.get()).isEqualTo("[email protected]"); + softly.assertThat(emailAddress.isPresent()).isTrue(); + softly.assertThat(emailAddress.get()).isEqualTo("[email protected]"); }); } @Test void verifyAndClaimShouldReturnClaimValueWhenValidTokenHasNotKid() { - Optional<String> email_address = OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN_HAS_NOT_KID, getJwksURL(), "email_address"); + Optional<String> emailAddress = new OidcJwtTokenVerifier(configForClaim("email_address")).verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN_HAS_NOT_KID); + SoftAssertions.assertSoftly(softly -> { - softly.assertThat(email_address.isPresent()).isTrue(); - softly.assertThat(email_address.get()).isEqualTo("[email protected]"); + softly.assertThat(emailAddress.isPresent()).isTrue(); + softly.assertThat(emailAddress.get()).isEqualTo("[email protected]"); }); } @Test void verifyAndClaimShouldReturnEmptyWhenValidTokenHasNotFoundKid() { - assertThat(OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN_HAS_NOT_FOUND_KID, getJwksURL(), "email_address")) + assertThat(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN_HAS_NOT_FOUND_KID)) .isEmpty(); } @Test void verifyAndClaimShouldReturnEmptyWhenClaimNameNotFound() { - assertThat(OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "not_found")) + assertThat(new OidcJwtTokenVerifier(configForClaim("not_found")) + .verifySignatureAndExtractClaim(OidcTokenFixture.VALID_TOKEN)) .isEmpty(); } @Test void verifyAndClaimShouldReturnEmptyWhenInvalidToken() { - assertThat(OidcJwtTokenVerifier.verifySignatureAndExtractClaim(OidcTokenFixture.INVALID_TOKEN, getJwksURL(), "email_address")) + assertThat(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifySignatureAndExtractClaim(OidcTokenFixture.INVALID_TOKEN)) .isEmpty(); } @@ -111,7 +116,8 @@ class OidcJwtTokenVerifierTest { .when(HttpRequest.request().withPath(USERINFO_PATH)) .respond(HttpResponse.response().withStatusCode(201)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", getUserInfoEndpoint())) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getUserInfoEndpoint())) .block()) .isInstanceOf(UserInfoCheckException.class) .hasMessageContaining("Error when check token by userInfo"); @@ -125,7 +131,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody("badResponse1", StandardCharsets.UTF_8)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", getUserInfoEndpoint())) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getUserInfoEndpoint())) .block()) .isInstanceOf(UserInfoCheckException.class) .hasMessageContaining("Error when check token by userInfo"); @@ -146,11 +153,25 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(userInfoResponse, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "preferred_username", getUserInfoEndpoint())) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("preferred_username")) + .verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getUserInfoEndpoint())) .block()) .isNull(); } + private OidcSASLConfiguration configForClaim(String claim) { + try { + return OidcSASLConfiguration.builder() + .jwksURL(getJwksURL()) + .scope("email") + .oidcConfigurationURL(new URL("https://whatever.nte")) + .claim(claim) + .build(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + @Test void verifyWithUserinfoShouldReturnClaimValueWhenPassCheckToken() { mockServer @@ -159,7 +180,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(USERINFO_RESPONSE, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", getUserInfoEndpoint())) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithUserinfo(OidcTokenFixture.VALID_TOKEN, getUserInfoEndpoint())) .block()) .isEqualTo("[email protected]"); } @@ -172,7 +194,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(USERINFO_RESPONSE, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.INVALID_TOKEN, getJwksURL(), "email_address", getUserInfoEndpoint())) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithUserinfo(OidcTokenFixture.INVALID_TOKEN, getUserInfoEndpoint())) .block()) .isNull(); } @@ -193,7 +216,7 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(userInfoResponse, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(OidcTokenFixture.INVALID_TOKEN, getJwksURL(), "preferred_username", getUserInfoEndpoint())) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("preferred_username")).verifyWithUserinfo(OidcTokenFixture.INVALID_TOKEN, getUserInfoEndpoint())) .block()) .isNull(); } @@ -204,8 +227,8 @@ class OidcJwtTokenVerifierTest { .when(HttpRequest.request().withPath(INTROSPECTION_PATH)) .respond(HttpResponse.response().withStatusCode(201)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address" - , new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isInstanceOf(TokenIntrospectionException.class) .hasMessageContaining("Error when introspecting token"); @@ -219,8 +242,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody("badResponse1", StandardCharsets.UTF_8)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isInstanceOf(TokenIntrospectionException.class) .hasMessageContaining("Error when introspecting token"); @@ -249,8 +272,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(introspectionResponse, StandardCharsets.UTF_8)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isInstanceOf(TokenIntrospectionException.class) .hasMessageContaining("Error when introspecting token"); @@ -280,8 +303,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(introspectionResponse, StandardCharsets.UTF_8)); - assertThatThrownBy(() -> Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThatThrownBy(() -> Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isInstanceOf(TokenIntrospectionException.class) .hasMessageContaining("Error when introspecting token"); @@ -295,8 +318,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(INTROSPECTION_RESPONSE, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "email_address", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isEqualTo("[email protected]"); } @@ -317,8 +340,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(introspectionResponse, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "preferred_username", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("preferred_username")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isNull(); } @@ -339,8 +362,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(introspectionResponse, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, getJwksURL(), "preferred_username", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("preferred_username")) + .verifyWithIntrospection(OidcTokenFixture.VALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isNull(); } @@ -353,8 +376,8 @@ class OidcJwtTokenVerifierTest { .withHeader("Content-Type", "application/json") .withBody(INTROSPECTION_RESPONSE, StandardCharsets.UTF_8)); - assertThat(Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(OidcTokenFixture.INVALID_TOKEN, getJwksURL(), "email_address", - new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) + assertThat(Mono.from(new OidcJwtTokenVerifier(configForClaim("email_address")) + .verifyWithIntrospection(OidcTokenFixture.INVALID_TOKEN, new IntrospectionEndpoint(getIntrospectionEndpoint(), Optional.empty()))) .block()) .isNull(); } diff --git a/server/protocols/protocols-imap4/pom.xml b/server/protocols/protocols-imap4/pom.xml index 7dda6aa773..cdb72157bf 100644 --- a/server/protocols/protocols-imap4/pom.xml +++ b/server/protocols/protocols-imap4/pom.xml @@ -192,7 +192,7 @@ </systemPropertyVariables> <argLine>-Djava.library.path= -javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco-maven-plugin.version}/org.jacoco.agent-${jacoco-maven-plugin.version}-runtime.jar=destfile=${basedir}/target/jacoco.exec - -Xms512m -Xmx1024m</argLine> + -Xms512m -Xmx1024m -Djames.sasl.oidc.force.introspect=false</argLine> <reuseForks>true</reuseForks> <!-- Fail tests longer than 20 minutes, prevent form random locking tests --> <forkedProcessTimeoutInSeconds>1200</forkedProcessTimeoutInSeconds> diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java index 5ded55125b..5a4baf01ec 100644 --- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java +++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java @@ -31,8 +31,8 @@ import org.apache.james.imap.api.ImapConstants; import org.apache.james.imap.api.process.ImapProcessor; import org.apache.james.imap.decode.ImapDecoder; import org.apache.james.imap.encode.ImapEncoder; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.metrics.api.GaugeRegistry; -import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer; import org.apache.james.protocols.netty.AbstractChannelPipelineFactory; import org.apache.james.protocols.netty.ChannelHandlerFactory; diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java index 6c8b228d10..23f9776a35 100644 --- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java +++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java @@ -35,8 +35,8 @@ import org.apache.james.imap.api.process.ImapSession; import org.apache.james.imap.api.process.SelectedMailbox; import org.apache.james.imap.encode.ImapResponseWriter; import org.apache.james.imap.message.Literal; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.mailbox.MailboxSession; -import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.protocols.netty.Encryption; import org.apache.james.protocols.netty.LineHandlerAware; diff --git a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/netty/LMTPServer.java b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/netty/LMTPServer.java index fcf15a105e..9da0f671e4 100644 --- a/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/netty/LMTPServer.java +++ b/server/protocols/protocols-lmtp/src/main/java/org/apache/james/lmtpserver/netty/LMTPServer.java @@ -23,9 +23,9 @@ import java.util.Optional; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.commons.configuration2.tree.ImmutableNode; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.lmtpserver.CoreCmdHandlerLoader; import org.apache.james.lmtpserver.jmx.JMXHandlersLoader; -import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolSession; import org.apache.james.protocols.api.ProtocolTransport; import org.apache.james.protocols.lib.handler.HandlersPackage; diff --git a/server/protocols/protocols-smtp/pom.xml b/server/protocols/protocols-smtp/pom.xml index 999d8a579a..49a4e2d46d 100644 --- a/server/protocols/protocols-smtp/pom.xml +++ b/server/protocols/protocols-smtp/pom.xml @@ -226,7 +226,7 @@ </systemPropertyVariables> <argLine>-Djava.library.path= -javaagent:"${settings.localRepository}"/org/jacoco/org.jacoco.agent/${jacoco-maven-plugin.version}/org.jacoco.agent-${jacoco-maven-plugin.version}-runtime.jar=destfile=${basedir}/target/jacoco.exec - -Xms512m -Xmx1024m</argLine> + -Xms512m -Xmx1024m -Djames.sasl.oidc.force.introspect=false</argLine> <reuseForks>true</reuseForks> <!-- Fail tests longer than 20 minutes, prevent form random locking tests --> <forkedProcessTimeoutInSeconds>1200</forkedProcessTimeoutInSeconds> diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/UsersRepositoryAuthHook.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/UsersRepositoryAuthHook.java index 7c16adf3d7..0af1611333 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/UsersRepositoryAuthHook.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/UsersRepositoryAuthHook.java @@ -18,17 +18,14 @@ ****************************************************************/ package org.apache.james.smtpserver; -import java.util.Optional; - import javax.inject.Inject; import org.apache.james.core.Username; import org.apache.james.jwt.OidcJwtTokenVerifier; -import org.apache.james.jwt.introspection.IntrospectionEndpoint; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.mailbox.Authorizator; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.protocols.api.OIDCSASLParser; -import org.apache.james.protocols.api.OidcSASLConfiguration; import org.apache.james.protocols.smtp.SMTPSession; import org.apache.james.protocols.smtp.hook.AuthHook; import org.apache.james.protocols.smtp.hook.HookResult; @@ -38,8 +35,6 @@ import org.apache.james.user.api.UsersRepositoryException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; - /** * This Auth hook can be used to authenticate against the james user repository */ @@ -76,7 +71,7 @@ public class UsersRepositoryAuthHook implements AuthHook { @Override public HookResult doSasl(SMTPSession session, OidcSASLConfiguration configuration, String initialResponse) { return OIDCSASLParser.parse(initialResponse) - .flatMap(oidcInitialResponseValue -> validateToken(configuration, oidcInitialResponseValue.getToken()) + .flatMap(oidcInitialResponseValue -> new OidcJwtTokenVerifier(configuration).validateToken(oidcInitialResponseValue.getToken()) .map(authenticatedUser -> { Username associatedUser = Username.of(oidcInitialResponseValue.getAssociatedUser()); if (!associatedUser.equals(authenticatedUser)) { @@ -114,35 +109,4 @@ public class UsersRepositoryAuthHook implements AuthHook { return HookResult.DECLINED; } } - - private Optional<Username> validateToken(OidcSASLConfiguration oidcSASLConfiguration, String token) { - if (oidcSASLConfiguration.isCheckTokenByIntrospectionEndpoint()) { - return validTokenWithIntrospection(oidcSASLConfiguration, token); - } else if (oidcSASLConfiguration.isCheckTokenByUserinfoEndpoint()) { - return validTokenWithUserInfo(oidcSASLConfiguration, token); - } else { - return OidcJwtTokenVerifier.verifySignatureAndExtractClaim(token, oidcSASLConfiguration.getJwksURL(), oidcSASLConfiguration.getClaim()) - .map(Username::of); - } - } - - private Optional<Username> validTokenWithUserInfo(OidcSASLConfiguration oidcSASLConfiguration, String token) { - return Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(token, - oidcSASLConfiguration.getJwksURL(), - oidcSASLConfiguration.getClaim(), - oidcSASLConfiguration.getUserInfoEndpoint().orElseThrow())) - .blockOptional() - .map(Username::of); - } - - private static Optional<Username> validTokenWithIntrospection(OidcSASLConfiguration oidcSASLConfiguration, String token) { - return Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(token, - oidcSASLConfiguration.getJwksURL(), - oidcSASLConfiguration.getClaim(), - oidcSASLConfiguration.getIntrospectionEndpoint() - .map(endpoint -> new IntrospectionEndpoint(endpoint, oidcSASLConfiguration.getIntrospectionEndpointAuthorization())) - .orElseThrow())) - .blockOptional() - .map(Username::of); - } } diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java index 93c5902ddb..6e9051bea4 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/netty/SMTPServer.java @@ -32,7 +32,7 @@ import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.james.dnsservice.api.DNSService; import org.apache.james.dnsservice.library.netmatcher.NetMatcher; -import org.apache.james.protocols.api.OidcSASLConfiguration; +import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolSession; import org.apache.james.protocols.api.ProtocolTransport; import org.apache.james.protocols.lib.handler.HandlersPackage; --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
