This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 47d443c JAMES-3680 SMTP enable OAUTHBEARER authentication (#810)
47d443c is described below
commit 47d443c62ddfcbd8e7cbf91b2953bf0a72be7a7a
Author: Trần Hồng Quân <[email protected]>
AuthorDate: Mon Jan 10 10:52:45 2022 +0700
JAMES-3680 SMTP enable OAUTHBEARER authentication (#810)
Co-authored-by: Tung Van TRAN <[email protected]>
---
.../protocols/lmtp/LMTPConfigurationImpl.java | 9 +
protocols/smtp/pom.xml | 4 +
.../james/protocols/smtp/SMTPConfiguration.java | 5 +
.../protocols/smtp/SMTPConfigurationImpl.java | 8 +
.../apache/james/protocols/smtp/SMTPSession.java | 4 +-
.../james/protocols/smtp/SMTPSessionImpl.java | 5 +
.../protocols/smtp/core/esmtp/AuthCmdHandler.java | 119 +++++--
.../apache/james/protocols/smtp/hook/AuthHook.java | 3 +
.../protocols/smtp/utils/BaseFakeSMTPSession.java | 5 +
.../sample-configuration/smtpserver.xml | 14 +-
.../src/test/resources/smtpserver.xml | 12 +
.../docs/modules/ROOT/pages/configure/smtp.adoc | 12 +
.../sample-configuration/smtpserver.xml | 14 +-
.../sample-configuration/smtpserver.xml | 14 +-
.../jpa-app/sample-configuration/smtpserver.xml | 14 +-
.../sample-configuration/smtpserver.xml | 14 +-
.../memory-app/sample-configuration/smtpserver.xml | 14 +-
.../src/main/resources/smtpserver.xml | 10 +
.../james/jwt/DefaultPublicKeyProviderTest.java | 1 -
.../apache/james/lmtpserver/netty/LMTPServer.java | 8 +
server/protocols/protocols-smtp/pom.xml | 15 +
.../james/smtpserver/UsersRepositoryAuthHook.java | 38 ++
.../apache/james/smtpserver/netty/SMTPServer.java | 34 +-
.../org/apache/james/smtpserver/SMTPSaslTest.java | 383 +++++++++++++++++++++
.../protocols-smtp/src/test/resources/keystore | Bin 0 -> 2245 bytes
.../test/resources/smtpserver-advancedSecurity.xml | 20 ++
src/site/xdoc/server/config-smtp-lmtp.xml | 8 +
27 files changed, 742 insertions(+), 45 deletions(-)
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 099b71c..9202c26 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
@@ -18,6 +18,10 @@
****************************************************************/
package org.apache.james.protocols.lmtp;
+import java.util.Optional;
+
+import org.apache.james.protocols.api.OidcSASLConfiguration;
+
public class LMTPConfigurationImpl extends LMTPConfiguration {
private long maxMessageSize = 0;
@@ -39,4 +43,9 @@ public class LMTPConfigurationImpl extends LMTPConfiguration {
public boolean isPlainAuthEnabled() {
return false;
}
+
+ @Override
+ public Optional<OidcSASLConfiguration> saslConfiguration() {
+ return Optional.empty();
+ }
}
diff --git a/protocols/smtp/pom.xml b/protocols/smtp/pom.xml
index d50cc37..d4592dc 100644
--- a/protocols/smtp/pom.xml
+++ b/protocols/smtp/pom.xml
@@ -90,6 +90,10 @@
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.james</groupId>
+ <artifactId>james-server-jwt</artifactId>
+ </dependency>
</dependencies>
<build>
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 65f526b..acbf2dc 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
@@ -21,6 +21,9 @@
package org.apache.james.protocols.smtp;
+import java.util.Optional;
+
+import org.apache.james.protocols.api.OidcSASLConfiguration;
import org.apache.james.protocols.api.ProtocolConfiguration;
@@ -72,4 +75,6 @@ public interface SMTPConfiguration extends
ProtocolConfiguration {
boolean isPlainAuthEnabled();
+ Optional<OidcSASLConfiguration> saslConfiguration();
+
}
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 39871f6..5ee7990 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,6 +20,9 @@
package org.apache.james.protocols.smtp;
+import java.util.Optional;
+
+import org.apache.james.protocols.api.OidcSASLConfiguration;
import org.apache.james.protocols.api.ProtocolConfigurationImpl;
/**
@@ -83,4 +86,9 @@ public class SMTPConfigurationImpl extends
ProtocolConfigurationImpl implements
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/SMTPSession.java
b/protocols/smtp/src/main/java/org/apache/james/protocols/smtp/SMTPSession.java
index 0be25fb..3bfda32 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
@@ -77,7 +77,9 @@ public interface SMTPSession extends ProtocolSession {
* @return recipient count
*/
int getRcptCount();
-
+
+
+ boolean supportsOAuth();
}
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 c583cf6..814c492 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
@@ -65,6 +65,11 @@ public class SMTPSessionImpl extends ProtocolSessionImpl
implements SMTPSession
}
@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 d1bc5c8..210c6a8 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
@@ -32,9 +32,11 @@ import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.StringTokenizer;
+import java.util.function.Function;
import org.apache.commons.lang3.StringUtils;
import org.apache.james.core.Username;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
import org.apache.james.protocols.api.Request;
import org.apache.james.protocols.api.Response;
import org.apache.james.protocols.api.handler.CommandHandler;
@@ -53,13 +55,14 @@ import
org.apache.james.protocols.smtp.hook.MailParametersHook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
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
@@ -70,8 +73,8 @@ public class AuthCmdHandler
private static final Collection<String> COMMANDS = ImmutableSet.of("AUTH");
private static final Logger LOGGER =
LoggerFactory.getLogger(CommandHandler.class);
private static final String[] MAIL_PARAMS = { "AUTH" };
- private static final List<String> ESMTP_FEATURES = ImmutableList.of("AUTH
LOGIN PLAIN", "AUTH=LOGIN PLAIN");
-
+ private static final String AUTH_TYPES_DELIMITER = " ";
+
private static final Response AUTH_ABORTED = new
SMTPResponse(SMTPRetCode.SYNTAX_ERROR_ARGUMENTS,
DSNStatus.getStatus(DSNStatus.PERMANENT, DSNStatus.SECURITY_AUTH) + "
Authentication aborted").immutable();
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();
@@ -99,12 +102,12 @@ public class AuthCmdHandler
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("No " + charset + " support!");
}
-
+
}
private Response handleCommand(SMTPSession session, String line) {
// See JAMES-939
-
+
// According to RFC2554:
// "If the client wishes to cancel an authentication exchange, it
issues a line with a single "*".
// If the server receives such an answer, it MUST reject the AUTH
@@ -132,10 +135,16 @@ 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;
/**
@@ -183,7 +192,7 @@ public class AuthCmdHandler
return doPlainAuthPass(session, userpass);
}
} else if (authType.equals(AUTH_TYPE_LOGIN) &&
session.getConfiguration().isPlainAuthEnabled()) {
-
+
if (initialResponse == null) {
session.pushLineHandler(new AbstractSMTPLineHandler() {
@Override
@@ -196,12 +205,42 @@ public class AuthCmdHandler
String user = initialResponse.trim();
return doLoginAuthPass(session, user);
}
+ } else if ((authType.equals(AUTH_TYPE_OAUTHBEARER) ||
authType.equals(AUTH_TYPE_XOAUTH2))
+ && session.supportsOAuth()) {
+ return doSASLAuthentication(session, initialResponse);
} else {
return doUnknownAuth(session, authType, initialResponse);
}
}
}
+ private Response doSASLAuthentication(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)))
+ .orElse(doUnknownAuth(session, AUTH_TYPE_OAUTHBEARER,
initialResponseString));
+ }
+
+ private Response failSasl(OidcSASLConfiguration saslConfiguration,
SMTPSession session) {
+ String rawResponse =
String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}",
+ saslConfiguration.getScope(),
+ saslConfiguration.getOidcConfigurationURL().toString());
+
+ session.pushLineHandler(new AbstractSMTPLineHandler() {
+ @Override
+ protected Response onCommand(SMTPSession session, String l) {
+ session.popLineHandler();
+
+ return AUTH_FAILED;
+ }
+ });
+ return new SMTPResponse("334",
Base64.getEncoder().encodeToString(rawResponse.getBytes()));
+ }
+
/**
* Carries out the Plain AUTH SASL exchange.
*
@@ -269,7 +308,7 @@ public class AuthCmdHandler
}
// Authenticate user
Response response = doAuthTest(session, Username.of(user), pass,
"PLAIN");
-
+
session.popLineHandler();
return response;
@@ -299,9 +338,9 @@ public class AuthCmdHandler
user = null;
}
}
-
+
session.popLineHandler();
-
+
session.pushLineHandler(new AbstractSMTPLineHandler() {
private Username username;
@@ -318,7 +357,7 @@ public class AuthCmdHandler
}.setUsername(Username.of(user)));
return AUTH_READY_PASSWORD_LOGIN;
}
-
+
private Response doLoginAuthPassCheck(SMTPSession session, Username
username, String pass) {
if (pass != null) {
try {
@@ -329,7 +368,7 @@ public class AuthCmdHandler
pass = null;
}
}
-
+
session.popLineHandler();
// Authenticate user
@@ -342,25 +381,12 @@ public class AuthCmdHandler
}
Response res = null;
-
+
List<AuthHook> hooks = getHooks();
-
+
if (hooks != null) {
for (AuthHook rawHook : hooks) {
- LOGGER.debug("executing hook {}", rawHook);
-
- long start = System.currentTimeMillis();
- HookResult hRes = rawHook.doAuth(session, username, pass);
- long executionTime = System.currentTimeMillis() - start;
-
- if (rHooks != null) {
- for (HookResultHook rHook : rHooks) {
- LOGGER.debug("executing hook {}", rHook);
- hRes = rHook.onHookResult(session, hRes,
executionTime, rawHook);
- }
- }
-
- res = calcDefaultSMTPResponse(hRes);
+ res = executeHook(session, rawHook, hook ->
hook.doAuth(session, username, pass));
if (res != null) {
if (SMTPRetCode.AUTH_FAILED.equals(res.getRetCode())) {
@@ -379,10 +405,26 @@ public class AuthCmdHandler
return res;
}
+ 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);
+ }
/**
* Calculate the SMTPResponse for the given result
- *
+ *
* @param result the HookResult which should converted to SMTPResponse
* @return the calculated SMTPResponse for the given HookReslut
*/
@@ -397,7 +439,7 @@ public class AuthCmdHandler
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);
@@ -465,9 +507,20 @@ public class AuthCmdHandler
@Override
public List<String> getImplementedEsmtpFeatures(SMTPSession session) {
if (session.isAuthAnnounced()) {
+ ImmutableList.Builder<String> authTypesBuilder =
ImmutableList.builder();
if (session.getConfiguration().isPlainAuthEnabled()) {
- return ESMTP_FEATURES;
+ 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();
+ if (authTypes.isEmpty()) {
+ return Collections.emptyList();
}
+ String joined = Joiner.on(AUTH_TYPES_DELIMITER).join(authTypes);
+ return ImmutableList.of("AUTH " + joined, "AUTH=" + joined);
}
return Collections.emptyList();
}
@@ -493,11 +546,11 @@ public class AuthCmdHandler
this.rHooks = (List<HookResultHook>) extension;
}
}
-
+
/**
* Return a list which holds all hooks for the cmdHandler
- *
+ *
* @return list containing all hooks for the cmd handler
*/
protected List<AuthHook> getHooks() {
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 60cc5a3..f9e7f76 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,6 +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.protocols.smtp.SMTPSession;
/**
@@ -35,4 +36,6 @@ public interface AuthHook extends Hook {
* @return HockResult
*/
HookResult doAuth(SMTPSession session, Username username, String password);
+
+ HookResult doSasl(SMTPSession session, OidcSASLConfiguration
saslConfiguration, String initialResponse);
}
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 e8c5b06..0954de6 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
@@ -63,6 +63,11 @@ public class BaseFakeSMTPSession implements SMTPSession {
}
@Override
+ public boolean supportsOAuth() {
+ return false;
+ }
+
+ @Override
public String getSessionID() {
throw new UnsupportedOperationException("Unimplemented Stub Method");
}
diff --git a/server/apps/cassandra-app/sample-configuration/smtpserver.xml
b/server/apps/cassandra-app/sample-configuration/smtpserver.xml
index e994912..1de4a16 100644
--- a/server/apps/cassandra-app/sample-configuration/smtpserver.xml
+++ b/server/apps/cassandra-app/sample-configuration/smtpserver.xml
@@ -47,7 +47,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -90,6 +90,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -132,6 +138,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git
a/server/apps/cli-integration-tests/src/test/resources/smtpserver.xml
b/server/apps/cli-integration-tests/src/test/resources/smtpserver.xml
index adc3a72..0fef5ec 100644
--- a/server/apps/cli-integration-tests/src/test/resources/smtpserver.xml
+++ b/server/apps/cli-integration-tests/src/test/resources/smtpserver.xml
@@ -65,6 +65,12 @@
<announce>always</announce>
<requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>0.0.0.0/0</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -94,6 +100,12 @@
<announce>always</announce>
<requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>0.0.0.0/0</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git
a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp.adoc
b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp.adoc
index e4cff82..2207ba7 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/configure/smtp.adoc
@@ -91,6 +91,18 @@ channels.
| This is an optional tag, defaults to true. If false, AUTH PLAIN and AUTH
LOGIN will not be exposed. This setting
can be used to enforce strong authentication mechanisms.
+| auth.oidc.oidcConfigurationURL
+| Provide OIDC url address for information to user. Only configure this when
you want to authenticate SMTP server using a OIDC provider.
+
+| auth.oidc.jwksURL
+| Provide url to get OIDC's JSON Web Key Set to validate user token. Only
configure this when you want to authenticate SMTP server using a OIDC provider.
+
+| auth.oidc.claim
+| Claim string uses to identify user. E.g: "email_address". Only configure
this when you want to authenticate SMTP server using a OIDC provider.
+
+| auth.oidc.scope
+| An OAuth scope that is valid to access the service (RF: RFC7628). Only
configure this when you want to authenticate SMTP server using a OIDC provider.
+
| authorizedAddresses
| Authorize specific addresses/networks.
diff --git a/server/apps/distributed-app/sample-configuration/smtpserver.xml
b/server/apps/distributed-app/sample-configuration/smtpserver.xml
index 71ebb29..e0c5c68 100644
--- a/server/apps/distributed-app/sample-configuration/smtpserver.xml
+++ b/server/apps/distributed-app/sample-configuration/smtpserver.xml
@@ -47,7 +47,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -90,6 +90,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -129,6 +135,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git
a/server/apps/distributed-pop3-app/sample-configuration/smtpserver.xml
b/server/apps/distributed-pop3-app/sample-configuration/smtpserver.xml
index 17478a0..b7472f5 100644
--- a/server/apps/distributed-pop3-app/sample-configuration/smtpserver.xml
+++ b/server/apps/distributed-pop3-app/sample-configuration/smtpserver.xml
@@ -37,7 +37,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -67,6 +67,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -96,6 +102,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git a/server/apps/jpa-app/sample-configuration/smtpserver.xml
b/server/apps/jpa-app/sample-configuration/smtpserver.xml
index 6445e6a..26f4189 100644
--- a/server/apps/jpa-app/sample-configuration/smtpserver.xml
+++ b/server/apps/jpa-app/sample-configuration/smtpserver.xml
@@ -47,7 +47,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -87,6 +87,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -126,6 +132,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git a/server/apps/jpa-smtp-app/sample-configuration/smtpserver.xml
b/server/apps/jpa-smtp-app/sample-configuration/smtpserver.xml
index 6445e6a..26f4189 100644
--- a/server/apps/jpa-smtp-app/sample-configuration/smtpserver.xml
+++ b/server/apps/jpa-smtp-app/sample-configuration/smtpserver.xml
@@ -47,7 +47,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -87,6 +87,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -126,6 +132,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git a/server/apps/memory-app/sample-configuration/smtpserver.xml
b/server/apps/memory-app/sample-configuration/smtpserver.xml
index 6445e6a..26f4189 100644
--- a/server/apps/memory-app/sample-configuration/smtpserver.xml
+++ b/server/apps/memory-app/sample-configuration/smtpserver.xml
@@ -47,7 +47,7 @@
<connectionLimitPerIP>0</connectionLimitPerIP>
<auth>
<announce>never</announce>
- <requireSSL>true</requireSSL>
+ <requireSSL>false</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
@@ -87,6 +87,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
@@ -126,6 +132,12 @@
<announce>forUnauthorizedAddresses</announce>
<requireSSL>true</requireSSL>
<plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
</auth>
<authorizedAddresses>127.0.0.0/8</authorizedAddresses>
<!-- Trust authenticated users -->
diff --git
a/server/mailet/integration-testing/src/main/resources/smtpserver.xml
b/server/mailet/integration-testing/src/main/resources/smtpserver.xml
index 641c9c4..8af96e3 100644
--- a/server/mailet/integration-testing/src/main/resources/smtpserver.xml
+++ b/server/mailet/integration-testing/src/main/resources/smtpserver.xml
@@ -73,6 +73,16 @@
{{#hasAuthorizedAddresses}}
<authorizedAddresses>{{authorizedAddresses}}</authorizedAddresses>
{{/hasAuthorizedAddresses}}
+ <auth>
+ <requireSSL>true</requireSSL>
+ <plainAuthEnabled>true</plainAuthEnabled>
+ <oidc>
+
<oidcConfigurationURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/.well-known/openid-configuration</oidcConfigurationURL>
+
<jwksURL>https://auth.upn.integration-open-paas.org/auth/realms/upn/protocol/openid-connect/certs</jwksURL>
+ <claim>email</claim>
+ <scope>openid profile email</scope>
+ </oidc>
+ </auth>
<!-- Trust authenticated users -->
<verifyIdentity>false</verifyIdentity>
<maxmessagesize>{{maxmessagesize}}</maxmessagesize>
diff --git
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/DefaultPublicKeyProviderTest.java
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/DefaultPublicKeyProviderTest.java
index c4778ba..4c59934 100644
---
a/server/protocols/jwt/src/test/java/org/apache/james/jwt/DefaultPublicKeyProviderTest.java
+++
b/server/protocols/jwt/src/test/java/org/apache/james/jwt/DefaultPublicKeyProviderTest.java
@@ -19,7 +19,6 @@
package org.apache.james.jwt;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.security.Security;
import java.security.interfaces.RSAPublicKey;
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 42c38b8..e1375fc 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
@@ -18,11 +18,14 @@
****************************************************************/
package org.apache.james.lmtpserver.netty;
+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.lmtpserver.CoreCmdHandlerLoader;
import org.apache.james.lmtpserver.jmx.JMXHandlersLoader;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
import org.apache.james.protocols.lib.handler.HandlersPackage;
import org.apache.james.protocols.lib.netty.AbstractProtocolAsyncServer;
import org.apache.james.protocols.lmtp.LMTPConfiguration;
@@ -108,6 +111,11 @@ public class LMTPServer extends
AbstractProtocolAsyncServer implements LMTPServe
public boolean isPlainAuthEnabled() {
return false;
}
+
+ @Override
+ public Optional<OidcSASLConfiguration> saslConfiguration() {
+ return Optional.empty();
+ }
}
@Override
diff --git a/server/protocols/protocols-smtp/pom.xml
b/server/protocols/protocols-smtp/pom.xml
index 936450b..0b916ca 100644
--- a/server/protocols/protocols-smtp/pom.xml
+++ b/server/protocols/protocols-smtp/pom.xml
@@ -79,6 +79,16 @@
</dependency>
<dependency>
<groupId>${james.groupId}</groupId>
+ <artifactId>james-server-jwt</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
+ <artifactId>james-server-jwt</artifactId>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${james.groupId}</groupId>
<artifactId>james-server-lifecycle-api</artifactId>
</dependency>
<dependency>
@@ -189,6 +199,11 @@
<artifactId>apache-jspf-resolver</artifactId>
</dependency>
<dependency>
+ <groupId>org.mock-server</groupId>
+ <artifactId>mockserver-netty</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
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 bf88b00..695025a 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,9 +18,16 @@
****************************************************************/
package org.apache.james.smtpserver;
+import java.util.Optional;
+
import javax.inject.Inject;
+import javax.mail.internet.AddressException;
+import org.apache.james.core.MailAddress;
import org.apache.james.core.Username;
+import org.apache.james.jwt.OidcJwtTokenVerifier;
+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;
@@ -59,4 +66,35 @@ public class UsersRepositoryAuthHook implements AuthHook {
}
return HookResult.DECLINED;
}
+
+ @Override
+ public HookResult doSasl(SMTPSession session, OidcSASLConfiguration
configuration, String initialResponse) {
+ return OIDCSASLParser.parse(initialResponse)
+ .flatMap(value -> new OidcJwtTokenVerifier()
+ .verifyAndExtractClaim(value.getToken(),
configuration.getJwksURL(), configuration.getClaim()))
+ .flatMap(this::extractUserFromClaim)
+ .map(username -> {
+ try {
+ users.assertValid(username);
+ session.setUsername(username);
+ session.setRelayingAllowed(true);
+ return HookResult.builder()
+ .hookReturnCode(HookReturnCode.ok())
+ .smtpDescription("Authentication successful.")
+ .build();
+ } catch (UsersRepositoryException e) {
+ LOGGER.warn("Invalid username", e);
+ return HookResult.DECLINED;
+ }
+ })
+ .orElse(HookResult.DECLINED);
+ }
+
+ private Optional<Username> extractUserFromClaim(String claimValue) {
+ try {
+ return Optional.of(Username.fromMailAddress(new
MailAddress(claimValue)));
+ } catch (AddressException e) {
+ return Optional.empty();
+ }
+ }
}
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 633813e..e725db7 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
@@ -21,6 +21,7 @@ package org.apache.james.smtpserver.netty;
import static
org.apache.james.smtpserver.netty.SMTPServer.AuthenticationAnnounceMode.ALWAYS;
import static
org.apache.james.smtpserver.netty.SMTPServer.AuthenticationAnnounceMode.NEVER;
+import java.net.MalformedURLException;
import java.util.Locale;
import java.util.Optional;
@@ -31,6 +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.protocols.api.ProtocolSession;
import org.apache.james.protocols.api.ProtocolTransport;
import org.apache.james.protocols.lib.handler.HandlersPackage;
@@ -86,15 +88,29 @@ public class SMTPServer extends AbstractProtocolAsyncServer
implements SMTPServe
}
public static class AuthenticationConfiguration {
+ private static final String OIDC_PATH = "auth.oidc";
-
- public static AuthenticationConfiguration
parse(HierarchicalConfiguration<ImmutableNode> configuration) {
+ public static AuthenticationConfiguration
parse(HierarchicalConfiguration<ImmutableNode> configuration) throws
ConfigurationException {
return new AuthenticationConfiguration(
Optional.ofNullable(configuration.getString("auth.announce",
null))
.map(AuthenticationAnnounceMode::parse)
.orElseGet(() ->
fallbackAuthenticationAnnounceMode(configuration)),
configuration.getBoolean("auth.requireSSL", false),
- configuration.getBoolean("auth.plainAuthEnabled", true));
+ configuration.getBoolean("auth.plainAuthEnabled", true),
+ parseSASLConfiguration(configuration));
+ }
+
+ private static Optional<OidcSASLConfiguration>
parseSASLConfiguration(HierarchicalConfiguration<ImmutableNode> configuration)
throws ConfigurationException {
+ boolean haveOidcProperties =
configuration.getKeys(OIDC_PATH).hasNext();
+ if (haveOidcProperties) {
+ try {
+ return
Optional.of(OidcSASLConfiguration.parse(configuration.configurationAt(OIDC_PATH)));
+ } catch (MalformedURLException exception) {
+ throw new ConfigurationException("Failed to retrieve oauth
component", exception);
+ }
+ } else {
+ return Optional.empty();
+ }
}
private static AuthenticationAnnounceMode
fallbackAuthenticationAnnounceMode(HierarchicalConfiguration<ImmutableNode>
configuration) {
@@ -104,11 +120,13 @@ public class SMTPServer extends
AbstractProtocolAsyncServer implements SMTPServe
private final AuthenticationAnnounceMode authenticationAnnounceMode;
private final boolean requireSSL;
private final boolean plainAuthEnabled;
+ private final Optional<OidcSASLConfiguration> saslConfiguration;
- public AuthenticationConfiguration(AuthenticationAnnounceMode
authenticationAnnounceMode, boolean requireSSL, boolean plainAuthEnabled) {
+ public AuthenticationConfiguration(AuthenticationAnnounceMode
authenticationAnnounceMode, boolean requireSSL, boolean plainAuthEnabled,
Optional<OidcSASLConfiguration> saslConfiguration) {
this.authenticationAnnounceMode = authenticationAnnounceMode;
this.requireSSL = requireSSL;
this.plainAuthEnabled = plainAuthEnabled;
+ this.saslConfiguration = saslConfiguration;
}
public AuthenticationAnnounceMode getAuthenticationAnnounceMode() {
@@ -122,6 +140,10 @@ public class SMTPServer extends
AbstractProtocolAsyncServer implements SMTPServe
public boolean isPlainAuthEnabled() {
return plainAuthEnabled;
}
+
+ public Optional<OidcSASLConfiguration> getSaslConfiguration() {
+ return saslConfiguration;
+ }
}
/**
@@ -318,6 +340,10 @@ public class SMTPServer extends
AbstractProtocolAsyncServer implements SMTPServe
return "JAMES SMTP Server ";
}
+ @Override
+ public Optional<OidcSASLConfiguration> saslConfiguration() {
+ return authenticationConfiguration.getSaslConfiguration();
+ }
}
@Override
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
new file mode 100644
index 0000000..0c07100
--- /dev/null
+++
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java
@@ -0,0 +1,383 @@
+/****************************************************************
+ * 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.smtpserver;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.apache.commons.configuration2.BaseHierarchicalConfiguration;
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.commons.net.smtp.SMTPClient;
+import org.apache.commons.net.smtp.SMTPSClient;
+import org.apache.james.UserEntityValidator;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.dnsservice.api.InMemoryDNSService;
+import org.apache.james.domainlist.api.DomainList;
+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.jwt.OidcTokenFixture;
+import org.apache.james.mailrepository.api.MailRepositoryStore;
+import org.apache.james.mailrepository.api.Protocol;
+import org.apache.james.mailrepository.memory.MailRepositoryStoreConfiguration;
+import org.apache.james.mailrepository.memory.MemoryMailRepository;
+import org.apache.james.mailrepository.memory.MemoryMailRepositoryStore;
+import org.apache.james.mailrepository.memory.MemoryMailRepositoryUrlStore;
+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.OIDCSASLHelper;
+import org.apache.james.protocols.api.utils.BogusSslContextFactory;
+import org.apache.james.protocols.api.utils.BogusTrustManagerFactory;
+import org.apache.james.protocols.api.utils.ProtocolServerUtils;
+import org.apache.james.protocols.lib.mock.ConfigLoader;
+import org.apache.james.protocols.lib.mock.MockProtocolHandlerLoader;
+import org.apache.james.queue.api.MailQueueFactory;
+import org.apache.james.queue.api.RawMailQueueItemDecoratorFactory;
+import org.apache.james.queue.memory.MemoryMailQueueFactory;
+import org.apache.james.rrt.api.AliasReverseResolver;
+import org.apache.james.rrt.api.CanSendFrom;
+import org.apache.james.rrt.api.RecipientRewriteTable;
+import org.apache.james.rrt.api.RecipientRewriteTableConfiguration;
+import org.apache.james.rrt.lib.AliasReverseResolverImpl;
+import org.apache.james.rrt.lib.CanSendFromImpl;
+import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
+import org.apache.james.server.core.configuration.Configuration;
+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.memory.MemoryUsersRepository;
+import org.apache.james.util.ClassLoaderUtils;
+import org.assertj.core.api.SoftAssertions;
+import org.jboss.netty.util.HashedWheelTimer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.HttpResponse;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.TypeLiteral;
+
+class SMTPSaslTest {
+ public static final String LOCAL_DOMAIN = "domain.org";
+ public static final Username USER = Username.of("[email protected]");
+ public static final String PASSWORD = "userpassword";
+ public static final String JWKS_URI_PATH = "/jwks";
+ public static final String OIDC_URL = "https://example.com/jwks";
+ public static final String SCOPE = "scope";
+ public static final String FAIL_RESPONSE_TOKEN =
Base64.getEncoder().encodeToString(
+
String.format("{\"status\":\"invalid_token\",\"scope\":\"%s\",\"schemes\":\"%s\"}",
SCOPE, OIDC_URL).getBytes(UTF_8));
+ public static final String VALID_TOKEN =
OIDCSASLHelper.generateOauthBearer(USER.asString(),
OidcTokenFixture.VALID_TOKEN);
+ public static final String INVALID_TOKEN =
OIDCSASLHelper.generateOauthBearer(USER.asString(),
OidcTokenFixture.INVALID_TOKEN);
+
+
+ protected HashedWheelTimer hashedWheelTimer;
+ protected MemoryDomainList domainList;
+ protected MemoryUsersRepository usersRepository;
+ protected SMTPServerTest.AlterableDNSServer dnsServer;
+ protected MemoryMailRepositoryStore mailRepositoryStore;
+ protected FileSystemImpl fileSystem;
+ protected Configuration configuration;
+ protected MockProtocolHandlerLoader chain;
+ protected MemoryMailQueueFactory queueFactory;
+ protected MemoryMailQueueFactory.MemoryCacheableMailQueue queue;
+
+ private SMTPServer smtpServer;
+ private ClientAndServer authServer;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ domainList = new MemoryDomainList(new InMemoryDNSService());
+ domainList.configure(DomainListConfiguration.DEFAULT);
+
+ domainList.addDomain(Domain.of(LOCAL_DOMAIN));
+ usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+ usersRepository.addUser(USER, PASSWORD);
+
+ createMailRepositoryStore();
+
+ setUpFakeLoader();
+ hashedWheelTimer = new HashedWheelTimer();
+ setUpSMTPServer();
+
+ authServer = ClientAndServer.startClientAndServer(0);
+ authServer
+ .when(HttpRequest.request().withPath(JWKS_URI_PATH))
+ .respond(HttpResponse.response().withStatusCode(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(OidcTokenFixture.JWKS_RESPONSE,
StandardCharsets.UTF_8));
+
+ HierarchicalConfiguration<ImmutableNode> config =
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml"));
+ config.addProperty("auth.oidc.jwksURL",
String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(),
JWKS_URI_PATH));
+ config.addProperty("auth.oidc.claim", OidcTokenFixture.CLAIM);
+ config.addProperty("auth.oidc.oidcConfigurationURL", OIDC_URL);
+ config.addProperty("auth.oidc.scope", SCOPE);
+ smtpServer.configure(config);
+ smtpServer.init();
+ }
+
+ protected void createMailRepositoryStore() throws Exception {
+ configuration = Configuration.builder()
+ .workingDirectory("../")
+ .configurationFromClasspath()
+ .build();
+ fileSystem = new FileSystemImpl(configuration.directories());
+ MemoryMailRepositoryUrlStore urlStore = new
MemoryMailRepositoryUrlStore();
+
+ MailRepositoryStoreConfiguration configuration =
MailRepositoryStoreConfiguration.forItems(
+ new MailRepositoryStoreConfiguration.Item(
+ ImmutableList.of(new Protocol("memory")),
+ MemoryMailRepository.class.getName(),
+ new BaseHierarchicalConfiguration()));
+
+ mailRepositoryStore = new MemoryMailRepositoryStore(urlStore, new
SimpleMailRepositoryLoader(), configuration);
+ mailRepositoryStore.init();
+ }
+
+ protected SMTPServer createSMTPServer(SmtpMetricsImpl smtpMetrics) {
+ return new SMTPServer(smtpMetrics);
+ }
+
+ protected void setUpSMTPServer() {
+ SmtpMetricsImpl smtpMetrics = mock(SmtpMetricsImpl.class);
+ when(smtpMetrics.getCommandsMetric()).thenReturn(mock(Metric.class));
+ when(smtpMetrics.getConnectionMetric()).thenReturn(mock(Metric.class));
+ smtpServer = createSMTPServer(smtpMetrics);
+ smtpServer.setDnsService(dnsServer);
+ smtpServer.setFileSystem(fileSystem);
+ smtpServer.setHashWheelTimer(hashedWheelTimer);
+ smtpServer.setProtocolHandlerLoader(chain);
+ }
+
+ protected void setUpFakeLoader() {
+ dnsServer = new SMTPServerTest.AlterableDNSServer();
+
+ MemoryRecipientRewriteTable rewriteTable = new
MemoryRecipientRewriteTable();
+
rewriteTable.setConfiguration(RecipientRewriteTableConfiguration.DEFAULT_ENABLED);
+ AliasReverseResolver aliasReverseResolver = new
AliasReverseResolverImpl(rewriteTable);
+ CanSendFrom canSendFrom = new CanSendFromImpl(rewriteTable,
aliasReverseResolver);
+ queueFactory = new MemoryMailQueueFactory(new
RawMailQueueItemDecoratorFactory());
+ queue = queueFactory.createQueue(MailQueueFactory.SPOOL);
+
+ chain = MockProtocolHandlerLoader.builder()
+ .put(binder ->
binder.bind(DomainList.class).toInstance(domainList))
+ .put(binder -> binder.bind(new TypeLiteral<MailQueueFactory<?>>()
{}).toInstance(queueFactory))
+ .put(binder ->
binder.bind(RecipientRewriteTable.class).toInstance(rewriteTable))
+ .put(binder ->
binder.bind(CanSendFrom.class).toInstance(canSendFrom))
+ .put(binder ->
binder.bind(FileSystem.class).toInstance(fileSystem))
+ .put(binder ->
binder.bind(MailRepositoryStore.class).toInstance(mailRepositoryStore))
+ .put(binder -> binder.bind(DNSService.class).toInstance(dnsServer))
+ .put(binder ->
binder.bind(UsersRepository.class).toInstance(usersRepository))
+ .put(binder ->
binder.bind(MetricFactory.class).to(RecordingMetricFactory.class))
+ .put(binder ->
binder.bind(UserEntityValidator.class).toInstance(UserEntityValidator.NOOP))
+ .build();
+ }
+
+ private SMTPSClient initSMTPSClient() throws IOException {
+ SMTPSClient client = new SMTPSClient(false,
BogusSslContextFactory.getClientContext());
+ client.setTrustManager(BogusTrustManagerFactory.getTrustManagers()[0]);
+ InetSocketAddress bindedAddress = new
ProtocolServerUtils(smtpServer).retrieveBindedAddress();
+ client.connect(bindedAddress.getAddress().getHostAddress(),
bindedAddress.getPort());
+ client.execTLS();
+ return client;
+ }
+
+ @AfterEach
+ void tearDown() {
+ smtpServer.destroy();
+ hashedWheelTimer.stop();
+ authServer.stop();
+ }
+
+ @Test
+ void oauthShouldSuccessWhenValidToken() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+
+ assertThat(client.getReplyString()).contains("235 Authentication
successful.");
+ }
+
+ @Test
+ void oauthShouldSupportXOAUTH2Type() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("AUTH XOAUTH2 " + VALID_TOKEN);
+
+ assertThat(client.getReplyString()).contains("235 Authentication
successful.");
+ }
+
+ @Test
+ void oauthWithNoTLSConnectShouldFail() throws Exception {
+ SMTPClient client = new SMTPClient();
+ InetSocketAddress bindedAddress = new
ProtocolServerUtils(smtpServer).retrieveBindedAddress();
+ client.connect(bindedAddress.getAddress().getHostAddress(),
bindedAddress.getPort());
+
+ client.sendCommand("EHLO localhost");
+ assertThat(client.getReplyString())
+ .as("Should not advertise OAUTHBEARER when no TLS connect.")
+ .doesNotContain("OAUTHBEARER");
+
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+ assertThat(client.getReplyString()).contains("504 Unrecognized
Authentication Type");
+ }
+
+ @Test
+ void oauthShouldFailWhenInvalidToken() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("AUTH OAUTHBEARER " + INVALID_TOKEN);
+ assertThat(client.getReplyString()).contains("334 " +
FAIL_RESPONSE_TOKEN);
+
+ client.sendCommand("AQ==");
+ assertThat(client.getReplyString()).contains("535 Authentication
Failed");
+ }
+
+ @Test
+ void sendMailShouldSuccessWhenAuthenticatedByOAuthBearer() throws
Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("EHLO localhost");
+
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+
+ client.setSender(USER.asString());
+ client.addRecipient("[email protected]");
+ client.sendShortMessageData("Subject: test\r\n\r\nTest body
testAuth\r\n");
+ client.quit();
+
+ assertThat(queue.getLastMail())
+ .as("mail received by mail server")
+ .isNotNull();
+ }
+
+ @Test
+ void sendMailShouldFailWhenNotAuthenticated() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("EHLO localhost");
+
+ client.setSender(USER.asString());
+ client.addRecipient("[email protected]");
+ client.sendShortMessageData("Subject: test\r\n\r\nTest body
testAuth\r\n");
+ client.quit();
+
+ assertThat(queue.getLastMail())
+ .as("mail received by mail server")
+ .isNull();
+ }
+
+ @Test
+ void shouldNotOauthWhenAlreadyAuthenticated() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+
+ assertThat(client.getReplyString()).contains("503 5.5.0 User has
previously authenticated. Further authentication is not required!");
+ }
+
+ @Test
+ void oauthShouldFailWhenConfigIsNotProvided() throws Exception {
+ smtpServer.destroy();
+ HierarchicalConfiguration<ImmutableNode> config =
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml"));
+ smtpServer.configure(config);
+ smtpServer.init();
+
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+ assertThat(client.getReplyString()).contains("504 Unrecognized
Authentication Type");
+ }
+
+ @Test
+ void ehloShouldAdvertiseOAUTHBEARERWhenConfigIsProvided() throws Exception
{
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("EHLO localhost");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(client.getReplyCode()).isEqualTo(250);
+ softly.assertThat(client.getReplyString())
+ .contains("250-AUTH OAUTHBEARER");
+ });
+ }
+
+ @Test
+ void ehloShouldAdvertiseXOAUTH2WhenConfigIsProvided() throws Exception {
+ SMTPSClient client = initSMTPSClient();
+
+ client.sendCommand("EHLO localhost");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(client.getReplyCode()).isEqualTo(250);
+ softly.assertThat(client.getReplyString())
+ .contains("XOAUTH2");
+ });
+ }
+
+ @Test
+ void ehloShouldNotAdvertiseOAUTHBEARERWhenConfigIsNotProvided() throws
Exception {
+ smtpServer.destroy();
+ HierarchicalConfiguration<ImmutableNode> config =
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml"));
+ smtpServer.configure(config);
+ smtpServer.init();
+
+ SMTPSClient client = initSMTPSClient();
+ client.sendCommand("EHLO localhost");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(client.getReplyCode()).isEqualTo(250);
+ softly.assertThat(client.getReplyString())
+ .doesNotContain("OAUTHBEARER");
+ });
+ }
+
+ @Test
+ void ehloShouldNotAdvertiseXOAUTH2WhenConfigIsNotProvided() throws
Exception {
+ smtpServer.destroy();
+ HierarchicalConfiguration<ImmutableNode> config =
ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("smtpserver-advancedSecurity.xml"));
+ smtpServer.configure(config);
+ smtpServer.init();
+
+ SMTPSClient client = initSMTPSClient();
+ client.sendCommand("EHLO localhost");
+
+ SoftAssertions.assertSoftly(softly -> {
+ softly.assertThat(client.getReplyCode()).isEqualTo(250);
+ softly.assertThat(client.getReplyString())
+ .doesNotContain("XOAUTH2");
+ });
+ }
+
+}
diff --git a/server/protocols/protocols-smtp/src/test/resources/keystore
b/server/protocols/protocols-smtp/src/test/resources/keystore
new file mode 100644
index 0000000..536a6c7
Binary files /dev/null and
b/server/protocols/protocols-smtp/src/test/resources/keystore differ
diff --git
a/server/protocols/protocols-smtp/src/test/resources/smtpserver-advancedSecurity.xml
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-advancedSecurity.xml
new file mode 100644
index 0000000..57f0cdd
--- /dev/null
+++
b/server/protocols/protocols-smtp/src/test/resources/smtpserver-advancedSecurity.xml
@@ -0,0 +1,20 @@
+<smtpserver enabled="true">
+ <bind>0.0.0.0:0</bind>
+ <tls socketTLS="false" startTLS="true">
+ <keystore>classpath://keystore</keystore>
+ <secret>james72laBalle</secret>
+ <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
+ <algorithm>SunX509</algorithm>
+ </tls>
+ <auth>
+ <announce>always</announce>
+ <requireSSL>true</requireSSL>
+ <plainAuthEnabled>false</plainAuthEnabled>
+ <!--OIDC path will be add dynamically in Tests-->
+ </auth>
+ <verifyIdentity>true</verifyIdentity>
+ <smtpGreeting>Apache JAMES awesome SMTP Server</smtpGreeting>
+ <handlerchain>
+ <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/>
+ </handlerchain>
+</smtpserver>
\ No newline at end of file
diff --git a/src/site/xdoc/server/config-smtp-lmtp.xml
b/src/site/xdoc/server/config-smtp-lmtp.xml
index 35b682b..0a41142 100644
--- a/src/site/xdoc/server/config-smtp-lmtp.xml
+++ b/src/site/xdoc/server/config-smtp-lmtp.xml
@@ -103,6 +103,14 @@
<dt><strong>auth.plainAuthEnabled</strong></dt>
<dd>This is an optional tag, defaults to true. If false, AUTH PLAIN
and AUTH LOGIN will not be exposed. This setting
can be used to enforce strong authentication mechanisms.</dd>
+ <dt><strong>auth.oidc.oidcConfigurationURL</strong></dt>
+ <dd>Provide OIDC url address for information to user. Only configure
this when you want to authenticate SMTP server using a OIDC provider.</dd>
+ <dt><strong>auth.oidc.jwksURL</strong></dt>
+ <dd>Provide url to get OIDC's JSON Web Key Set to validate user token.
Only configure this when you want to authenticate SMTP server using a OIDC
provider.</dd>
+ <dt><strong>auth.oidc.claim</strong></dt>
+ <dd>Claim string uses to identify user. E.g: "email_address". Only
configure this when you want to authenticate SMTP server using a OIDC
provider.</dd>
+ <dt><strong>auth.oidc.scope</strong></dt>
+ <dd>An OAuth scope that is valid to access the service (RF: RFC7628).
Only configure this when you want to authenticate SMTP server using a OIDC
provider.</dd>
<dt><strong>handler.authorizedAddresses</strong></dt>
<dd>Authorize specific addresses/networks.
If you use SMTP AUTH, addresses that match those specified here
will
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]