This is an automated email from the ASF dual-hosted git repository. chibenwa pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit cd8719eeb3ec69413c2aaacc0ff55c887dd7d45d Author: Quan Tran <[email protected]> AuthorDate: Wed Jun 24 11:03:26 2026 +0700 JAMES-4210 Drive SMTP AUTH through SASL exchanges Replace embedded SMTP PLAIN/OIDC authentication logic with the shared SaslMechanism exchange flow, including LOGIN framing, final server data acknowledgement, SMTP session authentication state, and SASL-focused regression tests. --- .../james/protocols/smtp/SMTPConfiguration.java | 6 - .../protocols/smtp/SMTPConfigurationImpl.java | 12 - .../protocols/smtp/SMTPProtocolHandlerChain.java | 45 +- .../apache/james/protocols/smtp/SMTPSession.java | 2 - .../james/protocols/smtp/SMTPSessionImpl.java | 5 - .../protocols/smtp/core/esmtp/AuthCmdHandler.java | 600 +++++++++------------ .../smtp/core/esmtp/AuthCmdHandlerTest.java | 68 --- .../protocols/smtp/utils/BaseFakeSMTPSession.java | 5 - .../apache/james/smtpserver/AuthAnnounceTest.java | 42 +- .../org/apache/james/smtpserver/SMTPSaslTest.java | 287 ++++++++-- .../apache/james/smtpserver/SMTPServerTest.java | 4 +- .../james/smtpserver/SMTPServerTestSystem.java | 60 ++- 12 files changed, 590 insertions(+), 546 deletions(-) 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 a0eb940633..32ae29440f 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 @@ -22,10 +22,8 @@ package org.apache.james.protocols.smtp; import java.util.Locale; -import java.util.Optional; import java.util.Set; -import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolConfiguration; import com.google.common.collect.ImmutableSet; @@ -97,10 +95,6 @@ public interface SMTPConfiguration extends ProtocolConfiguration { */ boolean useAddressBracketsEnforcement(); - boolean isPlainAuthEnabled(); - - Optional<OidcSASLConfiguration> saslConfiguration(); - default Set<String> disabledFeatures() { return ImmutableSet.of(); } 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 3890210d72..fe082f4723 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 @@ -20,9 +20,6 @@ package org.apache.james.protocols.smtp; -import java.util.Optional; - -import org.apache.james.jwt.OidcSASLConfiguration; import org.apache.james.protocols.api.ProtocolConfigurationImpl; /** @@ -86,13 +83,4 @@ public class SMTPConfigurationImpl extends ProtocolConfigurationImpl implements this.bracketsEnforcement = bracketsEnforcement; } - @Override - public boolean isPlainAuthEnabled() { - return true; - } - - @Override - public Optional<OidcSASLConfiguration> saslConfiguration() { - return Optional.empty(); - } } diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPProtocolHandlerChain.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPProtocolHandlerChain.java index 098d1a97bd..e17fdc847a 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPProtocolHandlerChain.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPProtocolHandlerChain.java @@ -19,13 +19,11 @@ package org.apache.james.protocols.smtp; import java.util.ArrayList; -import java.util.Collection; import java.util.List; import org.apache.james.metrics.api.MetricFactory; import org.apache.james.protocols.api.handler.CommandDispatcher; import org.apache.james.protocols.api.handler.CommandHandlerResultLogger; -import org.apache.james.protocols.api.handler.ExtensibleHandler; import org.apache.james.protocols.api.handler.ProtocolHandler; import org.apache.james.protocols.api.handler.ProtocolHandlerChain; import org.apache.james.protocols.api.handler.ProtocolHandlerChainImpl; @@ -49,7 +47,6 @@ import org.apache.james.protocols.smtp.core.esmtp.AuthCmdHandler; import org.apache.james.protocols.smtp.core.esmtp.EhloCmdHandler; import org.apache.james.protocols.smtp.core.esmtp.MailSizeEsmtpExtension; import org.apache.james.protocols.smtp.core.esmtp.StartTlsCmdHandler; -import org.apache.james.protocols.smtp.hook.AuthHook; import org.apache.james.protocols.smtp.hook.Hook; /** @@ -87,6 +84,7 @@ public class SMTPProtocolHandlerChain extends ProtocolHandlerChainImpl { protected List<ProtocolHandler> initDefaultHandlers() { List<ProtocolHandler> defaultHandlers = new ArrayList<>(); defaultHandlers.add(new CommandDispatcher<SMTPSession>()); + defaultHandlers.add(new AuthCmdHandler()); defaultHandlers.add(new ExpnCmdHandler()); defaultHandlers.add(new EhloCmdHandler(metricFactory)); defaultHandlers.add(new HeloCmdHandler(metricFactory)); @@ -109,45 +107,4 @@ public class SMTPProtocolHandlerChain extends ProtocolHandlerChainImpl { return defaultHandlers; } - private synchronized boolean checkForAuth(ProtocolHandler handler) { - if (isReadyOnly()) { - throw new UnsupportedOperationException("Read-Only"); - } - if (handler instanceof AuthHook) { - // check if we need to add the AuthCmdHandler - List<ExtensibleHandler> handlers = getHandlers(ExtensibleHandler.class); - for (ExtensibleHandler h: handlers) { - if (h.getMarkerInterfaces().contains(AuthHook.class)) { - return true; - } - } - if (!add(new AuthCmdHandler())) { - return false; - } - } - return true; - } - - @Override - public boolean add(ProtocolHandler handler) { - checkForAuth(handler); - return super.add(handler); - } - - @Override - public boolean addAll(Collection<? extends ProtocolHandler> c) { - return c.stream().allMatch(this::checkForAuth) && super.addAll(c); - } - - @Override - public boolean addAll(int index, Collection<? extends ProtocolHandler> c) { - return c.stream().allMatch(this::checkForAuth) && super.addAll(index, c); - } - - @Override - public void add(int index, ProtocolHandler element) { - checkForAuth(element); - super.add(index, element); - } - } diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java index 960bab9085..dfdefa0f7e 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java @@ -91,8 +91,6 @@ public interface SMTPSession extends ProtocolSession { int getRcptCount(); - boolean supportsOAuth(); - long currentMessageSize(); void setCurrentMessageSize(long increment); diff --git a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSessionImpl.java b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSessionImpl.java index b5b35ac5c9..1d13efdb64 100644 --- a/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSessionImpl.java +++ b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSessionImpl.java @@ -86,11 +86,6 @@ public class SMTPSessionImpl extends ProtocolSessionImpl implements SMTPSession .orElse(0); } - @Override - public boolean supportsOAuth() { - return getConfiguration().saslConfiguration().isPresent() && isAuthAnnounced(); - } - @Override public boolean isAuthAnnounced() { return getConfiguration().isAuthAnnounced(getRemoteAddress().getAddress().getHostAddress(), isTLSStarted()); 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 95055945cd..e17d6a5bf5 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 @@ -21,24 +21,28 @@ package org.apache.james.protocols.smtp.core.esmtp; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.function.Function; +import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; import org.apache.james.core.Username; -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; import org.apache.james.protocols.api.handler.ExtensibleHandler; import org.apache.james.protocols.api.handler.LineHandler; import org.apache.james.protocols.api.handler.WiringException; +import org.apache.james.protocols.api.sasl.SaslAuthenticator; +import org.apache.james.protocols.api.sasl.SaslExchange; +import org.apache.james.protocols.api.sasl.SaslFailure; +import org.apache.james.protocols.api.sasl.SaslInitialRequest; +import org.apache.james.protocols.api.sasl.SaslMechanism; +import org.apache.james.protocols.api.sasl.SaslMechanismNames; +import org.apache.james.protocols.api.sasl.SaslStep; import org.apache.james.protocols.smtp.SMTPResponse; import org.apache.james.protocols.smtp.SMTPRetCode; import org.apache.james.protocols.smtp.SMTPSession; @@ -46,15 +50,13 @@ import org.apache.james.protocols.smtp.dsn.DSNStatus; import org.apache.james.protocols.smtp.hook.AuthHook; import org.apache.james.protocols.smtp.hook.HookResult; import org.apache.james.protocols.smtp.hook.HookResultHook; -import org.apache.james.protocols.smtp.hook.HookReturnCode; import org.apache.james.protocols.smtp.hook.MailParametersHook; +import org.apache.james.protocols.smtp.hook.SaslAuthResultHook; import org.apache.james.util.AuditTrail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; -import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -63,10 +65,7 @@ import com.google.common.collect.ImmutableSet; /** * handles AUTH command * - * Note: we could extend this to use java5 sasl standard libraries and provide client - * support against a server implemented via non-james specific hooks. - * This would allow us to reuse hooks between imap4/pop3/smtp and eventually different - * system (simple pluggabilty against external authentication services). + * Authentication is delegated to configured SASL mechanisms. */ public class AuthCmdHandler implements CommandHandler<SMTPSession>, EhloExtension, ExtensibleHandler, MailParametersHook { @@ -80,11 +79,14 @@ public class AuthCmdHandler private static final Response ALREADY_AUTH = new SMTPResponse(SMTPRetCode.BAD_SEQUENCE, DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_OTHER) + " User has previously authenticated. " + " Further authentication is not required!").immutable(); private static final Response SYNTAX_ERROR = new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_INVALID_ARG) + " Usage: AUTH (authentication type) <challenge>").immutable(); - private static final Response AUTH_READY_PLAIN = new SMTPResponse(SMTPRetCode.AUTH_READY, "OK. Continue authentication").immutable(); private static final Response AUTH_READY_USERNAME_LOGIN = new SMTPResponse(SMTPRetCode.AUTH_READY, "VXNlcm5hbWU6").immutable(); // base64 encoded "Username:" private static final Response AUTH_READY_PASSWORD_LOGIN = new SMTPResponse(SMTPRetCode.AUTH_READY, "UGFzc3dvcmQ6").immutable(); // base64 encoded "Password: + private static final Response AUTH_SUCCEEDED = new SMTPResponse(SMTPRetCode.AUTH_OK, "Authentication Successful").immutable(); private static final Response AUTH_FAILED = new SMTPResponse(SMTPRetCode.AUTH_FAILED, "Authentication Failed").immutable(); private static final Response UNKNOWN_AUTH_TYPE = new SMTPResponse(SMTPRetCode.PARAMETER_NOT_IMPLEMENTED, "Unrecognized Authentication Type").immutable(); + private static final Response SERVER_ERROR = new SMTPResponse(SMTPRetCode.LOCAL_ERROR, "Unable to process request").immutable(); + + private static final SmtpSaslBridge SASL_BRIDGE = new SmtpSaslBridge(); private abstract static class AbstractSMTPLineHandler implements LineHandler<SMTPSession> { @@ -122,18 +124,18 @@ public class AuthCmdHandler */ protected static final String AUTH_TYPE_LOGIN = "LOGIN"; - /** - * The text string for the SMTP AUTH type OAUTHBEARER. - */ - protected static final String AUTH_TYPE_OAUTHBEARER = "OAUTHBEARER"; - protected static final String AUTH_TYPE_XOAUTH2 = "XOAUTH2"; - - /** - * The AuthHooks - */ - private List<AuthHook> hooks; - - private List<HookResultHook> rHooks; + private ImmutableList<SaslMechanism> saslMechanisms = ImmutableList.of(); + private ImmutableList<SaslMechanism> effectiveSaslMechanisms = ImmutableList.of(); + private Optional<SaslAuthenticator> saslAuthenticator = Optional.empty(); + private ImmutableList<AuthHook> authHooks = ImmutableList.of(); + private ImmutableList<HookResultHook> hookResultHooks = ImmutableList.of(); + private ImmutableList<SaslAuthResultHook> saslAuthResultHooks = ImmutableList.of(); + + public void configureSaslMechanisms(ImmutableList<SaslMechanism> saslMechanisms, SaslAuthenticator saslAuthenticator) { + this.saslMechanisms = saslMechanisms; + this.saslAuthenticator = Optional.of(saslAuthenticator); + updateEffectiveSaslMechanisms(); + } /** * handles AUTH command @@ -165,356 +167,273 @@ public class AuthCmdHandler argument = argument.substring(0,argument.indexOf(" ")); } String authType = argument.toUpperCase(Locale.US); - if (authType.equals(AUTH_TYPE_PLAIN) && session.getConfiguration().isPlainAuthEnabled()) { - return handlePlainContinuation(session, initialResponse); - } else if (authType.equals(AUTH_TYPE_LOGIN) && session.getConfiguration().isPlainAuthEnabled()) { - return handleLoginAuthContinuation(session, initialResponse); - } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) || authType.equals(AUTH_TYPE_XOAUTH2)) && session.supportsOAuth()) { - return handleOauth2Continuation(session, initialResponse); - } else { - return doUnknownAuth(authType); - } + return handleSaslAuthentication(session, authType, Optional.ofNullable(initialResponse).map(String::trim)); } } - private Response handlePlainContinuation(SMTPSession session, String initialResponse) { - return Optional.ofNullable(initialResponse) - .map(String::trim) - .map(userpass -> doPlainAuth(session, userpass)) - .orElseGet(() -> { - session.pushLineHandler(new AbstractSMTPLineHandler() { - @Override - protected Response onCommand(SMTPSession session, String l) { - return doPlainAuth(session, l); - } - }); - return AUTH_READY_USERNAME_LOGIN; - }); - } + private Response handleSaslAuthentication(SMTPSession session, String authType, Optional<String> initialResponse) { + if (authType.equals(AUTH_TYPE_LOGIN)) { + return handleLoginFraming(session, initialResponse); + } - private Response handleLoginAuthContinuation(SMTPSession session, String initialResponse) { - return Optional.ofNullable(initialResponse) - .map(String::trim) - .map(user -> doLoginAuthPass(session, user)) - .orElseGet(() -> { - session.pushLineHandler(new AbstractSMTPLineHandler() { - @Override - protected Response onCommand(SMTPSession session, String l) { - return doLoginAuthPass(session, l); - } - }); - return AUTH_READY_USERNAME_LOGIN; - }); + Optional<SaslMechanism> maybeMechanism = findAvailableMechanism(session, authType); + if (maybeMechanism.isEmpty()) { + return doUnknownAuth(authType); + } + + SaslMechanism mechanism = maybeMechanism.get(); + SaslExchange exchange; + try { + SaslInitialRequest request = SASL_BRIDGE.initialRequest(authType, initialResponse); + exchange = startExchange(session, mechanism, request); + return handleFirstSaslStep(session, authType, exchange); + } catch (IllegalArgumentException e) { + LOGGER.info("Could not decode parameters for AUTH {}", authType, e); + return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH " + authType); + } } - private Response handleOauth2Continuation(SMTPSession session, String initialResponse) { - return Optional.ofNullable(initialResponse) - .map(token -> doOauth2Authentication(session, token)) - .orElseGet(() -> { - session.pushLineHandler(new AbstractSMTPLineHandler() { - @Override - protected Response onCommand(SMTPSession session, String l) { - Response response = doOauth2Authentication(session, l); - session.popLineHandler(); - return response; - } - }); - return new SMTPResponse(SMTPRetCode.AUTH_READY, ""); - }); + private Response handleFirstSaslStep(SMTPSession session, String authType, SaslExchange exchange) { + try { + SaslStep step = exchange.firstStep(); + return handleSaslStep(session, authType, exchange, step); + } catch (RuntimeException e) { + SASL_BRIDGE.close(exchange); + throw e; + } } - private Response doOauth2Authentication(SMTPSession session, String initialResponseString) { - return session.getConfiguration().saslConfiguration() - .map(oidcSASLConfiguration -> hooks.stream() - .flatMap(hook -> Optional.ofNullable(executeHook(session, hook, - hook2 -> hook2.doSasl(session, oidcSASLConfiguration, initialResponseString))).stream()) - .filter(response -> !SMTPRetCode.AUTH_FAILED.equals(response.getRetCode())) - .findFirst() - .orElseGet(() -> failSasl(oidcSASLConfiguration, session))) - .orElseGet(() -> doUnknownAuth(AUTH_TYPE_OAUTHBEARER)); + private Response handleSaslStep(SMTPSession session, String authType, SaslExchange exchange, SaslStep step) { + return switch (step) { + case SaslStep.Challenge challenge -> { + session.pushLineHandler(saslLineHandler(authType, exchange)); + yield SASL_BRIDGE.challenge(challenge); + } + case SaslStep.Success success -> handleSaslSuccess(session, authType, exchange, success); + case SaslStep.Failure failure -> handleSaslFailure(session, authType, exchange, failure.failure()); + }; } - private Response failSasl(OidcSASLConfiguration saslConfiguration, SMTPSession session) { - String rawResponse = String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}", - saslConfiguration.getScope(), - saslConfiguration.getOidcConfigurationURL().toString()); + private Response handleSaslContinuation(SMTPSession session, String authType, SaslExchange exchange, String line) { + try { + SaslStep step = SASL_BRIDGE.onClientResponse(exchange, line.getBytes(session.getCharset())); + return switch (step) { + case SaslStep.Challenge challenge -> SASL_BRIDGE.challenge(challenge); + case SaslStep.Success success -> { + session.popLineHandler(); + yield handleSaslSuccess(session, authType, exchange, success); + } + case SaslStep.Failure failure -> { + session.popLineHandler(); + yield handleSaslFailure(session, authType, exchange, failure.failure()); + } + }; + } catch (IllegalArgumentException e) { + LOGGER.info("Could not decode parameters for AUTH {}", authType, e); + session.popLineHandler(); + SASL_BRIDGE.close(exchange); + return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH " + authType); + } catch (RuntimeException e) { + session.popLineHandler(); + SASL_BRIDGE.close(exchange); + throw e; + } + } - session.pushLineHandler(new AbstractSMTPLineHandler() { - @Override - protected Response onCommand(SMTPSession session, String l) { + private LineHandler<SMTPSession> saslLineHandler(String authType, SaslExchange exchange) { + return (session, line) -> { + if (SASL_BRIDGE.isAbort(line)) { session.popLineHandler(); - - return AUTH_FAILED; + SASL_BRIDGE.abort(exchange); + return AUTH_ABORTED; } - }); - return new SMTPResponse("334", Base64.getEncoder().encodeToString(rawResponse.getBytes())); + return handleSaslContinuation(session, authType, exchange, new String(line, session.getCharset())); + }; } - /** - * Carries out the Plain AUTH SASL exchange. - * - * According to RFC 2595 the client must send: [authorize-id] \0 authenticate-id \0 password. - * - * >>> AUTH PLAIN dGVzdAB0ZXN0QHdpei5leGFtcGxlLmNvbQB0RXN0NDI= - * Decoded: test\[email protected]\000tEst42 - * - * >>> AUTH PLAIN dGVzdAB0ZXN0AHRFc3Q0Mg== - * Decoded: test\000test\000tEst42 - * - * @param session SMTP session object - * @param line the initial response line passed in with the AUTH command - */ - private Response doPlainAuth(SMTPSession session, String line) { + private Response handleSaslSuccess(SMTPSession session, String authType, SaslExchange exchange, SaslStep.Success success) { + if (success.serverData().isPresent()) { + session.pushLineHandler(successDataAcknowledgementLineHandler(authType, exchange, success)); + return SASL_BRIDGE.successData(success); + } try { + return applySaslSuccess(session, authType, exchange, success); + } finally { + SASL_BRIDGE.close(exchange); + } + } - AuthValues authValues = - Optional.ofNullable(decodeBase64(line)) - .flatMap(AuthCmdHandler::parseAuthValues) - .orElseThrow(() -> new IllegalArgumentException("Can't parse line as authentication values")); - - Response response; + private LineHandler<SMTPSession> successDataAcknowledgementLineHandler(String authType, SaslExchange exchange, SaslStep.Success success) { + return (session, line) -> handleSaslSuccessDataAcknowledgement(session, authType, exchange, success, new String(line, session.getCharset())); + } - if (authValues.password.isEmpty()) { - response = doDelegation(session, authValues.username); - } else { - response = doAuthTest(session, Optional.of(authValues.username), authValues.password, AUTH_TYPE_PLAIN); + private Response handleSaslSuccessDataAcknowledgement(SMTPSession session, String authType, SaslExchange exchange, + SaslStep.Success success, String line) { + session.popLineHandler(); + boolean aborted = false; + try { + byte[] bytes = line.getBytes(session.getCharset()); + if (SASL_BRIDGE.isAbort(bytes)) { + aborted = true; + SASL_BRIDGE.abort(exchange); + return AUTH_ABORTED; + } + if (!SASL_BRIDGE.isEmptyClientResponse(bytes)) { + return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH " + authType); + } + return applySaslSuccess(session, authType, exchange, success); + } finally { + if (!aborted) { + SASL_BRIDGE.close(exchange); } - - session.popLineHandler(); - return response; - } catch (Exception e) { - LOGGER.info("Could not decode parameters for AUTH PLAIN", e); - return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH PLAIN"); } } - @VisibleForTesting - static Optional<AuthValues> parseAuthValues(String input) { - - List<String> parts = Splitter.on('\0').splitToStream(input).filter(token -> !token.isBlank()).toList(); - - return switch (parts.size()) { - case 1 -> Optional.of(new AuthValues(Username.of(parts.get(0)), Optional.empty())); - // If we got here, this is what happened. RFC 2595 - // says that "the client may leave the authorization - // identity empty to indicate that it is the same as - // the authentication identity." As noted above, - // that would be represented as a decoded string of - // the form: "\0authenticate-id\0password". The - // first call to nextToken will skip the empty - // authorize-id, and give us the authenticate-id, - // which we would store as the authorize-id. The - // second call will give us the password, which we - // think is the authenticate-id (user). Then when - // we ask for the password, there are no more - // elements, leading to the exception we just - // caught. So we need to move the user to the - // password, and the authorize_id to the user. - case 2 -> Optional.of(new AuthValues(Username.of(parts.get(0)), Optional.of(parts.get(1)))); - case 3 -> Optional.of(new AuthValues(Username.of(parts.get(1)), Optional.of(parts.get(2)))); - default -> Optional.empty(); - }; + private Response applySaslSuccess(SMTPSession session, String authType, SaslExchange exchange, SaslStep.Success success) { + Username username = success.identity().authorizationId(); + session.setUsername(username); + session.setRelayingAllowed(true); + saslAuthResultHooks.forEach(hook -> hook.onSuccess(session, authType, success.identity())); + + AUTHENTICATION_DEDICATED_LOGGER.debug("AUTH method {} succeeded", authType); + + AuditTrail.entry() + .username(username::asString) + .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress())) + .sessionId(session::getSessionID) + .protocol("SMTP") + .action("AUTH") + .parameters(() -> ImmutableMap.of("authType", authType)) + .log("SMTP Authentication succeeded."); + + return AuthHookSaslMechanism.terminalResponse(exchange) + .orElse(AUTH_SUCCEEDED); } - record AuthValues(Username username, Optional<String> password) { + private Response handleSaslFailure(SMTPSession session, String authType, SaslExchange exchange, SaslFailure failure) { + try { + saslAuthResultHooks.forEach(hook -> hook.onFailure(session, authType, failure)); + + failure.authenticationId().ifPresent(username -> AuditTrail.entry() + .username(username::asString) + .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress())) + .protocol("SMTP") + .action("AUTH") + .parameters(() -> ImmutableMap.of("authType", authType)) + .log("SMTP Authentication failed.")); + + return AuthHookSaslMechanism.terminalResponse(exchange) + .orElseGet(() -> switch (failure.type()) { + case MALFORMED -> new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH " + authType); + case SERVER_ERROR -> SERVER_ERROR; + case INVALID_CREDENTIALS, AUTHENTICATION_FAILED, USER_DOES_NOT_EXIST, DELEGATION_FORBIDDEN -> AUTH_FAILED; + }); + } finally { + SASL_BRIDGE.close(exchange); + } } - private String decodeBase64(String line) { - if (line != null) { - String lineWithoutTrailingCrLf = StringUtils.replace(line, "\r\n", ""); - return new String(Base64.getDecoder().decode(lineWithoutTrailingCrLf), StandardCharsets.UTF_8); + private Response handleLoginFraming(SMTPSession session, Optional<String> initialResponse) { + Optional<SaslMechanism> plain = findAvailableMechanism(session, SaslMechanismNames.PLAIN); + if (plain.isEmpty()) { + return doUnknownAuth(AUTH_TYPE_LOGIN); } - return null; + + return initialResponse + .map(user -> promptLoginPasswordThenDelegateToPlain(session, plain.get(), user)) + .orElseGet(() -> { + session.pushLineHandler(new AbstractSMTPLineHandler() { + @Override + protected Response onCommand(SMTPSession session, String line) { + session.popLineHandler(); + return promptLoginPasswordThenDelegateToPlain(session, plain.get(), line); + } + }); + return AUTH_READY_USERNAME_LOGIN; + }); } - /** - * Carries out the Login AUTH SASL exchange. - * - * @param session SMTP session object - * @param user the user passed in with the AUTH command - */ - private Response doLoginAuthPass(SMTPSession session, String user) { - session.popLineHandler(); + private Response promptLoginPasswordThenDelegateToPlain(SMTPSession session, SaslMechanism plain, String encodedUsername) { session.pushLineHandler(new AbstractSMTPLineHandler() { @Override - protected Response onCommand(SMTPSession session, String l) { - return doLoginAuthPassCheck(session, asUsername(user), l); + protected Response onCommand(SMTPSession session, String encodedPassword) { + session.popLineHandler(); + return delegateLoginCredentialsToPlain(session, plain, decodeLoginUsername(encodedUsername), encodedPassword); } }); return AUTH_READY_PASSWORD_LOGIN; } - private Optional<Username> asUsername(String user) { - try { - return Optional.of(Username.of(decodeBase64(user))); - } catch (Exception e) { - LOGGER.info("Failed parsing base64 username {}", user, e); - return Optional.empty(); + private Response delegateLoginCredentialsToPlain(SMTPSession session, SaslMechanism plain, Optional<Username> username, String encodedPassword) { + Optional<String> password = decodeLoginPassword(username, encodedPassword); + if (username.isEmpty() || password.isEmpty()) { + return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS, "Could not decode parameters for AUTH " + AUTH_TYPE_LOGIN); } + SaslInitialRequest request = new SaslInitialRequest(SaslMechanismNames.PLAIN, + Optional.of(toPlainInitialResponse(username.get(), password.get()))); + SaslExchange exchange = startExchange(session, plain, request); + return handleFirstSaslStep(session, AUTH_TYPE_LOGIN, exchange); } - private Response doLoginAuthPassCheck(SMTPSession session, Optional<Username> username, String pass) { - session.popLineHandler(); - // Authenticate user - return doAuthTest(session, username, sanitizePassword(username, pass), "LOGIN"); + private byte[] toPlainInitialResponse(Username username, String password) { + return ("\0" + username.asString() + "\0" + password).getBytes(StandardCharsets.UTF_8); } - private Optional<String> sanitizePassword(Optional<Username> username, String pass) { + private Optional<Username> decodeLoginUsername(String response) { try { - return Optional.of(decodeBase64(pass)); - } catch (Exception e) { - LOGGER.info("Failed parsing base64 password for user {}", username, e); - // Ignored - this parse error will be - // addressed in the if clause below + return Optional.of(Username.of(decodeSaslLoginResponse(response))); + } catch (IllegalArgumentException e) { + LOGGER.info("Could not decode LOGIN username", e); return Optional.empty(); } } - protected Response doDelegation(SMTPSession session, Username username) { - List<AuthHook> hooks = Optional.ofNullable(getHooks()) - .orElse(List.of()); - - for (AuthHook rawHook : hooks) { - rawHook.doDelegation(session, username); - Response res = executeHook(session, rawHook, hook -> rawHook.doDelegation(session, username)); - - if (res != null) { - if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) { - LOGGER.warn("{} was not authorized to connect as {}", session.getUsername(), username); - } else if (SMTPRetCode.AUTH_OK.equals(res.getRetCode())) { - LOGGER.info("{} was authorized to connect as {}", session.getUsername(), username); - } - return res; - } + private Optional<String> decodeLoginPassword(Optional<Username> username, String response) { + try { + return Optional.of(decodeSaslLoginResponse(response)); + } catch (IllegalArgumentException e) { + LOGGER.info("Could not decode LOGIN password for user {}", username, e); + return Optional.empty(); } + } - LOGGER.info("DELEGATE failed from {}@{}", username, session.getRemoteAddress().getAddress().getHostAddress()); - return AUTH_FAILED; + private String decodeSaslLoginResponse(String response) { + return new String(Base64.getDecoder().decode(response.replace("\r\n", "")), StandardCharsets.UTF_8); } - protected Response doAuthTest(SMTPSession session, Optional<Username> username, Optional<String> pass, String authType) { - if (username.isEmpty() || pass.isEmpty()) { - return new SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,"Could not decode parameters for AUTH " + authType); - } + private Optional<SaslMechanism> findAvailableMechanism(SMTPSession session, String authType) { + return effectiveSaslMechanisms + .stream() + .filter(mechanism -> mechanism.name().equalsIgnoreCase(authType)) + .filter(mechanism -> mechanism.isAvailableOnTransport(session.isTLSStarted())) + .findFirst(); + } - List<AuthHook> hooks = getHooks(); - - if (hooks != null) { - for (AuthHook rawHook : hooks) { - Response res = executeHook(session, rawHook, hook -> hook.doAuth(session, username.get(), pass.get())); - - if (res != null) { - if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) { - AUTHENTICATION_DEDICATED_LOGGER.info("AUTH method {} failed", authType); - } else if (SMTPRetCode.AUTH_OK.equals(res.getRetCode())) { - // TODO: Make this string a more useful debug message - AUTHENTICATION_DEDICATED_LOGGER.debug("AUTH method {} succeeded", authType); - - AuditTrail.entry() - .username(username.get()::asString) - .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress())) - .sessionId(session::getSessionID) - .protocol("SMTP") - .action("AUTH") - .parameters(() -> ImmutableMap.of("authType", authType)) - .log("SMTP Authentication succeeded."); - } - return res; - } - } + private SaslExchange startExchange(SMTPSession session, SaslMechanism mechanism, SaslInitialRequest request) { + if (mechanism instanceof AuthHookSaslMechanism authHookSaslMechanism) { + return authHookSaslMechanism.start(request, session); } - - AUTHENTICATION_DEDICATED_LOGGER.info("AUTH method {} failed from {}@{}", authType, username, session.getRemoteAddress().getAddress().getHostAddress()); - - AuditTrail.entry() - .username(username.get()::asString) - .remoteIP(() -> Optional.ofNullable(session.getRemoteAddress())) - .protocol("SMTP") - .action("AUTH") - .parameters(() -> ImmutableMap.of("authType", authType)) - .log("SMTP Authentication failed."); - - return AUTH_FAILED; + return mechanism.start(request, saslAuthenticator()); } - private Response executeHook(SMTPSession session, AuthHook rawHook, Function<AuthHook, HookResult> tc) { - LOGGER.debug("executing hook {}", rawHook); - - long start = System.currentTimeMillis(); - HookResult hRes = tc.apply(rawHook); - long executionTime = System.currentTimeMillis() - start; - - HookResult finalHookResult = Optional.ofNullable(rHooks) - .orElse(ImmutableList.of()).stream() - .peek(rHook -> LOGGER.debug("executing hook {}", rHook)) - .reduce(hRes, (a, b) -> b.onHookResult(session, a, executionTime, rawHook), (a, b) -> { - throw new UnsupportedOperationException(); - }); - - return calcDefaultSMTPResponse(finalHookResult); + private SaslAuthenticator saslAuthenticator() { + return saslAuthenticator + .orElseThrow(() -> new IllegalStateException("SASL authenticator is not configured")); } - /** - * Calculate the SMTPResponse for the given result - * - * @param result the HookResult which should converted to SMTPResponse - * @return the calculated SMTPResponse for the given HookReslut - */ - protected Response calcDefaultSMTPResponse(HookResult result) { - if (result != null) { - HookReturnCode returnCode = result.getResult(); - - String smtpReturnCode = Optional.ofNullable(result.getSmtpRetCode()) - .or(() -> retrieveDefaultSmtpReturnCode(returnCode)) - .orElse(null); - - String smtpDescription = Optional.ofNullable(result.getSmtpDescription()) - .or(() -> retrieveDefaultSmtpDescription(returnCode)) - .orElse(null); - - if (HookReturnCode.Action.ACTIVE_ACTIONS.contains(returnCode.getAction())) { - SMTPResponse response = new SMTPResponse(smtpReturnCode, smtpDescription); - - if (returnCode.isDisconnected()) { - response.setEndSession(true); - } - return response; - } else if (returnCode.isDisconnected()) { - return Response.DISCONNECT; - } - } - return null; - + private void updateEffectiveSaslMechanisms() { + this.effectiveSaslMechanisms = saslMechanisms.stream() + .map(this::adaptPlainMechanismForLegacyAuthHooks) + .collect(ImmutableList.toImmutableList()); } - private Optional<String> retrieveDefaultSmtpDescription(HookReturnCode returnCode) { - switch (returnCode.getAction()) { - case DENY: - return Optional.of("Authentication Failed"); - case DENYSOFT: - return Optional.of("Temporary problem. Please try again later"); - case OK: - return Optional.of("Authentication Succesfull"); - case DECLINED: - case NONE: - break; - } - return Optional.empty(); - } - - private Optional<String> retrieveDefaultSmtpReturnCode(HookReturnCode returnCode) { - switch (returnCode.getAction()) { - case DENY: - return Optional.of(SMTPRetCode.AUTH_FAILED); - case DENYSOFT: - return Optional.of(SMTPRetCode.LOCAL_ERROR); - case OK: - return Optional.of(SMTPRetCode.AUTH_OK); - case DECLINED: - case NONE: - break; + private SaslMechanism adaptPlainMechanismForLegacyAuthHooks(SaslMechanism mechanism) { + if (!authHooks.isEmpty() && mechanism.name().equalsIgnoreCase(SaslMechanismNames.PLAIN)) { + // Legacy AuthHooks own PLAIN authentication: replace, rather than append to, the configured mechanism + // so a declined hook remains a terminal authentication failure as it was before SASL modularization. + return new AuthHookSaslMechanism(mechanism, authHooks, hookResultHooks); } - return Optional.empty(); + return mechanism; } /** @@ -535,15 +454,7 @@ public class AuthCmdHandler @Override public List<String> getImplementedEsmtpFeatures(SMTPSession session) { if (session.isAuthAnnounced()) { - ImmutableList.Builder<String> authTypesBuilder = ImmutableList.builder(); - if (session.getConfiguration().isPlainAuthEnabled()) { - authTypesBuilder.add(AUTH_TYPE_LOGIN, AUTH_TYPE_PLAIN); - } - if (session.getConfiguration().saslConfiguration().isPresent()) { - authTypesBuilder.add(AUTH_TYPE_OAUTHBEARER); - authTypesBuilder.add(AUTH_TYPE_XOAUTH2); - } - ImmutableList<String> authTypes = authTypesBuilder.build(); + ImmutableList<String> authTypes = saslAuthTypes(session); if (authTypes.isEmpty()) { return Collections.emptyList(); } @@ -553,37 +464,36 @@ public class AuthCmdHandler return Collections.emptyList(); } + private ImmutableList<String> saslAuthTypes(SMTPSession session) { + return effectiveSaslMechanisms + .stream() + .filter(mechanism -> mechanism.isAvailableOnTransport(session.isTLSStarted())) + .flatMap(mechanism -> { + if (mechanism.name().equalsIgnoreCase(SaslMechanismNames.PLAIN)) { + return Stream.of(AUTH_TYPE_LOGIN, AUTH_TYPE_PLAIN); + } + return Stream.of(mechanism.name()); + }) + .distinct() + .collect(ImmutableList.toImmutableList()); + } + @Override public List<Class<?>> getMarkerInterfaces() { - List<Class<?>> classes = new ArrayList<>(2); - classes.add(AuthHook.class); - classes.add(HookResultHook.class); - return classes; + return ImmutableList.of(AuthHook.class, HookResultHook.class, SaslAuthResultHook.class); } - @Override @SuppressWarnings("unchecked") public void wireExtensions(Class<?> interfaceName, List<?> extension) throws WiringException { if (AuthHook.class.equals(interfaceName)) { - this.hooks = (List<AuthHook>) extension; - // If no AuthHook is configured then we revert to the default LocalUsersRespository check - if (hooks == null || hooks.isEmpty()) { - throw new WiringException("AuthCmdHandler used without AuthHooks"); - } + this.authHooks = ImmutableList.copyOf((List<AuthHook>) extension); } else if (HookResultHook.class.equals(interfaceName)) { - this.rHooks = (List<HookResultHook>) extension; + this.hookResultHooks = ImmutableList.copyOf((List<HookResultHook>) extension); + } else if (SaslAuthResultHook.class.equals(interfaceName)) { + this.saslAuthResultHooks = ImmutableList.copyOf((List<SaslAuthResultHook>) extension); } - } - - - /** - * Return a list which holds all hooks for the cmdHandler - * - * @return list containing all hooks for the cmd handler - */ - protected List<AuthHook> getHooks() { - return hooks; + updateEffectiveSaslMechanisms(); } @Override diff --git a/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java b/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java deleted file mode 100644 index edf407331d..0000000000 --- a/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/core/esmtp/AuthCmdHandlerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/**************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one * - * or more contributor license agreements. See the NOTICE file * - * distributed with this work for additional information * - * regarding copyright ownership. The ASF licenses this file * - * to you under the Apache License, Version 2.0 (the * - * "License"); you may not use this file except in compliance * - * with the License. You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ****************************************************************/ -package org.apache.james.protocols.smtp.core.esmtp; - -import java.util.Optional; - -import org.apache.james.core.Username; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -class AuthCmdHandlerTest { - - @Test - void shouldReturnEmptyWhenEmptyInput() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues("")).isEmpty(); - } - - @Test - void shouldReturnEmptyWhenBlankInput() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues(" \t\n")).isEmpty(); - } - - @Test - void shouldReturnEmptyWhenTwoBlankParts() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues(" \0\t\n")).isEmpty(); - } - - @Test - void shouldReturnEmptyWhenThreeBlankParts() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues(" \0\0 ")).isEmpty(); - } - - @Test - void shouldReturnUsernameWhenSinglePart() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues("bob")) - .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), Optional.empty())); - } - - @Test - void shouldReturnUsernameAndPassWhenTwoParts() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues("bob\0pass")) - .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), Optional.of("pass"))); - } - - @Test - void shouldReturnUsernameAndPassWhenThreeParts() { - Assertions.assertThat(AuthCmdHandler.parseAuthValues("something\0bob\0pass")) - .contains(new AuthCmdHandler.AuthValues(Username.of("bob"), Optional.of("pass"))); - } - - -} \ No newline at end of file diff --git a/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/utils/BaseFakeSMTPSession.java b/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/utils/BaseFakeSMTPSession.java index bff98d4ce0..8bc8abe5f8 100644 --- a/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/utils/BaseFakeSMTPSession.java +++ b/protocols/smtp/src/test/java/org/apache/james/protocols/smtp/utils/BaseFakeSMTPSession.java @@ -69,11 +69,6 @@ public class BaseFakeSMTPSession implements SMTPSession { throw new UnsupportedOperationException("Unimplemented Stub Method"); } - @Override - public boolean supportsOAuth() { - return false; - } - @Override public String getSessionID() { throw new UnsupportedOperationException("Unimplemented Stub Method"); diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/AuthAnnounceTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/AuthAnnounceTest.java index 7049978ed7..24464bc596 100644 --- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/AuthAnnounceTest.java +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/AuthAnnounceTest.java @@ -23,6 +23,8 @@ import static org.assertj.core.api.Assertions.assertThat; import java.net.InetSocketAddress; +import org.apache.commons.configuration2.HierarchicalConfiguration; +import org.apache.commons.configuration2.tree.ImmutableNode; import org.apache.commons.net.smtp.SMTPClient; import org.apache.james.server.core.configuration.FileConfigurationProvider; import org.assertj.core.api.SoftAssertions; @@ -45,9 +47,7 @@ class AuthAnnounceTest { @Test void authAnnounceAlwaysShouldAnnounceAuth() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-authAnnounceAlways.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-authAnnounceAlways.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -63,9 +63,7 @@ class AuthAnnounceTest { @Test void authAnnounceSometimeShouldNotAnnounceAuthWhenMatching() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-authAnnounceSometimeMatching.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-authAnnounceSometimeMatching.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -81,9 +79,7 @@ class AuthAnnounceTest { @Test void plainAuthShouldNotBeAnnouncedWhenDisabled() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-no-plain.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-no-plain.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -99,9 +95,7 @@ class AuthAnnounceTest { @Test void plainAuthShouldFailWhenDisabled() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-no-plain.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-no-plain.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -114,9 +108,7 @@ class AuthAnnounceTest { @Test void authAnnounceSometimeShouldAnnounceAuthWhenNotMatching() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-authAnnounceSometimeNotMatching.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-authAnnounceSometimeNotMatching.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -132,9 +124,7 @@ class AuthAnnounceTest { @Test void authAnnounceNeverShouldNotAnnounceAuth() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-authAnnounceNever.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-authAnnounceNever.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -150,9 +140,7 @@ class AuthAnnounceTest { @Test void authShouldNotBeAnnouncedOnPlainChannelsWhenRequireSSL() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-requireSSL.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-requireSSL.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -168,9 +156,7 @@ class AuthAnnounceTest { @Test void shouldStartWithPreviousConfiguration() throws Exception { - testSystem.smtpServer.configure(FileConfigurationProvider.getConfig( - ClassLoader.getSystemResourceAsStream("smtpserver-noauth.xml"))); - testSystem.smtpServer.init(); + configureAndInit("smtpserver-noauth.xml"); SMTPClient smtpProtocol = new SMTPClient(); InetSocketAddress bindedAddress = testSystem.getBindedAddress(); @@ -184,4 +170,12 @@ class AuthAnnounceTest { .doesNotContain("250-AUTH LOGIN PLAIN"); }); } + + private void configureAndInit(String configurationName) throws Exception { + HierarchicalConfiguration<ImmutableNode> configuration = FileConfigurationProvider.getConfig( + ClassLoader.getSystemResourceAsStream(configurationName)); + testSystem.configureSaslMechanisms(configuration); + testSystem.smtpServer.configure(configuration); + testSystem.smtpServer.init(); + } } diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java index 45bac66783..8ad3db7278 100644 --- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Optional; import org.apache.commons.configuration2.HierarchicalConfiguration; import org.apache.commons.configuration2.tree.ImmutableNode; @@ -37,9 +38,19 @@ import org.apache.james.core.Username; import org.apache.james.jwt.OidcTokenFixture; import org.apache.james.mailbox.Authorizator; import org.apache.james.protocols.api.OIDCSASLHelper; +import org.apache.james.protocols.api.sasl.SaslAuthenticator; +import org.apache.james.protocols.api.sasl.SaslExchange; +import org.apache.james.protocols.api.sasl.SaslFailure; +import org.apache.james.protocols.api.sasl.SaslIdentity; +import org.apache.james.protocols.api.sasl.SaslInitialRequest; +import org.apache.james.protocols.api.sasl.SaslMechanism; +import org.apache.james.protocols.api.sasl.SaslStep; import org.apache.james.protocols.api.utils.BogusSslContextFactory; import org.apache.james.protocols.api.utils.BogusTrustManagerFactory; import org.apache.james.protocols.lib.mock.ConfigLoader; +import org.apache.james.protocols.sasl.OauthBearerSaslMechanismFactory; +import org.apache.james.protocols.sasl.XOauth2SaslMechanismFactory; +import org.apache.james.protocols.sasl.plain.PlainSaslMechanism; import org.apache.james.util.ClassLoaderUtils; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.AfterEach; @@ -49,6 +60,8 @@ import org.mockserver.integration.ClientAndServer; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; +import com.google.common.collect.ImmutableList; + class SMTPSaslTest { public static final String LOCAL_DOMAIN = "domain.org"; public static final Username USER = Username.of("[email protected]"); @@ -63,6 +76,8 @@ class SMTPSaslTest { String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}", SCOPE, OIDC_URL).getBytes(UTF_8)); public static final String VALID_OAUTHBEARER_TOKEN = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN); public static final String INVALID_OAUTHBEARER_TOKEN = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.INVALID_TOKEN); + private static final SaslMechanism ADVERTISED_OAUTHBEARER = new AdvertisedOnlySaslMechanism("OAUTHBEARER"); + private static final SaslMechanism ADVERTISED_XOAUTH2 = new AdvertisedOnlySaslMechanism("XOAUTH2"); private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem(); @@ -83,17 +98,29 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); - Authorizator authorizator = (userId, otherUserId) -> { + setUpServerWithOidcMechanisms(config); + } + + private void setUpServerWithOidcMechanisms(HierarchicalConfiguration<ImmutableNode> config) throws Exception { + testSystem.setUpWithSaslMechanisms(config, authorizator(), ImmutableList.of( + new OauthBearerSaslMechanismFactory().create(config), + new XOauth2SaslMechanismFactory().create(config))); + addUsers(); + } + + private void addUsers() throws Exception { + testSystem.domainList.addDomain(Domain.of(LOCAL_DOMAIN)); + testSystem.usersRepository.addUser(USER, PASSWORD); + testSystem.usersRepository.addUser(USER2, PASSWORD); + } + + private Authorizator authorizator() { + return (userId, otherUserId) -> { if (userId.equals(USER) && otherUserId.equals(USER2)) { return Authorizator.AuthorizationState.ALLOWED; } return Authorizator.AuthorizationState.FORBIDDEN; }; - - testSystem.setUp(config, authorizator); - testSystem.domainList.addDomain(Domain.of(LOCAL_DOMAIN)); - testSystem.usersRepository.addUser(USER, PASSWORD); - testSystem.usersRepository.addUser(USER2, PASSWORD); } private SMTPSClient initSMTPSClient() throws IOException { @@ -117,7 +144,7 @@ class SMTPSaslTest { client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); client.sendCommand("NOOP"); assertThat(client.getReplyString()).contains("250 2.0.0 OK"); @@ -131,7 +158,7 @@ class SMTPSaslTest { assertThat(client.getReplyString()).contains("334"); client.sendCommand(VALID_OAUTHBEARER_TOKEN); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); client.sendCommand("NOOP"); assertThat(client.getReplyString()).contains("250 2.0.0 OK"); @@ -145,7 +172,7 @@ class SMTPSaslTest { assertThat(client.getReplyString()).contains("334"); client.sendCommand(OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN)); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); client.sendCommand("NOOP"); assertThat(client.getReplyString()).contains("250 2.0.0 OK"); @@ -157,7 +184,7 @@ class SMTPSaslTest { client.sendCommand("AUTH XOAUTH2 " + OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN)); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); } @Test @@ -234,8 +261,8 @@ class SMTPSaslTest { void oauthShouldFailWhenConfigIsNotProvided() throws Exception { testSystem.smtpServer.destroy(); HierarchicalConfiguration<ImmutableNode> config = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml")); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + testSystem.setUp(config, authorizator()); + addUsers(); SMTPSClient client = initSMTPSClient(); @@ -273,8 +300,8 @@ class SMTPSaslTest { void ehloShouldNotAdvertiseOAUTHBEARERWhenConfigIsNotProvided() throws Exception { testSystem.smtpServer.destroy(); HierarchicalConfiguration<ImmutableNode> config = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml")); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + testSystem.setUp(config, authorizator()); + addUsers(); SMTPSClient client = initSMTPSClient(); client.sendCommand("EHLO localhost"); @@ -290,8 +317,8 @@ class SMTPSaslTest { void ehloShouldNotAdvertiseXOAUTH2WhenConfigIsNotProvided() throws Exception { testSystem.smtpServer.destroy(); HierarchicalConfiguration<ImmutableNode> config = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml")); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + testSystem.setUp(config, authorizator()); + addUsers(); SMTPSClient client = initSMTPSClient(); client.sendCommand("EHLO localhost"); @@ -318,8 +345,7 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); config.addProperty("auth.oidc.introspection.url", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), INTROSPECT_TOKEN_URI_PATH)); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + setUpServerWithOidcMechanisms(config); SMTPSClient client = initSMTPSClient(); @@ -347,14 +373,13 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); config.addProperty("auth.oidc.introspection.url", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), INTROSPECT_TOKEN_URI_PATH)); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + setUpServerWithOidcMechanisms(config); SMTPSClient client = initSMTPSClient(); client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); } @Test @@ -372,8 +397,7 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); config.addProperty("auth.oidc.introspection.url", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), invalidURI)); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + setUpServerWithOidcMechanisms(config); SMTPSClient client = initSMTPSClient(); @@ -397,14 +421,13 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); config.addProperty("auth.oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), USERINFO_URI_PATH)); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + setUpServerWithOidcMechanisms(config); SMTPSClient client = initSMTPSClient(); client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); } @Test @@ -421,8 +444,7 @@ class SMTPSaslTest { config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL); config.addProperty("auth.oidc.scope", SCOPE); config.addProperty("auth.oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), USERINFO_URI_PATH)); - testSystem.smtpServer.configure(config); - testSystem.smtpServer.init(); + setUpServerWithOidcMechanisms(config); SMTPSClient client = initSMTPSClient(); @@ -437,9 +459,6 @@ class SMTPSaslTest { String tokenWithImpersonation = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse("[email protected]", OidcTokenFixture.VALID_TOKEN); client.sendCommand("AUTH OAUTHBEARER " + tokenWithImpersonation); - assertThat(client.getReplyString()).contains("334 "); - - client.sendCommand("AQ=="); assertThat(client.getReplyString()).contains("535 Authentication Failed"); } @@ -449,7 +468,7 @@ class SMTPSaslTest { String tokenWithImpersonation = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER2.asString(), OidcTokenFixture.VALID_TOKEN); client.sendCommand("AUTH OAUTHBEARER " + tokenWithImpersonation); - assertThat(client.getReplyString()).contains("235 Authentication successful."); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); } @Test @@ -469,4 +488,208 @@ class SMTPSaslTest { .as("mail received by mail server") .isNotNull(); } + + @Test + void ehloShouldAdvertisePlainAndLoginWhenPlainMechanismIsConfigured() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("EHLO localhost"); + + assertThat(client.getReplyString()).contains("250-AUTH LOGIN PLAIN"); + } + + @Test + void ehloShouldPreserveConfiguredMechanismOrderAndDeduplicateAdvertisement() throws Exception { + resetWithMechanisms(ImmutableList.of( + ADVERTISED_OAUTHBEARER, + new PlainSaslMechanism(true, false), + ADVERTISED_XOAUTH2, + new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("EHLO localhost"); + + assertThat(client.getReplyString()) + .contains("250-AUTH OAUTHBEARER LOGIN PLAIN XOAUTH2") + .doesNotContain("LOGIN PLAIN XOAUTH2 LOGIN PLAIN"); + } + + @Test + void ehloShouldAdvertiseOnlyConfiguredSaslMechanisms() throws Exception { + resetWithMechanisms(ImmutableList.of(ADVERTISED_XOAUTH2)); + SMTPClient client = connectedClient(); + + client.sendCommand("EHLO localhost"); + + assertThat(client.getReplyString()) + .contains("250-AUTH XOAUTH2") + .doesNotContain("LOGIN") + .doesNotContain("PLAIN") + .doesNotContain("OAUTHBEARER"); + } + + @Test + void authPlainShouldBeRejectedWhenPlainMechanismIsNotConfigured() throws Exception { + resetWithMechanisms(ImmutableList.of(ADVERTISED_XOAUTH2)); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH PLAIN " + plainInitialResponse(SMTPServerTestSystem.BOB.asString(), SMTPServerTestSystem.PASSWORD)); + + assertThat(client.getReplyString()).contains("504 Unrecognized Authentication Type"); + } + + @Test + void authPlainWithInitialResponseShouldSucceedWhenCredentialsAreValid() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH PLAIN " + plainInitialResponse(SMTPServerTestSystem.BOB.asString(), SMTPServerTestSystem.PASSWORD)); + + assertThat(client.getReplyString()).contains("235 Authentication Successful"); + } + + @Test + void authPlainContinuationShouldSucceedWhenCredentialsAreValid() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH PLAIN"); + assertThat(client.getReplyString()).contains("334 "); + client.sendCommand(plainInitialResponse(SMTPServerTestSystem.BOB.asString(), SMTPServerTestSystem.PASSWORD)); + + assertThat(client.getReplyString()).contains("235 Authentication Successful"); + } + + @Test + void authLoginShouldSucceedWhenCredentialsAreValid() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH LOGIN"); + assertThat(client.getReplyString()).contains("334 VXNlcm5hbWU6"); + client.sendCommand(base64(SMTPServerTestSystem.BOB.asString())); + assertThat(client.getReplyString()).contains("334 UGFzc3dvcmQ6"); + client.sendCommand(base64(SMTPServerTestSystem.PASSWORD)); + + assertThat(client.getReplyString()).contains("235 Authentication Successful"); + } + + @Test + void authPlainShouldFailWhenCredentialsAreInvalid() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH PLAIN " + plainInitialResponse(SMTPServerTestSystem.BOB.asString(), "bad-password")); + + assertThat(client.getReplyString()).contains("535 Authentication Failed"); + } + + @Test + void authShouldSendFinalServerDataBeforeSuccess() throws Exception { + resetWithMechanisms(ImmutableList.of(new ServerDataSaslMechanism())); + SMTPClient client = connectedClient(); + + client.sendCommand("AUTH SERVER-DATA"); + assertThat(client.getReplyString()).contains("334 " + base64("server-data")); + + client.sendCommand(""); + assertThat(client.getReplyString()).contains("235 Authentication Successful"); + } + + @Test + void mechanismUnavailableOnClearTransportShouldNotBeAdvertisedAndShouldBeRejected() throws Exception { + resetWithMechanisms(ImmutableList.of(new PlainSaslMechanism(true, true))); + SMTPClient client = connectedClient(); + + client.sendCommand("EHLO localhost"); + assertThat(client.getReplyString()).doesNotContain("250-AUTH LOGIN PLAIN"); + + client.sendCommand("AUTH PLAIN " + plainInitialResponse(SMTPServerTestSystem.BOB.asString(), SMTPServerTestSystem.PASSWORD)); + assertThat(client.getReplyString()).contains("504 Unrecognized Authentication Type"); + } + + private void resetWithMechanisms(ImmutableList<SaslMechanism> saslMechanisms) throws Exception { + testSystem.smtpServer.destroy(); + HierarchicalConfiguration<ImmutableNode> configuration = ConfigLoader.getConfig( + ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-authAnnounceAlways.xml")); + testSystem.setUpWithSaslMechanisms(configuration, authorizator(), saslMechanisms); + } + + private SMTPClient connectedClient() throws IOException { + SMTPClient client = new SMTPClient(); + InetSocketAddress bindedAddress = testSystem.getBindedAddress(); + client.connect(bindedAddress.getAddress().getHostAddress(), bindedAddress.getPort()); + return client; + } + + private String plainInitialResponse(String username, String password) { + return base64("\0" + username + "\0" + password); + } + + private String base64(String value) { + return Base64.getEncoder().encodeToString(value.getBytes(UTF_8)); + } + + private static class AdvertisedOnlySaslMechanism implements SaslMechanism { + private final String name; + + private AdvertisedOnlySaslMechanism(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) { + return new SaslExchange() { + @Override + public SaslStep firstStep() { + return new SaslStep.Failure(SaslFailure.authenticationFailed( + Optional.empty(), Optional.empty(), "Test-only mechanism")); + } + + @Override + public SaslStep onResponse(byte[] clientResponse) { + return new SaslStep.Failure(SaslFailure.authenticationFailed( + Optional.empty(), Optional.empty(), "Test-only mechanism")); + } + + @Override + public void close() { + } + }; + } + } + + private static class ServerDataSaslMechanism implements SaslMechanism { + @Override + public String name() { + return "SERVER-DATA"; + } + + @Override + public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) { + return new SaslExchange() { + @Override + public SaslStep firstStep() { + return new SaslStep.Success( + new SaslIdentity(USER, USER), + Optional.of("server-data".getBytes(UTF_8))); + } + + @Override + public SaslStep onResponse(byte[] clientResponse) { + return firstStep(); + } + + @Override + public void close() { + } + }; + } + } } diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java index c7b82cb41c..71391cf7db 100644 --- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTest.java @@ -1363,8 +1363,10 @@ public class SMTPServerTest { smtpProtocol.sendCommand("AUTH PLAIN"); smtpProtocol.sendCommand("canNotDecode"); + // RFC 4954 distinguishes malformed AUTH exchange data from invalid credentials: + // this payload has no valid PLAIN separators, so 501 is more accurate than 535. assertThat(smtpProtocol.getReplyString()) - .contains("535 Authentication Failed"); + .contains("501 Could not decode parameters for AUTH PLAIN"); } @Test diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTestSystem.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTestSystem.java index d276912fe2..4b9ee5a181 100644 --- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTestSystem.java +++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPServerTestSystem.java @@ -25,6 +25,7 @@ import java.net.InetSocketAddress; import java.time.Clock; import java.time.Instant; import java.time.ZoneId; +import java.util.Optional; import org.apache.commons.configuration2.BaseHierarchicalConfiguration; import org.apache.commons.configuration2.HierarchicalConfiguration; @@ -39,6 +40,7 @@ import org.apache.james.domainlist.lib.DomainListConfiguration; import org.apache.james.domainlist.memory.MemoryDomainList; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.mailbox.Authorizator; +import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailrepository.api.MailRepositoryStore; import org.apache.james.mailrepository.api.Protocol; import org.apache.james.mailrepository.memory.MailRepositoryStoreConfiguration; @@ -49,9 +51,15 @@ import org.apache.james.mailrepository.memory.SimpleMailRepositoryLoader; import org.apache.james.metrics.api.Metric; import org.apache.james.metrics.api.MetricFactory; import org.apache.james.metrics.tests.RecordingMetricFactory; +import org.apache.james.protocols.api.sasl.SaslAuthenticationResult; +import org.apache.james.protocols.api.sasl.SaslAuthenticator; +import org.apache.james.protocols.api.sasl.SaslFailure; +import org.apache.james.protocols.api.sasl.SaslIdentity; +import org.apache.james.protocols.api.sasl.SaslMechanism; import org.apache.james.protocols.api.utils.ProtocolServerUtils; import org.apache.james.protocols.lib.LegacyJavaEncryptionFactory; import org.apache.james.protocols.lib.mock.MockProtocolHandlerLoader; +import org.apache.james.protocols.sasl.plain.PlainSaslMechanism; import org.apache.james.queue.api.MailQueueFactory; import org.apache.james.queue.api.RawMailQueueItemDecoratorFactory; import org.apache.james.queue.memory.MemoryMailQueueFactory; @@ -67,6 +75,7 @@ import org.apache.james.server.core.filesystem.FileSystemImpl; import org.apache.james.smtpserver.netty.SMTPServer; import org.apache.james.smtpserver.netty.SmtpMetricsImpl; import org.apache.james.user.api.UsersRepository; +import org.apache.james.user.api.UsersRepositoryException; import org.apache.james.user.memory.MemoryUsersRepository; import com.google.common.collect.ImmutableList; @@ -100,6 +109,23 @@ class SMTPServerTestSystem { void setUp(HierarchicalConfiguration<ImmutableNode> configuration, Authorizator authorizator) throws Exception { preSetUp(authorizator); + configureSaslMechanisms(configuration); + smtpServer.configure(configuration); + smtpServer.init(); + } + + void configureSaslMechanisms(HierarchicalConfiguration<ImmutableNode> configuration) { + smtpServer.setSaslMechanisms(ImmutableList.of(new PlainSaslMechanism( + configuration.getBoolean("auth.plainAuthEnabled", true), + configuration.getBoolean("auth.requireSSL", false)))); + } + + void setUpWithSaslMechanisms(HierarchicalConfiguration<ImmutableNode> configuration, Authorizator authorizator, + ImmutableList<SaslMechanism> saslMechanisms) throws Exception { + preSetUp(authorizator); + + smtpServer.setSaslMechanisms(saslMechanisms); + smtpServer.setSaslAuthenticator(Optional.of(testSaslAuthenticator(authorizator))); smtpServer.configure(configuration); smtpServer.init(); } @@ -116,7 +142,7 @@ class SMTPServerTestSystem { createMailRepositoryStore(); setUpFakeLoader(authorizator); - setUpSMTPServer(); + setUpSMTPServer(authorizator); } void preSetUp() throws Exception { @@ -141,7 +167,7 @@ class SMTPServerTestSystem { return new SMTPServer(smtpMetrics); } - protected void setUpSMTPServer() { + protected void setUpSMTPServer(Authorizator authorizator) { SmtpMetricsImpl smtpMetrics = mock(SmtpMetricsImpl.class); when(smtpMetrics.getCommandsMetric()).thenReturn(mock(Metric.class)); when(smtpMetrics.getConnectionMetric()).thenReturn(mock(Metric.class)); @@ -150,6 +176,8 @@ class SMTPServerTestSystem { smtpServer.setFileSystem(fileSystem); smtpServer.setEncryptionFactory(new LegacyJavaEncryptionFactory(fileSystem)); smtpServer.setProtocolHandlerLoader(chain); + smtpServer.setSaslMechanisms(ImmutableList.of(new PlainSaslMechanism(true, false))); + smtpServer.setSaslAuthenticator(Optional.of(testSaslAuthenticator(authorizator))); } protected void setUpFakeLoader(Authorizator authorizator) { @@ -178,6 +206,34 @@ class SMTPServerTestSystem { .build(); } + private SaslAuthenticator testSaslAuthenticator(Authorizator authorizator) { + return new SaslAuthenticator() { + @Override + public SaslAuthenticationResult authenticatePassword(Username authenticationId, Optional<Username> authorizationId, String password) { + try { + return usersRepository.test(authenticationId, password) + .<SaslAuthenticationResult>map(authenticatedUser -> authorize(new SaslIdentity(authenticatedUser, authorizationId.orElse(authenticatedUser)))) + .orElseGet(() -> new SaslAuthenticationResult.Failure(SaslFailure.invalidCredentials(authenticationId, authorizationId, "Invalid credentials"))); + } catch (UsersRepositoryException e) { + return new SaslAuthenticationResult.Failure(SaslFailure.serverError(Optional.of(authenticationId), authorizationId, "Authentication failed", e)); + } + } + + @Override + public SaslAuthenticationResult authorize(SaslIdentity identity) { + try { + if (identity.authenticationId().equals(identity.authorizationId()) + || authorizator.user(identity.authenticationId()).canLoginAs(identity.authorizationId()) == Authorizator.AuthorizationState.ALLOWED) { + return new SaslAuthenticationResult.Success(identity); + } + return new SaslAuthenticationResult.Failure(SaslFailure.delegationForbidden(identity.authenticationId(), identity.authorizationId(), "Delegation forbidden")); + } catch (MailboxException e) { + return new SaslAuthenticationResult.Failure(SaslFailure.serverError(Optional.of(identity.authenticationId()), Optional.of(identity.authorizationId()), "Authentication failed", e)); + } + } + }; + } + InetSocketAddress getBindedAddress() { return new ProtocolServerUtils(smtpServer).retrieveBindedAddress(); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
