You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2022/01/07 07:24:34 UTC

[james-project] 05/05: JAMES-3680 AuthenticateProcessor - implement doSASL for IMAP

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

commit 82839949cdfcc89460cc1588cba3b7c776f217c1
Author: Tung Van TRAN <vt...@linagora.com>
AuthorDate: Thu Dec 16 14:56:54 2021 +0700

    JAMES-3680 AuthenticateProcessor - implement doSASL for IMAP
---
 pom.xml                                            |   6 +
 .../james/protocols/api/OidcSASLConfiguration.java |  77 +++++++++++
 .../apache/james/protocols/api/OIDCSASLHelper.java |  35 ++---
 protocols/imap/pom.xml                             |   4 +
 .../apache/james/imap/api/process/ImapSession.java |   5 +
 .../apache/james/imap/encode/FakeImapSession.java  |  12 ++
 .../imap/processor/AbstractAuthProcessor.java      |   6 +
 .../imap/processor/AuthenticateProcessor.java      |  66 +++++++--
 .../imap/processor/DefaultProcessorChain.java      |   2 +-
 ...KeyProvider.java => JwksPublicKeyProvider.java} |  14 +-
 .../org/apache/james/jwt/OidcJwtTokenVerifier.java |  31 ++++-
 ...derTest.java => JwksPublicKeyProviderTest.java} |  10 +-
 server/protocols/protocols-imap4/pom.xml           |  11 ++
 .../apache/james/imapserver/netty/IMAPServer.java  |  39 +++++-
 .../netty/ImapChannelUpstreamHandler.java          |   3 +-
 .../james/imapserver/netty/NettyImapSession.java   |  19 ++-
 .../james/imapserver/netty/IMAPServerTest.java     | 148 ++++++++++++++++++++-
 .../protocols-imap4/src/test/resources/oauth.xml   |  13 ++
 18 files changed, 441 insertions(+), 60 deletions(-)

diff --git a/pom.xml b/pom.xml
index 744d7cd..fcd136a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1573,6 +1573,12 @@
             </dependency>
             <dependency>
                 <groupId>${james.groupId}</groupId>
+                <artifactId>james-server-jwt</artifactId>
+                <version>${project.version}</version>
+                <type>test-jar</type>
+            </dependency>
+            <dependency>
+                <groupId>${james.groupId}</groupId>
                 <artifactId>james-server-lifecycle-api</artifactId>
                 <version>${project.version}</version>
             </dependency>
diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java b/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java
new file mode 100644
index 0000000..313bca2
--- /dev/null
+++ b/protocols/api/src/main/java/org/apache/james/protocols/api/OidcSASLConfiguration.java
@@ -0,0 +1,77 @@
+/****************************************************************
+ * 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.api;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+
+import com.google.common.base.Preconditions;
+
+public class OidcSASLConfiguration {
+
+    public static OidcSASLConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws MalformedURLException {
+        String jwksURL = configuration.getString("jwksURL", null);
+        String claim = configuration.getString("claim", null);
+        String oidcConfigurationURL = configuration.getString("oidcConfigurationURL", null);
+        String scope = configuration.getString("scope", null);
+
+        Preconditions.checkNotNull(jwksURL, "`jwksURL` property need to be specified inside the oidc tag");
+        Preconditions.checkNotNull(claim, "`claim` property need to be specified inside the oidc tag");
+        Preconditions.checkNotNull(oidcConfigurationURL, "`oidcConfigurationURL` property need to be specified inside the oidc tag");
+        Preconditions.checkNotNull(scope, "`scope` property need to be specified inside the oidc tag");
+
+        return new OidcSASLConfiguration(jwksURL, claim, oidcConfigurationURL, scope);
+    }
+
+    private final URL jwksURL;
+    private final String claim;
+    private final URL oidcConfigurationURL;
+    private final String scope;
+
+    public OidcSASLConfiguration(URL jwksURL, String claim, URL oidcConfigurationURL, String scope) {
+        this.jwksURL = jwksURL;
+        this.claim = claim;
+        this.oidcConfigurationURL = oidcConfigurationURL;
+        this.scope = scope;
+    }
+
+    public OidcSASLConfiguration(String jwksURL, String claim, String oidcConfigurationURL, String scope) throws MalformedURLException {
+        this(new URL(jwksURL), claim, new URL(oidcConfigurationURL), scope);
+    }
+
+    public URL getJwksURL() {
+        return jwksURL;
+    }
+
+    public String getClaim() {
+        return claim;
+    }
+
+    public URL getOidcConfigurationURL() {
+        return oidcConfigurationURL;
+    }
+
+    public String getScope() {
+        return scope;
+    }
+}
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenHelper.java b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
similarity index 52%
rename from server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenHelper.java
rename to protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
index a97ad09..1436649 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwtTokenHelper.java
+++ b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
@@ -17,34 +17,17 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jwt;
+package org.apache.james.protocols.api;
 
-import java.util.Optional;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
 
-import io.jsonwebtoken.Claims;
-import io.jsonwebtoken.Header;
-import io.jsonwebtoken.Jwt;
-import io.jsonwebtoken.JwtException;
-import io.jsonwebtoken.Jwts;
-import io.jsonwebtoken.MalformedJwtException;
+import com.google.common.collect.ImmutableList;
 
-public class JwtTokenHelper {
-
-    public static  <T> Optional<T> getClaimWithoutSignatureVerification(String token, String claimName, Class<T> returnType) {
-        int signatureIndex = token.lastIndexOf('.');
-        if (signatureIndex <= 0) {
-            return Optional.empty();
-        }
-        String nonSignedToken = token.substring(0, signatureIndex + 1);
-        try {
-            Jwt<Header, Claims> headerClaims = Jwts.parser().parseClaimsJwt(nonSignedToken);
-            T claim = (T) headerClaims.getHeader().get(claimName);
-            if (claim == null) {
-                throw new MalformedJwtException("'" + claimName + "' field in token is mandatory");
-            }
-            return Optional.of(claim);
-        } catch (JwtException e) {
-            return Optional.empty();
-        }
+public class OIDCSASLHelper {
+    public static String generateOauthBearer(String username, String token) {
+        return Base64.getEncoder().encodeToString(String.join("" + OIDCSASLParser.SASL_SEPARATOR,
+                ImmutableList.of("user=" + username, "auth=Bearer " + token, "", ""))
+            .getBytes(StandardCharsets.US_ASCII));
     }
 }
diff --git a/protocols/imap/pom.xml b/protocols/imap/pom.xml
index d462ac7..9642012 100644
--- a/protocols/imap/pom.xml
+++ b/protocols/imap/pom.xml
@@ -62,6 +62,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-jwt</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-util</artifactId>
         </dependency>
         <dependency>
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java
index 31293bc..6c3ac4a 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/api/process/ImapSession.java
@@ -27,6 +27,7 @@ import org.apache.james.core.Username;
 import org.apache.james.imap.api.ImapSessionState;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.protocols.api.CommandDetectionSession;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 /**
  * Encapsulates all state held for an ongoing Imap session, which commences when
@@ -219,6 +220,10 @@ public interface ImapSession extends CommandDetectionSession {
      */
     boolean isPlainAuthEnabled();
 
+    boolean supportsOAuth();
+
+    Optional<OidcSASLConfiguration> oidcSaslConfiguration();
+
     default void setMailboxSession(MailboxSession mailboxSession) {
         setAttribute(MAILBOX_SESSION_ATTRIBUTE_SESSION_KEY, mailboxSession);
     }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java b/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java
index 83c2c67..153f615 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/encode/FakeImapSession.java
@@ -19,12 +19,14 @@
 package org.apache.james.imap.encode;
 
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.james.imap.api.ImapSessionState;
 import org.apache.james.imap.api.process.ImapLineHandler;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.api.process.SelectedMailbox;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 public class FakeImapSession implements ImapSession {
 
@@ -155,6 +157,16 @@ public class FakeImapSession implements ImapSession {
     }
 
     @Override
+    public boolean supportsOAuth() {
+        return false;
+    }
+
+    @Override
+    public Optional<OidcSASLConfiguration> oidcSaslConfiguration() {
+        return Optional.empty();
+    }
+
+    @Override
     public boolean isTLSActive() {
         return false;
     }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
index f8af8a1..cf4deec 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AbstractAuthProcessor.java
@@ -166,6 +166,12 @@ public abstract class AbstractAuthProcessor<R extends ImapRequest> extends Abstr
         return new AuthenticationAttempt(Optional.empty(), authenticationId, password);
     }
 
+    protected void authSuccess(Username username, ImapSession session, ImapRequest request, Responder responder) {
+        session.authenticated();
+        session.setMailboxSession(getMailboxManager().createSystemSession(username));
+        okComplete(request, responder);
+    }
+
     protected static class AuthenticationAttempt {
         private final Optional<Username> delegateUserName;
         private final Username authenticationId;
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java
index 6ce9764..c5303c7 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/AuthenticateProcessor.java
@@ -24,8 +24,12 @@ import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Base64;
 import java.util.List;
+import java.util.Optional;
 import java.util.StringTokenizer;
 
+import javax.mail.internet.AddressException;
+
+import org.apache.james.core.MailAddress;
 import org.apache.james.core.Username;
 import org.apache.james.imap.api.display.HumanReadableText;
 import org.apache.james.imap.api.message.Capability;
@@ -36,8 +40,10 @@ import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.message.request.AuthenticateRequest;
 import org.apache.james.imap.message.request.IRAuthenticateRequest;
 import org.apache.james.imap.message.response.AuthenticateResponse;
+import org.apache.james.jwt.OidcJwtTokenVerifier;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.protocols.api.OIDCSASLParser;
 import org.apache.james.util.MDCBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -49,17 +55,21 @@ import com.google.common.collect.ImmutableList;
  */
 public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateRequest> implements CapabilityImplementingProcessor {
     private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticateProcessor.class);
-    private static final String PLAIN = "PLAIN";
-    
+    private static final String AUTH_TYPE_PLAIN = "PLAIN";
+    private static final String AUTH_TYPE_OAUTHBEARER = "OAUTHBEARER";
+    private static final String AUTH_TYPE_XOAUTH2 = "XOAUTH2";
+    private static final List<Capability> OAUTH_CAPABILITIES = ImmutableList.of(Capability.of("AUTH=" + AUTH_TYPE_OAUTHBEARER), Capability.of("AUTH=" + AUTH_TYPE_XOAUTH2));
+
     public AuthenticateProcessor(ImapProcessor next, MailboxManager mailboxManager, StatusResponseFactory factory,
-            MetricFactory metricFactory) {
+                                 MetricFactory metricFactory) {
         super(AuthenticateRequest.class, next, mailboxManager, factory, metricFactory);
     }
 
     @Override
     protected void processRequest(AuthenticateRequest request, ImapSession session, final Responder responder) {
         final String authType = request.getAuthType();
-        if (authType.equalsIgnoreCase(PLAIN)) {
+
+        if (authType.equalsIgnoreCase(AUTH_TYPE_PLAIN)) {
             // See if AUTH=PLAIN is allowed. See IMAP-304
             if (session.isPlainAuthDisallowed()) {
                 no(request, responder, HumanReadableText.DISABLED_LOGIN);
@@ -70,16 +80,23 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq
                 } else {
                     responder.respond(new AuthenticateResponse());
                     session.pushLineHandler((requestSession, data) -> {
-                        // cut of the CRLF
-                        String initialClientResponse = new String(data, 0, data.length - 2, StandardCharsets.US_ASCII);
-
-                        doPlainAuth(initialClientResponse, requestSession, request, responder);
-
+                        doPlainAuth(extractInitialClientResponse(data), requestSession, request, responder);
                         // remove the handler now
                         requestSession.popLineHandler();
                     });
                 }
             }
+        } else if (authType.equalsIgnoreCase(AUTH_TYPE_OAUTHBEARER) || authType.equalsIgnoreCase(AUTH_TYPE_XOAUTH2)) {
+            if (request instanceof IRAuthenticateRequest) {
+                IRAuthenticateRequest irRequest = (IRAuthenticateRequest) request;
+                doOAuth(irRequest.getInitialClientResponse(), session, request, responder);
+            } else {
+                responder.respond(new AuthenticateResponse());
+                session.pushLineHandler((requestSession, data) -> {
+                    doOAuth(extractInitialClientResponse(data), requestSession, request, responder);
+                    requestSession.popLineHandler();
+                });
+            }
         } else {
             LOGGER.debug("Unsupported authentication mechanism '{}'", authType);
             no(request, responder, HumanReadableText.UNSUPPORTED_AUTHENTICATION_MECHANISM);
@@ -145,6 +162,9 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq
         }
         // Support for SASL-IR. See RFC4959
         caps.add(Capability.of("SASL-IR"));
+        if (session.supportsOAuth()) {
+            caps.addAll(OAUTH_CAPABILITIES);
+        }
         return ImmutableList.copyOf(caps);
     }
 
@@ -155,4 +175,32 @@ public class AuthenticateProcessor extends AbstractAuthProcessor<AuthenticateReq
             .addToContext("authType", request.getAuthType())
             .build();
     }
+
+    private void doOAuth(String initialResponse, ImapSession session, ImapRequest request, Responder responder) {
+        if (!session.supportsOAuth()) {
+            no(request, responder, HumanReadableText.UNSUPPORTED_AUTHENTICATION_MECHANISM);
+        } else {
+            OIDCSASLParser.parse(initialResponse)
+                .flatMap(oidcInitialResponseValue -> session.oidcSaslConfiguration()
+                    .flatMap(configuration -> new OidcJwtTokenVerifier().verifyAndExtractClaim(oidcInitialResponseValue.getToken(), configuration.getJwksURL(), configuration.getClaim())))
+                .flatMap(this::extractUserFromClaim)
+                .ifPresentOrElse(username -> authSuccess(username, session, request, responder),
+                    () -> manageFailureCount(session, request, responder, HumanReadableText.AUTHENTICATION_FAILED));
+        }
+        session.stopDetectingCommandInjection();
+    }
+
+    private Optional<Username> extractUserFromClaim(String claimValue) {
+        try {
+            return Optional.of(Username.fromMailAddress(new MailAddress(claimValue)));
+        } catch (AddressException e) {
+            return Optional.empty();
+        }
+    }
+
+    private static String extractInitialClientResponse(byte[] data) {
+        // cut of the CRLF
+        return new String(data, 0, data.length - 2, StandardCharsets.US_ASCII);
+    }
+
 }
diff --git a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessorChain.java b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessorChain.java
index 41a2422..9ad9ea6 100644
--- a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessorChain.java
+++ b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessorChain.java
@@ -130,7 +130,7 @@ public class DefaultProcessorChain {
         // announce the COMPRESS extension. Sew RFC4978
         capabilityProcessor.addProcessor(compressProcessor);
         
-        // add to announnce AUTH=PLAIN
+        // add to announnce AUTH=PLAIN, AUTH=OAUTHBEARER
         capabilityProcessor.addProcessor(authenticateProcessor);
 
         // add to announnce ENABLE
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwksPublicKeyProvider.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwksPublicKeyProvider.java
similarity index 87%
rename from server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwksPublicKeyProvider.java
rename to server/protocols/jwt/src/main/java/org/apache/james/jwt/JwksPublicKeyProvider.java
index 5d0d59a..0074981 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwksPublicKeyProvider.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/JwksPublicKeyProvider.java
@@ -35,21 +35,21 @@ import com.github.fge.lambdas.Throwing;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
-public class OidcJwksPublicKeyProvider implements PublicKeyProvider {
-    private static final Logger LOGGER = LoggerFactory.getLogger(OidcJwksPublicKeyProvider.class);
+public class JwksPublicKeyProvider implements PublicKeyProvider {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JwksPublicKeyProvider.class);
 
-    public static OidcJwksPublicKeyProvider of(URL jwksURL, String kid) {
-        return new OidcJwksPublicKeyProvider(jwksURL, Optional.of(kid));
+    public static JwksPublicKeyProvider of(URL jwksURL, String kid) {
+        return new JwksPublicKeyProvider(jwksURL, Optional.of(kid));
     }
 
-    public static OidcJwksPublicKeyProvider of(URL jwksURL) {
-        return new OidcJwksPublicKeyProvider(jwksURL, Optional.empty());
+    public static JwksPublicKeyProvider of(URL jwksURL) {
+        return new JwksPublicKeyProvider(jwksURL, Optional.empty());
     }
 
     private final UrlJwkProvider jwkProvider;
     private final Optional<String> kid;
 
-    private OidcJwksPublicKeyProvider(URL jwksURL, Optional<String> kid) {
+    private JwksPublicKeyProvider(URL jwksURL, Optional<String> kid) {
         Preconditions.checkNotNull(jwksURL);
         Preconditions.checkNotNull(kid);
         this.jwkProvider = new UrlJwkProvider(jwksURL);
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
index b1a1923..c3d4541 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcJwtTokenVerifier.java
@@ -22,12 +22,37 @@ package org.apache.james.jwt;
 import java.net.URL;
 import java.util.Optional;
 
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Header;
+import io.jsonwebtoken.Jwt;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+
 public class OidcJwtTokenVerifier {
 
     public Optional<String> verifyAndExtractClaim(String jwtToken, URL jwksURL, String claimName) {
-        PublicKeyProvider jwksPublicKeyProvider = JwtTokenHelper.getClaimWithoutSignatureVerification(jwtToken, "kid", String.class)
-            .map(kidValue -> OidcJwksPublicKeyProvider.of(jwksURL, kidValue))
-            .orElse(OidcJwksPublicKeyProvider.of(jwksURL));
+        PublicKeyProvider jwksPublicKeyProvider = getClaimWithoutSignatureVerification(jwtToken, "kid", String.class)
+            .map(kidValue -> JwksPublicKeyProvider.of(jwksURL, kidValue))
+            .orElse(JwksPublicKeyProvider.of(jwksURL));
         return new JwtTokenVerifier(jwksPublicKeyProvider).verifyAndExtractClaim(jwtToken, claimName, String.class);
     }
+
+    public static <T> Optional<T> getClaimWithoutSignatureVerification(String token, String claimName, Class<T> returnType) {
+        int signatureIndex = token.lastIndexOf('.');
+        if (signatureIndex <= 0) {
+            return Optional.empty();
+        }
+        String nonSignedToken = token.substring(0, signatureIndex + 1);
+        try {
+            Jwt<Header, Claims> headerClaims = Jwts.parser().parseClaimsJwt(nonSignedToken);
+            T claim = (T) headerClaims.getHeader().get(claimName);
+            if (claim == null) {
+                throw new MalformedJwtException("'" + claimName + "' field in token is mandatory");
+            }
+            return Optional.of(claim);
+        } catch (JwtException e) {
+            return Optional.empty();
+        }
+    }
 }
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwksPublicKeyProviderTest.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
similarity index 93%
rename from server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwksPublicKeyProviderTest.java
rename to server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
index 6c6d613..4d1033b 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcJwksPublicKeyProviderTest.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/JwksPublicKeyProviderTest.java
@@ -36,7 +36,7 @@ import org.mockserver.integration.ClientAndServer;
 import org.mockserver.model.HttpRequest;
 import org.mockserver.model.HttpResponse;
 
-class OidcJwksPublicKeyProviderTest {
+class JwksPublicKeyProviderTest {
     private static final String JWKS_URI_PATH = "/auth/realms/realm1/protocol/openid-connect/certs";
     private static final String JWKS_JSON = "{" +
         "    \"keys\": [" +
@@ -91,7 +91,7 @@ class OidcJwksPublicKeyProviderTest {
 
     @Test
     void getShouldSuccessWhenKeyProvided() {
-        PublicKeyProvider testee = OidcJwksPublicKeyProvider.of(getJwksURL(), "wu-9VZEr0gHF986PYPVzvU-5IP1q26EzzQVK_sjG29Q");
+        PublicKeyProvider testee = JwksPublicKeyProvider.of(getJwksURL(), "wu-9VZEr0gHF986PYPVzvU-5IP1q26EzzQVK_sjG29Q");
         List<PublicKey> publicKeys = testee.get();
         SoftAssertions.assertSoftly(softly -> {
             softly.assertThat(publicKeys).hasSize(1);
@@ -101,7 +101,7 @@ class OidcJwksPublicKeyProviderTest {
 
     @Test
     void getShouldReturnEmptyWhenKeyNotProvided() {
-        PublicKeyProvider testee = OidcJwksPublicKeyProvider.of(getJwksURL(), "notfound");
+        PublicKeyProvider testee = JwksPublicKeyProvider.of(getJwksURL(), "notfound");
         assertThat(testee.get()).isEmpty();
     }
 
@@ -112,7 +112,7 @@ class OidcJwksPublicKeyProviderTest {
             .respond(HttpResponse.response().withStatusCode(200)
                 .withBody("invalid body", StandardCharsets.UTF_8));
 
-        PublicKeyProvider testee = OidcJwksPublicKeyProvider.of(new URL(String.format("http://127.0.0.1:%s/invalid", mockServer.getLocalPort())),
+        PublicKeyProvider testee = JwksPublicKeyProvider.of(new URL(String.format("http://127.0.0.1:%s/invalid", mockServer.getLocalPort())),
             "wu-9VZEr0gHF986PYPVzvU-5IP1q26EzzQVK_sjG29Q");
         assertThatThrownBy(testee::get)
             .isInstanceOf(MissingOrInvalidKeyException.class);
@@ -120,7 +120,7 @@ class OidcJwksPublicKeyProviderTest {
 
     @Test
     void getShouldReturnAllPublicKeyWhenKidNoProvided() {
-        PublicKeyProvider testee = OidcJwksPublicKeyProvider.of(getJwksURL());
+        PublicKeyProvider testee = JwksPublicKeyProvider.of(getJwksURL());
         assertThat(testee.get())
             .hasSize(2);
     }
diff --git a/server/protocols/protocols-imap4/pom.xml b/server/protocols/protocols-imap4/pom.xml
index 851cb44..0308f96 100644
--- a/server/protocols/protocols-imap4/pom.xml
+++ b/server/protocols/protocols-imap4/pom.xml
@@ -67,6 +67,12 @@
         </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-protocols-library</artifactId>
         </dependency>
         <dependency>
@@ -134,6 +140,11 @@
             <artifactId>commons-configuration2</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-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
index 09feae4..e3cc52e 100644
--- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
+++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/IMAPServer.java
@@ -20,6 +20,7 @@ package org.apache.james.imapserver.netty;
 
 import static org.jboss.netty.channel.Channels.pipeline;
 
+import java.net.MalformedURLException;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
@@ -34,6 +35,7 @@ import org.apache.james.imap.api.process.ImapProcessor;
 import org.apache.james.imap.decode.ImapDecoder;
 import org.apache.james.imap.encode.ImapEncoder;
 import org.apache.james.protocols.api.Encryption;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.netty.ChannelGroupHandler;
 import org.apache.james.protocols.netty.ChannelHandlerFactory;
@@ -65,11 +67,26 @@ public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapC
     public static class AuthenticationConfiguration {
         private static final boolean PLAIN_AUTH_DISALLOWED_DEFAULT = true;
         private static final boolean PLAIN_AUTH_ENABLED_DEFAULT = true;
-
-        public static AuthenticationConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) {
-            return new AuthenticationConfiguration(
-                configuration.getBoolean("auth.requireSSL", fallback(configuration)),
-                configuration.getBoolean("auth.plainAuthEnabled", PLAIN_AUTH_ENABLED_DEFAULT));
+        private static final String OAUTH_PATH = "auth.oauth";
+
+        public static AuthenticationConfiguration parse(HierarchicalConfiguration<ImmutableNode> configuration) throws ConfigurationException {
+            boolean isRequireSSL = configuration.getBoolean("auth.requireSSL", fallback(configuration));
+            boolean isPlainAuthEnabled = configuration.getBoolean("auth.plainAuthEnabled", PLAIN_AUTH_ENABLED_DEFAULT);
+
+            if (configuration.immutableConfigurationsAt(OAUTH_PATH).isEmpty()) {
+                return new AuthenticationConfiguration(
+                    isRequireSSL,
+                    isPlainAuthEnabled);
+            } else {
+                try {
+                    return new AuthenticationConfiguration(
+                        isRequireSSL,
+                        isPlainAuthEnabled,
+                        OidcSASLConfiguration.parse(configuration.configurationAt(OAUTH_PATH)));
+                } catch (MalformedURLException | NullPointerException exception) {
+                    throw new ConfigurationException("Failed to retrieve oauth component", exception);
+                }
+            }
         }
 
         private static boolean fallback(HierarchicalConfiguration<ImmutableNode> configuration) {
@@ -78,10 +95,18 @@ public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapC
 
         private final boolean isSSLRequired;
         private final boolean plainAuthEnabled;
+        private final Optional<OidcSASLConfiguration> oidcSASLConfiguration;
 
         public AuthenticationConfiguration(boolean isSSLRequired, boolean plainAuthEnabled) {
             this.isSSLRequired = isSSLRequired;
             this.plainAuthEnabled = plainAuthEnabled;
+            this.oidcSASLConfiguration = Optional.empty();
+        }
+
+        public AuthenticationConfiguration(boolean isSSLRequired, boolean plainAuthEnabled, OidcSASLConfiguration oidcSASLConfiguration) {
+            this.isSSLRequired = isSSLRequired;
+            this.plainAuthEnabled = plainAuthEnabled;
+            this.oidcSASLConfiguration = Optional.of(oidcSASLConfiguration);
         }
 
         public boolean isSSLRequired() {
@@ -91,6 +116,10 @@ public class IMAPServer extends AbstractConfigurableAsyncServer implements ImapC
         public boolean isPlainAuthEnabled() {
             return plainAuthEnabled;
         }
+
+        public Optional<OidcSASLConfiguration> getOidcSASLConfiguration() {
+            return oidcSASLConfiguration;
+        }
     }
 
     private static final String softwaretype = "JAMES " + VERSION + " Server ";
diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
index 65949ab..1f87867 100644
--- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
+++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/ImapChannelUpstreamHandler.java
@@ -145,7 +145,8 @@ public class ImapChannelUpstreamHandler extends SimpleChannelUpstreamHandler imp
     @Override
     public void channelBound(final ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
         ImapSession imapsession = new NettyImapSession(ctx.getChannel(), secure, compress, authenticationConfiguration.isSSLRequired(),
-            authenticationConfiguration.isPlainAuthEnabled(), SessionId.generate());
+            authenticationConfiguration.isPlainAuthEnabled(), SessionId.generate(),
+            authenticationConfiguration.getOidcSASLConfiguration());
         MDCBuilder boundMDC = IMAPMDCContext.boundMDC(ctx);
         imapsession.setAttribute(MDC_KEY, boundMDC);
         attributes.set(ctx.getChannel(), imapsession);
diff --git a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java
index 0148a6c..b582066 100644
--- a/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java
+++ b/server/protocols/protocols-imap4/src/main/java/org/apache/james/imapserver/netty/NettyImapSession.java
@@ -20,12 +20,14 @@ package org.apache.james.imapserver.netty;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Optional;
 
 import org.apache.james.imap.api.ImapSessionState;
 import org.apache.james.imap.api.process.ImapLineHandler;
 import org.apache.james.imap.api.process.ImapSession;
 import org.apache.james.imap.api.process.SelectedMailbox;
 import org.apache.james.protocols.api.Encryption;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.jboss.netty.channel.Channel;
 import org.jboss.netty.handler.codec.compression.ZlibDecoder;
 import org.jboss.netty.handler.codec.compression.ZlibEncoder;
@@ -44,8 +46,11 @@ public class NettyImapSession implements ImapSession, NettyConstants {
     private final boolean plainAuthEnabled;
     private final SessionId sessionId;
     private boolean needsCommandInjectionDetection;
+    private Optional<OidcSASLConfiguration> oidcSASLConfiguration;
+    private boolean supportsOAuth;
 
-    public NettyImapSession(Channel channel, Encryption secure, boolean compress, boolean requiredSSL, boolean plainAuthEnabled, SessionId sessionId) {
+    public NettyImapSession(Channel channel, Encryption secure, boolean compress, boolean requiredSSL, boolean plainAuthEnabled, SessionId sessionId,
+                            Optional<OidcSASLConfiguration> oidcSASLConfiguration) {
         this.channel = channel;
         this.secure = secure;
         this.compress = compress;
@@ -53,6 +58,8 @@ public class NettyImapSession implements ImapSession, NettyConstants {
         this.plainAuthEnabled = plainAuthEnabled;
         this.sessionId = sessionId;
         this.needsCommandInjectionDetection = true;
+        this.oidcSASLConfiguration = oidcSASLConfiguration;
+        this.supportsOAuth = oidcSASLConfiguration.isPresent();
     }
 
     @Override
@@ -208,6 +215,16 @@ public class NettyImapSession implements ImapSession, NettyConstants {
     }
 
     @Override
+    public boolean supportsOAuth() {
+        return supportsOAuth;
+    }
+
+    @Override
+    public Optional<OidcSASLConfiguration> oidcSaslConfiguration() {
+        return oidcSASLConfiguration;
+    }
+
+    @Override
     public boolean isTLSActive() {
         return channel.getPipeline().get(SSL_HANDLER) != null;
     }
diff --git a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
index abddd7c..5fa8cb6 100644
--- a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
+++ b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
@@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import java.io.EOFException;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Properties;
 
 import javax.mail.Folder;
@@ -39,13 +40,17 @@ import javax.mail.search.RecipientStringTerm;
 import javax.mail.search.SearchTerm;
 import javax.mail.search.SubjectTerm;
 
+import org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.ex.ConfigurationException;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.commons.net.imap.AuthenticatingIMAPClient;
 import org.apache.commons.net.imap.IMAPReply;
 import org.apache.commons.net.imap.IMAPSClient;
 import org.apache.james.core.Username;
 import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
 import org.apache.james.imap.main.DefaultImapDecoderFactory;
 import org.apache.james.imap.processor.main.DefaultImapProcessorFactory;
+import org.apache.james.jwt.OidcTokenFixture;
 import org.apache.james.mailbox.MailboxSession;
 import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
@@ -55,6 +60,7 @@ import org.apache.james.mailbox.store.FakeAuthenticator;
 import org.apache.james.mailbox.store.FakeAuthorizator;
 import org.apache.james.mailbox.store.StoreSubscriptionManager;
 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.lib.mock.ConfigLoader;
@@ -69,6 +75,9 @@ import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.RepeatedTest;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.HttpResponse;
 
 import nl.altindag.ssl.exception.GenericKeyStoreException;
 import nl.altindag.ssl.exception.PrivateKeyParseException;
@@ -85,7 +94,7 @@ class IMAPServerTest {
     @RegisterExtension
     public TestIMAPClient testIMAPClient = new TestIMAPClient();
 
-    private IMAPServer createImapServer(String configurationFile) throws Exception {
+    private IMAPServer createImapServer(HierarchicalConfiguration<ImmutableNode> config) throws Exception {
         FakeAuthenticator authenticator = new FakeAuthenticator();
         authenticator.addUser(USER, USER_PASS);
         authenticator.addUser(USER2, USER_PASS);
@@ -121,12 +130,16 @@ class IMAPServerTest {
         FileSystemImpl fileSystem = new FileSystemImpl(configuration.directories());
         imapServer.setFileSystem(fileSystem);
 
-        imapServer.configure(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)));
+        imapServer.configure(config);
         imapServer.init();
 
         return imapServer;
     }
 
+    private IMAPServer createImapServer(String configurationFile) throws Exception {
+        return createImapServer(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)));
+    }
+
     @Nested
     class NoLimit {
         IMAPServer imapServer;
@@ -616,6 +629,137 @@ class IMAPServerTest {
     }
 
     @Nested
+    class Oauth {
+        String JWKS_URI_PATH = "/jwks";
+        ClientAndServer authServer;
+        IMAPServer imapServer;
+        int port;
+
+        @BeforeEach
+        void authSetup() throws Exception {
+            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("oauth.xml"));
+            config.addProperty("auth.oauth.jwksURL", String.format("http://127.0.0.1:%s%s", authServer.getLocalPort(), JWKS_URI_PATH));
+            config.addProperty("auth.oauth.claim", OidcTokenFixture.CLAIM);
+            config.addProperty("auth.oauth.oidcConfigurationURL", "https://example.com/jwks");
+            config.addProperty("auth.oauth.scope", "email");
+            imapServer = createImapServer(config);
+            port = imapServer.getListenAddresses().get(0).getPort();
+        }
+
+        @AfterEach
+        void tearDown() {
+            if (imapServer != null) {
+                imapServer.destroy();
+            }
+            authServer.stop();
+        }
+
+        @Test
+        void oauthShouldSuccessWhenValidToken() throws Exception {
+            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            IMAPSClient client = imapsClient(port);
+            client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
+            assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
+        }
+
+        @Test
+        void oauthShouldFailWhenInValidToken() throws Exception {
+            IMAPSClient client = imapsClient(port);
+            client.sendCommand("AUTHENTICATE OAUTHBEARER invalidtoken");
+            assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed.");
+        }
+
+        @Test
+        void oauthShouldFailWhenConfigIsNotProvided() throws Exception {
+            imapServer.destroy();
+            HierarchicalConfiguration<ImmutableNode> config = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("imapServerRequireSSLIsTrueAndStartSSLIsTrue.xml"));
+            imapServer = createImapServer(config);
+            int port = imapServer.getListenAddresses().get(0).getPort();
+
+            IMAPSClient client = imapsClient(port);
+            client.sendCommand("AUTHENTICATE OAUTHBEARER " + OidcTokenFixture.VALID_TOKEN);
+            assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed. Authentication mechanism is unsupported.");
+        }
+
+        @Test
+        void capabilityShouldAdvertiseOAUTHBEARERWhenConfigIsProvided() throws Exception {
+            IMAPSClient client = imapsClient(port);
+            client.capability();
+            assertThat(client.getReplyString()).contains("AUTH=OAUTHBEARER");
+        }
+
+        @Test
+        void capabilityShouldAdvertiseXOAUTH2WhenConfigIsProvided() throws Exception {
+            IMAPSClient client = imapsClient(port);
+            client.capability();
+            assertThat(client.getReplyString()).contains("AUTH=XOAUTH2");
+        }
+
+        @Test
+        void oauthShouldSupportOAUTH2Type() throws Exception {
+            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            IMAPSClient client = imapsClient(port);
+            client.sendCommand("AUTHENTICATE XOAUTH2 " + oauthBearer);
+            assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
+        }
+
+        @Test
+        void capabilityShouldNotAdvertiseOAUTHBEARERWhenConfigIsNotProvided() throws Exception {
+            imapServer.destroy();
+            HierarchicalConfiguration<ImmutableNode> config = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("imapServerRequireSSLIsTrueAndStartSSLIsTrue.xml"));
+            imapServer = createImapServer(config);
+            int port = imapServer.getListenAddresses().get(0).getPort();
+
+            IMAPSClient client = imapsClient(port);
+            client.capability();
+            assertThat(client.getReplyString()).doesNotContain("AUTH=OAUTHBEARER");
+            assertThat(client.getReplyString()).doesNotContain("AUTH=XOAUTH2");
+        }
+
+        @Test
+        void shouldNotOauthWhenAuthIsReady() throws Exception {
+            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            IMAPSClient client = imapsClient(port);
+            client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
+            client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
+            assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed. Command not valid in this state.");
+        }
+
+        @Test
+        void appendShouldSuccessWhenAuthenticated() throws Exception {
+            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            IMAPSClient imapsClient = imapsClient(port);
+            imapsClient.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
+            imapsClient.create("INBOX");
+            imapsClient.append("INBOX", null, null, SMALL_MESSAGE);
+
+            assertThat(imapsClient.getReplyString()).contains("APPEND completed.");
+        }
+
+        @Test
+        void appendShouldFailWhenNotAuthenticated() throws Exception {
+            IMAPSClient imapsClient = imapsClient(port);
+            imapsClient.create("INBOX");
+            assertThat(imapsClient.getReplyString()).contains("Command not valid in this state.");
+        }
+    }
+
+    private AuthenticatingIMAPClient imapsClient(int port) throws Exception {
+        AuthenticatingIMAPClient client = new AuthenticatingIMAPClient(false, BogusSslContextFactory.getClientContext());
+        client.setTrustManager(BogusTrustManagerFactory.getTrustManagers()[0]);
+        client.connect("127.0.0.1", port);
+        client.execTLS();
+        return client;
+    }
+
+    @Nested
     class Search {
         IMAPServer imapServer;
         private int port;
diff --git a/server/protocols/protocols-imap4/src/test/resources/oauth.xml b/server/protocols/protocols-imap4/src/test/resources/oauth.xml
new file mode 100644
index 0000000..50a720a
--- /dev/null
+++ b/server/protocols/protocols-imap4/src/test/resources/oauth.xml
@@ -0,0 +1,13 @@
+<imapserver enabled="true">
+    <jmxName>imapserver</jmxName>
+    <bind>0.0.0.0:9993</bind>
+    <tls socketTLS="false" startTLS="true">
+        <privateKey>private.key</privateKey>
+        <certificates>certs.self-signed.csr</certificates>
+        <secret>123456</secret>
+    </tls>
+    <auth>
+        <plainAuthEnabled>true</plainAuthEnabled>
+        <requireSSL>true</requireSSL>
+    </auth>
+</imapserver>
\ No newline at end of file

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org