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 2026/01/19 16:18:14 UTC

(james-project) branch master updated (aef61d6fc9 -> 67f879c597)

This is an automated email from the ASF dual-hosted git repository.

btellier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


    from aef61d6fc9 JAMES-4164 VacationMailet: add support for replyMode
     new 6512a48048 feat(managesieve): add XOAUTH2 authentication mechanism
     new 06445aef14 fix(oidc): correct differentiation between XOAUTH2 and OAUTHBEARER
     new ebbc90ae00 feat(managesieve): add OAUTHBEARER authentication mechanism
     new 82fa8fce18 fix(managesieve): small fixes in authentication logic
     new 0c8b5d20d4 test(managesieve): test authentication of managesieve server
     new b946db9fdd docs(managesieve): document usage of oidc authentication
     new 0e78452ba3 fix(examples): fix introspection mismatching issuer error
     new 6b35fedb6a feature(examples): configure managesieve and test oidc authentication in oidc example
     new 24edc560f1 refactor(examples): small improvements for oidc example
     new 1cd3f9547e refactor: add more errors and reset authentication state on authentication error
     new a8950829cf fix(managesieve): Implement continuation as described in RFC-4616
     new c6bd40ff0b fix(oidc): wrong format of gs2-header
     new 8b9bb31cf3 refactor(managesieve): accept plain authentication without leading null byte
     new 79c6c8ebb2 feat(managesieve): add test for old SASL PLAIN parsing behavior
     new ddc1be1b8d refactor(managesieve): only accept unquoted initial client response during continuation
     new 5c2ab8502e feat(managesieve): add additional test for usage of spaces as delimiters in SASL PLAIN
     new 67f879c597 refactor(managesieve): rebase on new oidc token validation

The 17 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 docs/modules/servers/partials/configure/sieve.adoc |  26 +-
 examples/oidc/README.md                            |  47 +-
 examples/oidc/apisix/conf/apisix.yaml              |   2 +-
 examples/oidc/{docker-compose.yml => compose.yaml} |  47 +-
 examples/oidc/james/imapserver.xml                 |  12 +-
 examples/oidc/james/managesieveserver.xml          |  21 +
 examples/oidc/james/smtpserver.xml                 |  15 +-
 examples/oidc/james/usersrepository.xml            |   2 +-
 examples/oidc/test.sh                              |  52 +-
 .../james/managesieve/scripts/authenticate.test    |  21 +-
 .../managesieve/scripts/authenticateBase64.test    |   2 +-
 .../james/managesieve/scripts/capability.test      |   4 +-
 .../james/managesieve/scripts/checkscript.test     |   3 +-
 .../james/managesieve/scripts/deletescript.test    |   4 +-
 .../james/managesieve/scripts/getscript.test       |   4 +-
 .../james/managesieve/scripts/havespace.test       |   4 +-
 .../james/managesieve/scripts/listscripts.test     |   2 +-
 .../apache/james/managesieve/scripts/logout.test   |   2 +-
 .../org/apache/james/managesieve/scripts/noop.test |   2 +-
 .../james/managesieve/scripts/putscript.test       |   3 +-
 .../james/managesieve/scripts/renamescript.test    |   4 +-
 .../james/managesieve/scripts/setactive.test       |   2 +-
 .../apache/james/managesieve/scripts/starttls.test |   4 +-
 .../james/managesieve/scripts/unauthenticate.test  |   4 +-
 .../apache/james/protocols/api/OIDCSASLParser.java |  18 +-
 .../apache/james/protocols/api/OIDCSASLHelper.java |  12 +-
 protocols/managesieve/pom.xml                      |   8 +
 .../managesieve/api/CapabilityAdvertiser.java      |  27 -
 .../org/apache/james/managesieve/api/Session.java  |   6 +
 .../managesieve/api/commands/Authenticate.java     |   2 +-
 .../managesieve/api/commands/CoreCommands.java     |   5 +-
 .../james/managesieve/core/CoreProcessor.java      |  66 ++-
 .../core/OAUTHAuthenticationProcessor.java         |  75 +++
 .../core/PlainAuthenticationProcessor.java         |   6 +-
 .../managesieve/transcode/ArgumentParser.java      |   4 -
 .../transcode/ManageSieveProcessor.java            |  64 ++-
 .../apache/james/managesieve/util/ParserUtils.java |   4 +-
 .../james/managesieve/util/SettableSession.java    |  14 +
 .../org/apache/james/jwt/OidcJwtTokenVerifier.java |   2 +-
 .../apache/james/jwt/OidcSASLConfiguration.java    |   7 +-
 .../org/apache/james/jwt/OidcTokenFixture.java     |   3 +-
 .../james/imapserver/netty/IMAPServerTest.java     |  26 +-
 server/protocols/protocols-managesieve/pom.xml     |  53 ++
 .../netty/ManageSieveChannelUpstreamHandler.java   |   9 +-
 .../managesieveserver/netty/ManageSieveServer.java |  17 +-
 .../james/managesieveserver/AuthenticateTest.java  | 238 ++++++++
 .../james/managesieveserver/CapabilityTest.java    |  74 +++
 .../james/managesieveserver/ManageSieveClient.java | 108 ++++
 .../ManageSieveServerTestSystem.java               |  93 ++++
 .../apache/james/managesieveserver/OIDCTest.java   | 596 +++++++++++++++++++++
 .../src/test/resources/managesieveserver-oidc.xml  |  20 +
 .../src/test/resources/managesieveserver.xml       |   9 +
 .../org/apache/james/smtpserver/SMTPSaslTest.java  |  40 +-
 53 files changed, 1669 insertions(+), 226 deletions(-)
 rename examples/oidc/{docker-compose.yml => compose.yaml} (62%)
 create mode 100644 examples/oidc/james/managesieveserver.xml
 mode change 100644 => 100755 examples/oidc/test.sh
 delete mode 100644 protocols/managesieve/src/main/java/org/apache/james/managesieve/api/CapabilityAdvertiser.java
 create mode 100644 protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
 create mode 100644 server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
 create mode 100644 server/protocols/protocols-managesieve/src/test/resources/managesieveserver.xml


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


(james-project) 03/17: feat(managesieve): add OAUTHBEARER authentication mechanism

Posted by bt...@apache.org.
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 ebbc90ae00e9c6c61861c7e8b21e33a8503c938a
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Fri Aug 15 07:53:04 2025 +0200

    feat(managesieve): add OAUTHBEARER authentication mechanism
---
 .../java/org/apache/james/managesieve/api/commands/Authenticate.java | 2 +-
 .../main/java/org/apache/james/managesieve/core/CoreProcessor.java   | 3 ++-
 ...uthenticationProcessor.java => OAUTHAuthenticationProcessor.java} | 5 ++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
index 6912348283..4e4820cd90 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
@@ -30,7 +30,7 @@ import org.apache.james.managesieve.api.UnknownSaslMechanism;
 public interface Authenticate {
 
     enum SupportedMechanism {
-        PLAIN, XOAUTH2;
+        PLAIN, XOAUTH2, OAUTHBEARER;
 
         public static SupportedMechanism retrieveMechanism(String serializedData) throws UnknownSaslMechanism {
             for (SupportedMechanism supportedMechanism : SupportedMechanism.values()) {
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index c8f6aab73b..3417fff3cd 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -100,7 +100,8 @@ public class CoreProcessor implements CoreCommands {
             capabilities.put(Capabilities.OWNER, session.getUser().asString());
         }
         session.getOidcSASLConfiguration().ifPresent(oidcConfiguration -> {
-            this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.XOAUTH2, new XOAUTH2AuthenticationProcessor(oidcConfiguration));
+            this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.XOAUTH2, new OAUTHAuthenticationProcessor(oidcConfiguration));
+            this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.OAUTHBEARER, new OAUTHAuthenticationProcessor(oidcConfiguration));
         });
         capabilities.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
         return capabilities;
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
similarity index 96%
rename from protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java
rename to protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
index 8a65202ac5..ba925141a8 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
@@ -35,11 +35,11 @@ import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 import reactor.core.publisher.Mono;
 
-public class XOAUTH2AuthenticationProcessor implements AuthenticationProcessor {
+public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
     private final OidcSASLConfiguration oidcConfiguration;
 
-    public XOAUTH2AuthenticationProcessor(OidcSASLConfiguration oidcConfiguration) {
+    public OAUTHAuthenticationProcessor(OidcSASLConfiguration oidcConfiguration) {
         this.oidcConfiguration = oidcConfiguration;
     }
 
@@ -107,4 +107,3 @@ public class XOAUTH2AuthenticationProcessor implements AuthenticationProcessor {
             .map(Username::of);
     }
 }
-


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


(james-project) 13/17: refactor(managesieve): accept plain authentication without leading null byte

Posted by bt...@apache.org.
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 8b9bb31cf399d71353ea0f27a7d59e2061b1e4b6
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Nov 17 11:05:08 2025 +0100

    refactor(managesieve): accept plain authentication without leading null byte
---
 .../java/org/apache/james/managesieveserver/AuthenticateTest.java    | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index e9aea8302d..dfdce97777 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -82,13 +82,12 @@ public class AuthenticateTest {
     // The SASL PLAIN standard (https://datatracker.ietf.org/doc/html/rfc4616) defines the following message:
     // message = [authzid] UTF8NUL authcid UTF8NUL passwd
     // The current code is more lenient and accepts the message without the first null byte.
-    @Disabled
     @Test
-    void plainLoginWithMalformedMessageShouldNotSucceed() throws IOException {
+    void plainLoginWithoutLeadingNullByteShouldSucceed() throws IOException {
         String initialClientResponse = ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
-        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
     }
 
     @Test


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


(james-project) 10/17: refactor: add more errors and reset authentication state on authentication error

Posted by bt...@apache.org.
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 1cd3f9547e6e9ae913fc0c5615b28a891a409b66
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Wed Oct 8 17:20:38 2025 +0200

    refactor: add more errors and reset authentication state on authentication error
    
    While trying to get the MPT tests for managesieve running, I realized that a client
    is trapped in the state "AuthenticationInProgress" when using continuation. This
    made longer test scripts impossible.
    To consistently reset the session, I introduced more errors that are handled in one
    place instead of returning a "NO" answer directly during authentication.
    
    Those changes also required some small changes to the MPT and the normal tests.
    
    Additionally, there were a parsing bug when sending space-separated username and
    password in the authentication continuation which is fixed now.
---
 .../james/managesieve/scripts/authenticate.test    | 23 +++++----
 .../managesieve/scripts/authenticateBase64.test    |  2 +-
 .../james/managesieve/scripts/capability.test      |  7 +--
 .../james/managesieve/scripts/checkscript.test     |  6 +--
 .../james/managesieve/scripts/deletescript.test    |  7 +--
 .../james/managesieve/scripts/getscript.test       |  7 ++-
 .../james/managesieve/scripts/havespace.test       |  7 +--
 .../james/managesieve/scripts/listscripts.test     |  5 +-
 .../apache/james/managesieve/scripts/logout.test   |  2 +-
 .../org/apache/james/managesieve/scripts/noop.test |  2 +-
 .../james/managesieve/scripts/putscript.test       |  6 +--
 .../james/managesieve/scripts/renamescript.test    |  7 ++-
 .../james/managesieve/scripts/setactive.test       |  5 +-
 .../apache/james/managesieve/scripts/starttls.test |  7 +--
 .../james/managesieve/scripts/unauthenticate.test  |  7 +--
 .../james/managesieve/core/CoreProcessor.java      | 50 ++++++++++--------
 .../core/PlainAuthenticationProcessor.java         |  6 +--
 .../transcode/ManageSieveProcessor.java            | 59 +++++++++++++---------
 .../apache/james/managesieve/util/ParserUtils.java |  4 +-
 .../james/managesieveserver/AuthenticateTest.java  | 22 ++++----
 .../apache/james/managesieveserver/OIDCTest.java   |  4 +-
 21 files changed, 137 insertions(+), 108 deletions(-)

diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
index 334699cf14..9914f24f28 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
@@ -18,20 +18,25 @@
 ################################################################
 
 C: AUTHENTICATE
-S: NO ManageSieve syntax is incorrect : You must specify a SASL mechanism as an argument of AUTHENTICATE command
+S: NO "ManageSieve syntax is incorrect: quoted SASL mechanism must be supplied"
 
 C: AUTHENTICATE "UNKNOWN"
-S: NO Unknown SASL mechanism UNKNOWN
+S: NO "Unknown SASL mechanism UNKNOWN"
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
+S: ""
+S: OK
 C: GETSCRIPT toto.sieve
-S: NO ManageSieve syntax is incorrect : You must supply a password for the authentication mechanism. Formal syntax : <NULL>username<NULL>password
+S: NO "ManageSieve syntax is incorrect: quoted authentication data must be supplied"
 
-C:  tin password
-S: NO authentication failed
+C: AUTHENTICATE "PLAIN"
+S: ""
+S: OK
+C: "tin password"
+S: NO "Authentication failed with: Verification of credentials failed"
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
-S: OK
\ No newline at end of file
+S: ""
+S: OK
+C: "user password"
+S: OK
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticateBase64.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticateBase64.test
index cee22fb254..720257bbb2 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticateBase64.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticateBase64.test
@@ -18,4 +18,4 @@
 ################################################################
 
 C: AUTHENTICATE "PLAIN" "AHVzZXIAcGFzc3dvcmQ="
-S: OK
\ No newline at end of file
+S: OK
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
index 1b2141b124..16e6a75e02 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
@@ -38,8 +38,9 @@ S: "VERSION" "1.0"
 S: OK
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: CAPABILITY
@@ -51,4 +52,4 @@ S: "STARTTLS"
 S: "IMPLEMENTATION" "Apache ManageSieve v1.0"
 S: "VERSION" "1.0"
 }
-S: OK
\ No newline at end of file
+S: OK
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
index 8fc76dd500..33cc62e821 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
@@ -34,8 +34,9 @@ C:
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: CHECKSCRIPT {99+}
@@ -51,4 +52,3 @@ C: #comment
 C: InvalidSieveCommand
 C:
 S: NO "Syntax Error: org.apache.jsieve.parser.generated.ParseException: Encountered "<EOF>" at line 2, column 21.
-
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
index 5352d2aaf6..91658e2b55 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
@@ -24,8 +24,9 @@ C: DELETESCRIPT "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: DELETESCRIPT "foo"
@@ -77,4 +78,4 @@ C: SETACTIVE "mysievescript"
 S: OK
 
 C: DELETESCRIPT "mysievescript"
-S: NO \(ACTIVE\) "You may not delete an active script"
\ No newline at end of file
+S: NO \(ACTIVE\) "You may not delete an active script"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
index aa5ca1d9bb..8d1359100e 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
@@ -24,8 +24,9 @@ C: GETSCRIPT "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: GETSCRIPT "foo"
@@ -48,5 +49,3 @@ S:   fileinto "INBOX.sent";
 S: \}
 S:
 S: OK
-
-
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
index 2b0958276e..653f60fe43 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
@@ -27,12 +27,13 @@ C: HAVESPACE "scriptname" 49
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: HAVESPACE "scriptname" 49
 S: OK
 
 C: HAVESPACE "scriptname" 51
-S: NO \(QUOTA/MAXSIZE\) "Quota exceeded"
\ No newline at end of file
+S: NO \(QUOTA/MAXSIZE\) "Quota exceeded"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
index 1539277af0..a59dd4a700 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
@@ -21,8 +21,9 @@ C: LISTSCRIPTS
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: LISTSCRIPTS
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/logout.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/logout.test
index 125c4210e4..65bb0a2cc5 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/logout.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/logout.test
@@ -18,4 +18,4 @@
 ################################################################
 
 C: LOGOUT
-S: OK channel is closing
\ No newline at end of file
+S: OK channel is closing
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/noop.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/noop.test
index 837fb93001..bebba0bd4a 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/noop.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/noop.test
@@ -25,4 +25,4 @@ S: OK \(TAG \{16\}
 S: STARTTLS-SYNC-42\) "DONE"
 
 C: NooP
-S: OK "NOOP completed"
\ No newline at end of file
+S: OK "NOOP completed"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
index e8e300f64d..2b9c72e33f 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
@@ -39,8 +39,9 @@ C:
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: PUTSCRIPT "mysievescript" {97+}
@@ -56,4 +57,3 @@ C: #comment
 C: InvalidSieveCommand
 C:
 S: NO "Syntax Error: org.apache.jsieve.parser.generated.ParseException: Encountered "<EOF>" at line 2, column 21.
-
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
index 355cd84461..1de65f25d0 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
@@ -27,8 +27,9 @@ C: RENAMESCRIPT "foo" "bar"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: PUTSCRIPT "mysievescript" {99+}
@@ -75,5 +76,3 @@ S: OK
 
 C: RENAMESCRIPT "mysievescript" "mysievescriptbis"
 S: NO \(ALREADYEXISTS\) "A script with that name already exists"
-
-
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
index c05ff82e95..5a836693f6 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
@@ -24,8 +24,9 @@ C: SETACTIVE "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: SETACTIVE "foo"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
index 7e973540b4..8220140f7e 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
@@ -24,11 +24,12 @@ C: STARTTLS
 S: NO You can't enable two time SSL encryption
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: STARTTLS
 S: NO command STARTTLS is issued in the wrong state. It must be issued as you are unauthenticated
 
-C: LOGOUT
\ No newline at end of file
+C: LOGOUT
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
index 4acc436f4a..21ef39e49f 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
@@ -27,8 +27,9 @@ C: UNAUTHENTICATE
 S: NO UNAUTHENTICATE command must be issued in authenticated state
 
 C: AUTHENTICATE "PLAIN"
-S: \+ ""
-C:  user password
+S: ""
+S: OK
+C: "user password"
 S: OK
 
 C: GETSCRIPT any
@@ -38,4 +39,4 @@ C: UNAUTHENTICATE
 S: OK
 
 C: GETSCRIPT any
-S: NO
\ No newline at end of file
+S: NO
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index ebb8b7e36d..33cc055fb1 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -210,25 +210,26 @@ public class CoreProcessor implements CoreCommands {
 
     @Override
     public String chooseMechanism(Session session, String mechanism) {
-        if (session.isAuthenticated()) {
-            return "NO \"already authenticated\"";
-        }
         try {
             if (Strings.isNullOrEmpty(mechanism)) {
-                return "NO ManageSieve syntax is incorrect : You must specify a SASL mechanism as an argument of AUTHENTICATE command";
+                throw new SyntaxException("quoted SASL mechanism must be supplied");
             }
-            String unquotedMechanism = ParserUtils.unquoteFirst(mechanism);
-            SupportedMechanism supportedMechanism = SupportedMechanism.retrieveMechanism(unquotedMechanism);
+
+            SupportedMechanism supportedMechanism = SupportedMechanism.retrieveMechanism(mechanism);
             if (!this.authenticationProcessorMap.containsKey(supportedMechanism)) {
-                throw new UnknownSaslMechanism("SASL mechanism disabled: " + unquotedMechanism);
+                throw new UnknownSaslMechanism("SASL mechanism disabled: " + mechanism);
             }
 
             session.setChoosedAuthenticationMechanism(supportedMechanism);
             session.setState(Session.State.AUTHENTICATION_IN_PROGRESS);
             AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(supportedMechanism);
             return authenticationProcessor.initialServerResponse(session);
-        } catch (UnknownSaslMechanism unknownSaslMechanism) {
-            return "NO " + unknownSaslMechanism.getMessage();
+        } catch (UnknownSaslMechanism e) {
+            resetSession(session);
+            return "NO \"" + e.getMessage() + "\"";
+        } catch (SyntaxException e) {
+            resetSession(session);
+            return "NO \"ManageSieve syntax is incorrect: " + e.getMessage() + "\"";
         }
     }
 
@@ -237,41 +238,46 @@ public class CoreProcessor implements CoreCommands {
         try {
             SupportedMechanism currentAuthenticationMechanism = session.getChoosedAuthenticationMechanism();
             AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(currentAuthenticationMechanism);
-            String unquotedSuppliedData = ParserUtils.unquoteFirst(suppliedData);
-            if (unquotedSuppliedData == null) {
-                return "NO \"authentication failed\"";
+            if (Strings.isNullOrEmpty(suppliedData)) {
+                throw new SyntaxException("quoted authentication data must be supplied");
             }
-            if (unquotedSuppliedData.equals("*")) {
-                return "NO \"authentication aborted\"";
+            if (suppliedData.equals("*")) {
+                throw new AuthenticationException("authentication aborted by client");
             }
-            Username authenticatedUsername = authenticationProcessor.isAuthenticationSuccesfull(session, unquotedSuppliedData);
+            Username authenticatedUsername = authenticationProcessor.isAuthenticationSuccesfull(session, suppliedData);
             if (authenticatedUsername != null) {
                 session.setUser(authenticatedUsername);
                 session.setState(Session.State.AUTHENTICATED);
                 return "OK";
             } else {
-                session.setState(Session.State.UNAUTHENTICATED);
-                session.setUser(null);
-                return "NO authentication failed";
+                resetSession(session);
+                return "NO \"authentication failed\"";
             }
         } catch (AuthenticationException e) {
-            return "NO Authentication failed with: " + e.getMessage();
+            resetSession(session);
+            return "NO \"Authentication failed with: " + e.getMessage() + "\"";
         } catch (SyntaxException e) {
-            return "NO ManageSieve syntax is incorrect : " + e.getMessage();
+            resetSession(session);
+            return "NO \"ManageSieve syntax is incorrect: " + e.getMessage() + "\"";
         }
     }
 
     @Override
     public String unauthenticate(Session session) {
         if (session.isAuthenticated()) {
-            session.setState(Session.State.UNAUTHENTICATED);
-            session.setUser(null);
+            resetSession(session);
             return "OK";
         } else {
             return "NO UNAUTHENTICATE command must be issued in authenticated state";
         }
     }
 
+    private static void resetSession(Session session) {
+        session.setState(Session.State.UNAUTHENTICATED);
+        session.setUser(null);
+        session.setChoosedAuthenticationMechanism(null);
+    }
+
     @Override
     public void logout() throws SessionTerminatedException {
         throw new SessionTerminatedException();
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
index 72837b37e6..1a6164b5d8 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
@@ -72,11 +72,11 @@ public class PlainAuthenticationProcessor implements AuthenticationProcessor {
     private Username authenticateWithSeparator(Session session, String suppliedClientData, char c) throws SyntaxException, AuthenticationException {
         Iterator<String> it = Splitter.on(c).omitEmptyStrings().split(suppliedClientData).iterator();
         if (!it.hasNext()) {
-            throw new SyntaxException("You must supply a username for the authentication mechanism. Formal syntax : <NULL>username<NULL>password");
+            throw new SyntaxException("You must supply a username for the authentication mechanism. Formal syntax: <NULL>username<NULL>password");
         }
         Username userName = Username.of(it.next());
         if (!it.hasNext()) {
-            throw new SyntaxException("You must supply a password for the authentication mechanism. Formal syntax : <NULL>username<NULL>password");
+            throw new SyntaxException("You must supply a password for the authentication mechanism. Formal syntax: <NULL>username<NULL>password");
         }
         String password = it.next();
         session.setUser(userName);
@@ -85,7 +85,7 @@ public class PlainAuthenticationProcessor implements AuthenticationProcessor {
             if (user != null && user.verifyPassword(password)) {
                 return user.getUserName();
             } else {
-                return null;
+                throw new AuthenticationException("Verification of credentials failed");
             }
         } catch (UsersRepositoryException e) {
             throw new AuthenticationException(e.getMessage());
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
index d07b323c4d..83cca6f8a0 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
@@ -22,10 +22,10 @@ package org.apache.james.managesieve.transcode;
 
 import jakarta.inject.Inject;
 
-import org.apache.commons.lang3.StringUtils;
 import org.apache.james.managesieve.api.ManageSieveException;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.SessionTerminatedException;
+import org.apache.james.managesieve.util.ParserUtils;
 import org.apache.james.sieverepository.api.exception.SieveRepositoryException;
 
 public class ManageSieveProcessor {
@@ -54,6 +54,17 @@ public class ManageSieveProcessor {
     }
 
     public String handleRequest(Session session, String request) throws ManageSieveException, SieveRepositoryException {
+        if (request.endsWith("\n")) {
+            request = request.substring(0, request.length() - 1);
+        }
+        if (request.endsWith("\r")) {
+            request = request.substring(0, request.length() - 1);
+        }
+
+        if (session.getState() == Session.State.AUTHENTICATION_IN_PROGRESS) {
+            return matchCommandWithImplementation(session, request.trim(), AUTHENTICATE) + "\r\n";
+        }
+
         int firstWordEndIndex = request.indexOf(' ');
         String arguments = parseArguments(request, firstWordEndIndex);
         String command = parseCommand(request, firstWordEndIndex);
@@ -67,12 +78,6 @@ public class ManageSieveProcessor {
         } else {
             command = request;
         }
-        if (command.endsWith("\n")) {
-            command = command.substring(0, command.length() - 1);
-        }
-        if (command.endsWith("\r")) {
-            command = command.substring(0, command.length() - 1);
-        }
         return command;
     }
 
@@ -85,27 +90,35 @@ public class ManageSieveProcessor {
     }
 
     private String matchCommandWithImplementation(Session session, String arguments, String command) throws SessionTerminatedException {
-        if (session.getState() == Session.State.AUTHENTICATION_IN_PROGRESS) {
-            return argumentParser.authenticate(session, command);
-        }
         if (command.equalsIgnoreCase(AUTHENTICATE)) {
-            if (StringUtils.countMatches(arguments, "\"") == 4) {
-                String result = argumentParser.chooseMechanism(session, arguments);
+            // The RFC forbids the AUTHENTICATE command if the session is already authenticated.
+            if (session.isAuthenticated()) {
+                return "NO \"already authenticated\"";
+            }
+
+            // If no authentication is in progress, the authentication mechanism needs to be chosen.
+            if (session.getState() != Session.State.AUTHENTICATION_IN_PROGRESS) {
+                String mechanism = ParserUtils.unquoteFirst(arguments);
+                String result = argumentParser.chooseMechanism(session, mechanism);
+                // If the authentication is not in progress, return the result (error) because choosing the mechanism has failed.
                 if (session.getState() != Session.State.AUTHENTICATION_IN_PROGRESS) {
                     return result;
                 }
-                int bracket1 = arguments.indexOf('\"');
-                int bracket2 = arguments.indexOf('\"', bracket1 + 1);
-                int bracket3 = arguments.indexOf('\"', bracket2 + 1);
-                int bracket4 = arguments.indexOf('\"', bracket3 + 1);
-
-                return argumentParser.authenticate(session, arguments.substring(bracket3, bracket4 + 1));
-            } else if (arguments.split(" ").length != 1) {
-                // The client send additional arguments but didn't quote them. It probably thinks that it does not need
-                // to send more, but the server expects more. Reject this authentication now to solve this conflict.
-                return "NO \"unquoted argument found\"";
+
+                // Skips the whole mechanism, the closing quote, and the space if present.
+                // If the request is well-formatted, the arguments are now empty or contain the client's initial response.
+                arguments = arguments.substring(arguments.indexOf(mechanism) + mechanism.length() + 1);
+                if (arguments.startsWith(" ")) {
+                    arguments = arguments.substring(1);
+                }
+                // If there are is no initial client response left, return the result (initial server response).
+                if (arguments.isEmpty()) {
+                    return result;
+                }
             }
-            return argumentParser.chooseMechanism(session, arguments);
+
+            // The authentication is in progress, the mechanism has been chosen, and the arguments contain an initial client response.
+            return argumentParser.authenticate(session, ParserUtils.unquoteFirst(arguments));
         } else if (command.equalsIgnoreCase(CAPABILITY)) {
             return argumentParser.capability(session, arguments);
         } else if (command.equalsIgnoreCase(CHECKSCRIPT)) {
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
index f07198843f..86fbd7c02d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/ParserUtils.java
@@ -56,8 +56,8 @@ public class ParserUtils {
         }
         if (quoted.length() > 2 && quoted.startsWith("\"") && quoted.indexOf('\"', 1) >= 0) {
             return quoted.substring(1, quoted.indexOf('\"', 1));
-        } else if (quoted.startsWith("'") && quoted.endsWith("'")) {
-            return quoted.substring(1, quoted.length() - 1);
+        } else if (quoted.length() > 2 && quoted.startsWith("'") && quoted.indexOf('\'', 1) >= 0) {
+            return quoted.substring(1, quoted.indexOf('\'', 1));
         }
         return null;
     }
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index b9d0829aa3..91d99139c2 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -57,7 +57,7 @@ public class AuthenticateTest {
 
     @Test
     void plainLoginWithWrongPasswordShouldNotSucceed() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD + "wrong");
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD + "wrong";
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -65,7 +65,7 @@ public class AuthenticateTest {
 
     @Test
     void plainLoginWithNotExistingUserShouldNotSucceed() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "not-existing" + "\0" + "pwd");
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "not-existing" + "\0" + "pwd";
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -73,7 +73,7 @@ public class AuthenticateTest {
 
     @Test
     void plainLoginWithoutPasswordShouldNotSucceed() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0");
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0";
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -81,11 +81,11 @@ public class AuthenticateTest {
 
     // The SASL PLAIN standard (https://datatracker.ietf.org/doc/html/rfc4616) defines the following message:
     // message = [authzid] UTF8NUL authcid UTF8NUL passwd
-    // The current code is more lenient.
+    // The current code is more lenient and accepts the message without the first null byte.
     @Disabled
     @Test
     void plainLoginWithMalformedMessageShouldNotSucceed() throws IOException {
-        String initialClientResponse = (ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -93,7 +93,7 @@ public class AuthenticateTest {
 
     @Test
     void plainLoginWithoutMechanismQuotesShouldNotSucceed() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE PLAIN \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -101,7 +101,7 @@ public class AuthenticateTest {
 
     @Test
     void plainLoginWithoutInitialResponseQuotesShouldNotSucceed() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" " + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)));
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
@@ -114,7 +114,7 @@ public class AuthenticateTest {
         Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
         Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
 
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("\"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
@@ -130,12 +130,12 @@ public class AuthenticateTest {
         this.client.sendCommand("\"*\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
-        Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+        Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");
     }
 
     @Test
     void doubleAuthenticationShouldFail() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         String command = "AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"";
 
         this.client.sendCommand(command);
@@ -212,7 +212,7 @@ public class AuthenticateTest {
     }
 
     void authenticatePlain() throws IOException {
-        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
index d331c24f71..b122d8ddf6 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
@@ -132,7 +132,7 @@ public class OIDCTest {
             this.client.sendCommand("\"*\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
-            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");
         }
 
         @Test
@@ -171,7 +171,7 @@ public class OIDCTest {
             this.client.sendCommand("\"*\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
-            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");
         }
 
         @Test


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


(james-project) 11/17: fix(managesieve): Implement continuation as described in RFC-4616

Posted by bt...@apache.org.
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 a8950829cf959bee8f5f18c0572ac44a5610dc77
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Nov 17 11:02:56 2025 +0100

    fix(managesieve): Implement continuation as described in RFC-4616
---
 .../apache/james/managesieve/scripts/authenticate.test   |  9 +++------
 .../org/apache/james/managesieve/scripts/capability.test |  3 +--
 .../apache/james/managesieve/scripts/checkscript.test    |  3 +--
 .../apache/james/managesieve/scripts/deletescript.test   |  3 +--
 .../org/apache/james/managesieve/scripts/getscript.test  |  3 +--
 .../org/apache/james/managesieve/scripts/havespace.test  |  3 +--
 .../apache/james/managesieve/scripts/listscripts.test    |  3 +--
 .../org/apache/james/managesieve/scripts/putscript.test  |  3 +--
 .../apache/james/managesieve/scripts/renamescript.test   |  3 +--
 .../org/apache/james/managesieve/scripts/setactive.test  |  3 +--
 .../org/apache/james/managesieve/scripts/starttls.test   |  3 +--
 .../apache/james/managesieve/scripts/unauthenticate.test |  3 +--
 .../managesieve/core/OAUTHAuthenticationProcessor.java   |  2 +-
 .../managesieve/core/PlainAuthenticationProcessor.java   |  2 +-
 .../apache/james/managesieveserver/AuthenticateTest.java |  8 ++++----
 .../james/managesieveserver/ManageSieveClient.java       |  4 ++++
 .../org/apache/james/managesieveserver/OIDCTest.java     | 16 ++++++++--------
 17 files changed, 32 insertions(+), 42 deletions(-)

diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
index 9914f24f28..54c8a6140c 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
@@ -24,19 +24,16 @@ C: AUTHENTICATE "UNKNOWN"
 S: NO "Unknown SASL mechanism UNKNOWN"
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: GETSCRIPT toto.sieve
 S: NO "ManageSieve syntax is incorrect: quoted authentication data must be supplied"
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "tin password"
 S: NO "Authentication failed with: Verification of credentials failed"
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
index 16e6a75e02..095c34db56 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
@@ -38,8 +38,7 @@ S: "VERSION" "1.0"
 S: OK
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
index 33cc62e821..768909990c 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
@@ -34,8 +34,7 @@ C:
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
index 91658e2b55..be80044cac 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
@@ -24,8 +24,7 @@ C: DELETESCRIPT "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
index 8d1359100e..a04c277122 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
@@ -24,8 +24,7 @@ C: GETSCRIPT "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
index 653f60fe43..aaa2d3bfe9 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
@@ -27,8 +27,7 @@ C: HAVESPACE "scriptname" 49
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
index a59dd4a700..9f048b42f9 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
@@ -21,8 +21,7 @@ C: LISTSCRIPTS
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
index 2b9c72e33f..b8048ce22a 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
@@ -39,8 +39,7 @@ C:
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
index 1de65f25d0..7b5ff18344 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
@@ -27,8 +27,7 @@ C: RENAMESCRIPT "foo" "bar"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
index 5a836693f6..c233498a47 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
@@ -24,8 +24,7 @@ C: SETACTIVE "foo"
 S: NO
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
index 8220140f7e..39fdbf3f1d 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
@@ -24,8 +24,7 @@ C: STARTTLS
 S: NO You can't enable two time SSL encryption
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
index 21ef39e49f..92576fdba8 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
@@ -27,8 +27,7 @@ C: UNAUTHENTICATE
 S: NO UNAUTHENTICATE command must be issued in authenticated state
 
 C: AUTHENTICATE "PLAIN"
-S: ""
-S: OK
+S: \+ ""
 C: "user password"
 S: OK
 
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
index e424d2759e..ba925141a8 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
@@ -45,7 +45,7 @@ public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
     @Override
     public String initialServerResponse(Session session) {
-        return "\"\"\r\nOK";
+        return "+ \"\"";
     }
 
     @Override
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
index 1a6164b5d8..1e9e659638 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
@@ -50,7 +50,7 @@ public class PlainAuthenticationProcessor implements AuthenticationProcessor {
 
     @Override
     public String initialServerResponse(Session session) {
-        return "\"\"\r\nOK";
+        return "+ \"\"";
     }
 
 
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index 91d99139c2..e9aea8302d 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -111,8 +111,8 @@ public class AuthenticateTest {
     void plainLoginWithContinuationShouldSucceed() throws IOException {
         this.client.sendCommand("AUTHENTICATE \"PLAIN\"");
         ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-        Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+        Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
         String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("\"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
@@ -124,8 +124,8 @@ public class AuthenticateTest {
     void plainLoginWithContinuationCanBeAborted() throws IOException {
         this.client.sendCommand("AUTHENTICATE \"PLAIN\"");
         ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-        Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+        Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
         this.client.sendCommand("\"*\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
index e712489b70..c4f042b7ba 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
@@ -37,6 +37,7 @@ public class ManageSieveClient extends SocketClient {
 
     enum ResponseType {
         BYE,
+        CONTINUATION,
         NO,
         OK;
     }
@@ -90,6 +91,9 @@ public class ManageSieveClient extends SocketClient {
                 }
 
                 response = new ServerResponse(responseType, responseCode, explanation, lines);
+            } else if (tokens[0].equals("+")) {
+                Optional explanation = Optional.of(tokens[1].substring(1, tokens[1].length() - 1));
+                response = new ServerResponse(ResponseType.CONTINUATION, Optional.empty(), explanation, new ArrayList());
             } else {
                 lines.addLast(line);
             }
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
index b122d8ddf6..a07785e77d 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
@@ -114,8 +114,8 @@ public class OIDCTest {
         void oauthbearerLoginWithValidTokenAndContinuationShouldSucceed() throws Exception {
             this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\"");
             ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+            Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
             this.client.sendCommand("\"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
@@ -126,8 +126,8 @@ public class OIDCTest {
         void oauthbearerLoginWithValidTokenAndContinuationCanBeAborted() throws Exception {
             this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\"");
             ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+            Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
             this.client.sendCommand("\"*\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
@@ -153,8 +153,8 @@ public class OIDCTest {
         void xoauth2LoginWithValidTokenAndContinuationShouldSucceed() throws Exception {
             this.client.sendCommand("AUTHENTICATE \"XOAUTH2\"");
             ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+            Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
             this.client.sendCommand("\"" + VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE + "\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
@@ -165,8 +165,8 @@ public class OIDCTest {
         void xoauth2LoginWithValidTokenAndContinuationCanBeAborted() throws Exception {
             this.client.sendCommand("AUTHENTICATE \"XOAUTH2\"");
             ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
-            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
-            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
+            Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
             this.client.sendCommand("\"*\"");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();


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


(james-project) 07/17: fix(examples): fix introspection mismatching issuer error

Posted by bt...@apache.org.
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 0e78452ba3f915bee4f4b8829b8238147e8ddad4
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Sep 8 14:08:51 2025 +0200

    fix(examples): fix introspection mismatching issuer error
---
 examples/oidc/docker-compose.yml   |  6 +++---
 examples/oidc/james/imapserver.xml | 12 +++---------
 examples/oidc/james/smtpserver.xml | 15 +++------------
 3 files changed, 9 insertions(+), 24 deletions(-)

diff --git a/examples/oidc/docker-compose.yml b/examples/oidc/docker-compose.yml
index a27f1ff294..65659d06f4 100644
--- a/examples/oidc/docker-compose.yml
+++ b/examples/oidc/docker-compose.yml
@@ -27,6 +27,8 @@ services:
     volumes:
       - ./james/usersrepository.xml:/root/conf/usersrepository.xml
       - ./james/jmap.properties:/root/conf/jmap.properties
+      - ./james/imapserver.xml:/root/conf/imapserver.xml
+      - ./james/smtpserver.xml:/root/conf/smtpserver.xml
     ports:
       - "8000:8000"
     healthcheck:
@@ -46,9 +48,7 @@ services:
       - KEYCLOAK_PASSWORD=admin
       - KEYCLOAK_IMPORT=/tmp/realm-oidc.json
     networks:
-      james:
-        aliases:
-          - keycloak
+      - james
 
   ldap:
     container_name: ldap
diff --git a/examples/oidc/james/imapserver.xml b/examples/oidc/james/imapserver.xml
index e590c7dd5e..641f6c5067 100644
--- a/examples/oidc/james/imapserver.xml
+++ b/examples/oidc/james/imapserver.xml
@@ -4,12 +4,6 @@
         <jmxName>imapserver</jmxName>
         <bind>0.0.0.0:143</bind>
         <connectionBacklog>200</connectionBacklog>
-        <tls socketTLS="false" startTLS="false">
-            <keystore>file://conf/keystore</keystore>
-            <keystoreType>PKCS12</keystoreType>
-            <secret>james72laBalle</secret>
-            <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
-        </tls>
         <connectionLimit>0</connectionLimit>
         <connectionLimitPerIP>0</connectionLimitPerIP>
         <idleTimeInterval>120</idleTimeInterval>
@@ -18,12 +12,12 @@
         <auth>
             <plainAuthEnabled>true</plainAuthEnabled>
             <oidc>
-                <oidcConfigurationURL>http://keycloak:8080/auth/realms/oidc/.well-known/openid-configuration</oidcConfigurationURL>
-                <jwksURL>http://keycloak:8080/auth/realms/oidc/protocol/openid-connect/certs</jwksURL>
+                <oidcConfigurationURL>http://sso.example.com:8080/auth/realms/oidc/.well-known/openid-configuration</oidcConfigurationURL>
+                <jwksURL>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/certs</jwksURL>
                 <claim>email</claim>
                 <scope>openid profile email</scope>
                 <introspection>
-                    <url>http://keycloak:8080/auth/realms/oidc/protocol/openid-connect/token/introspect</url>
+                    <url>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/token/introspect</url>
                     <auth>Basic amFtZXMtdGh1bmRlcmJpcmQ6WHc5aHQxdmVUdTBUazVzTU15MDNQZHpZM0FpRnZzc3c=</auth>
                 </introspection>
             </oidc>
diff --git a/examples/oidc/james/smtpserver.xml b/examples/oidc/james/smtpserver.xml
index 6af07c4554..e4f3655157 100644
--- a/examples/oidc/james/smtpserver.xml
+++ b/examples/oidc/james/smtpserver.xml
@@ -4,13 +4,6 @@
         <jmxName>smtpserver</jmxName>
         <bind>0.0.0.0:587</bind>
         <connectionBacklog>200</connectionBacklog>
-        <tls socketTLS="false" startTLS="false">
-            <keystore>file://conf/keystore</keystore>
-            <keystoreType>PKCS12</keystoreType>
-            <secret>james72laBalle</secret>
-            <provider>org.bouncycastle.jce.provider.BouncyCastleProvider</provider>
-            <algorithm>SunX509</algorithm>
-        </tls>
         <connectiontimeout>360</connectiontimeout>
         <connectionLimit>0</connectionLimit>
         <connectionLimitPerIP>0</connectionLimitPerIP>
@@ -18,12 +11,12 @@
             <announce>forUnauthorizedAddresses</announce>
             <plainAuthEnabled>true</plainAuthEnabled>
             <oidc>
-                <oidcConfigurationURL>http://keycloak:8080/auth/realms/oidc/.well-known/openid-configuration</oidcConfigurationURL>
-                <jwksURL>http://keycloak:8080/auth/realms/oidc/protocol/openid-connect/certs</jwksURL>
+                <oidcConfigurationURL>http://sso.example.com:8080/auth/realms/oidc/.well-known/openid-configuration</oidcConfigurationURL>
+                <jwksURL>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/certs</jwksURL>
                 <claim>email</claim>
                 <scope>openid profile email</scope>
                 <introspection>
-                    <url>http://keycloak:8080/auth/realms/oidc/protocol/openid-connect/token/introspect</url>
+                    <url>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/token/introspect</url>
                     <auth>Basic amFtZXMtdGh1bmRlcmJpcmQ6WHc5aHQxdmVUdTBUazVzTU15MDNQZHpZM0FpRnZzc3c=</auth>
                 </introspection>
             </oidc>
@@ -39,5 +32,3 @@
         </handlerchain>
     </smtpserver>
 </smtpservers>
-
-


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


(james-project) 05/17: test(managesieve): test authentication of managesieve server

Posted by bt...@apache.org.
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 0c8b5d20d40405a93621d25e3c70a7180c8b488f
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Fri Aug 15 08:05:13 2025 +0200

    test(managesieve): test authentication of managesieve server
---
 .../org/apache/james/jwt/OidcTokenFixture.java     |   2 +-
 server/protocols/protocols-managesieve/pom.xml     |  53 ++
 .../james/managesieveserver/AuthenticateTest.java  | 220 ++++++++
 .../james/managesieveserver/CapabilityTest.java    |  74 +++
 .../james/managesieveserver/ManageSieveClient.java | 104 ++++
 .../ManageSieveServerTestSystem.java               |  93 ++++
 .../apache/james/managesieveserver/OIDCTest.java   | 568 +++++++++++++++++++++
 .../src/test/resources/managesieveserver-oidc.xml  |  16 +
 .../src/test/resources/managesieveserver.xml       |   9 +
 9 files changed, 1138 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
index d07646e5c4..dcf09137ba 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
@@ -107,7 +107,7 @@ public class OidcTokenFixture {
         "}";
 
     public static final String CLAIM = "email_address";
-    //  "email_address": "user@domain.org"
+    public static final String USER_EMAIL_ADDRESS = "user@domain.org";
     public static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Inc4MFBzNUlhc24tYUdXbXcyVHJ4RGlOY2FocEgyc1h6NXBxZGhBbDlIWGMifQ.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5 [...]
     public static final String VALID_TOKEN_HAS_NOT_KID = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5vbmNlIjoiNWUyOGJjNTAtODE5NS00NjM3LThmMWEtYWUzNWFlYTk0NTc1I [...]
     public static final String VALID_TOKEN_HAS_NOT_FOUND_KID = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5vdEZvdW5kIn0.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5vbmNlIjoiNWUyOGJjNTAtODE5NS00 [...]
diff --git a/server/protocols/protocols-managesieve/pom.xml b/server/protocols/protocols-managesieve/pom.xml
index 3a96075ebc..0c7f1e5b1d 100644
--- a/server/protocols/protocols-managesieve/pom.xml
+++ b/server/protocols/protocols-managesieve/pom.xml
@@ -13,14 +13,42 @@
     <name>Apache James :: Server :: ManageSieve</name>
 
     <dependencies>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-file</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-data-memory</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-filesystem-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-filesystem-api</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </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>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-protocols-library</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-util</artifactId>
@@ -30,6 +58,16 @@
             <artifactId>testing-base</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-api</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-managesieve</artifactId>
@@ -38,6 +76,11 @@
             <groupId>${james.protocols.groupId}</groupId>
             <artifactId>protocols-netty</artifactId>
         </dependency>
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>io.netty</groupId>
             <artifactId>netty-handler</artifactId>
@@ -54,6 +97,16 @@
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-configuration2</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mock-server</groupId>
+            <artifactId>mockserver-netty</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>jcl-over-slf4j</artifactId>
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
new file mode 100644
index 0000000000..b9d0829aa3
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -0,0 +1,220 @@
+/****************************************************************
+ * 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.managesieveserver;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+public class AuthenticateTest {
+    private ManageSieveClient client;
+    private final ManageSieveServerTestSystem testSystem;
+
+    public AuthenticateTest() throws Exception {
+        this.testSystem = new ManageSieveServerTestSystem();
+    }
+
+    @BeforeEach
+    void setUp() throws Exception {
+        this.testSystem.setUp();
+        this.client = new ManageSieveClient();
+        this.client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort());
+        this.client.readResponse();
+    }
+
+    @AfterEach
+    void tearDown() {
+        this.testSystem.manageSieveServer.destroy();
+    }
+
+    @Test
+    void plainLoginWithCorrectCredentialsShouldSucceed() throws IOException {
+        this.authenticatePlain();
+    }
+
+    @Test
+    void plainLoginWithWrongPasswordShouldNotSucceed() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD + "wrong");
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void plainLoginWithNotExistingUserShouldNotSucceed() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "not-existing" + "\0" + "pwd");
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void plainLoginWithoutPasswordShouldNotSucceed() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0");
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    // The SASL PLAIN standard (https://datatracker.ietf.org/doc/html/rfc4616) defines the following message:
+    // message = [authzid] UTF8NUL authcid UTF8NUL passwd
+    // The current code is more lenient.
+    @Disabled
+    @Test
+    void plainLoginWithMalformedMessageShouldNotSucceed() throws IOException {
+        String initialClientResponse = (ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void plainLoginWithoutMechanismQuotesShouldNotSucceed() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        this.client.sendCommand("AUTHENTICATE PLAIN \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void plainLoginWithoutInitialResponseQuotesShouldNotSucceed() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" " + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)));
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void plainLoginWithContinuationShouldSucceed() throws IOException {
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\"");
+        ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        this.client.sendCommand("\"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+    }
+
+    @Test
+    void plainLoginWithContinuationCanBeAborted() throws IOException {
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\"");
+        ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+        Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+        this.client.sendCommand("\"*\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+    }
+
+    @Test
+    void doubleAuthenticationShouldFail() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        String command = "AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"";
+
+        this.client.sendCommand(command);
+        ManageSieveClient.ServerResponse firstAuthenticationResponse = this.client.readResponse();
+        Assertions.assertThat(firstAuthenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+
+        this.client.sendCommand(command);
+        ManageSieveClient.ServerResponse secondAuthenticationResponse = this.client.readResponse();
+        Assertions.assertThat(secondAuthenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        Assertions.assertThat(secondAuthenticationResponse.explanation()).get().isEqualTo("already authenticated");
+    }
+
+    @Test
+    void unauthenticateInUnauthenticatedStateShouldFail() throws IOException {
+        this.client.sendCommand("UNAUTHENTICATE");
+        ManageSieveClient.ServerResponse response = this.client.readResponse();
+        Assertions.assertThat(response.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    @Test
+    void unauthenticateInAuthenticatedStateShouldSucceed() throws IOException {
+        this.authenticatePlain();
+
+        this.client.sendCommand("UNAUTHENTICATE");
+        ManageSieveClient.ServerResponse response = this.client.readResponse();
+        Assertions.assertThat(response.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+    }
+
+    @Test
+    void authenticatedStateUnlocksNewCommands() throws IOException {
+        this.client.sendCommand("LISTSCRIPTS");
+        ManageSieveClient.ServerResponse unauthenticatedResponse = this.client.readResponse();
+        Assertions.assertThat(unauthenticatedResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+
+        this.authenticatePlain();
+
+        this.client.sendCommand("LISTSCRIPTS");
+        ManageSieveClient.ServerResponse authenticatedResponse = this.client.readResponse();
+        Assertions.assertThat(authenticatedResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+
+        this.client.sendCommand("UNAUTHENTICATE");
+        ManageSieveClient.ServerResponse response = this.client.readResponse();
+        Assertions.assertThat(response.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+
+        this.client.sendCommand("LISTSCRIPTS");
+        ManageSieveClient.ServerResponse loggedOutResponse = this.client.readResponse();
+        Assertions.assertThat(loggedOutResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+    }
+
+    // The server actually disconnects but isConnected still returns True.
+    // Even when adding a delay, it still returns True.
+    // There is probably something else broken with this test.
+    @Disabled
+    @Test
+    void logoutShouldWorkInUnauthenticatedState() throws IOException, InterruptedException {
+        this.client.sendCommand("LOGOUT");
+        ManageSieveClient.ServerResponse response = this.client.readResponse();
+        Assertions.assertThat(response.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        Assertions.assertThat(this.client.isConnected()).isFalse();
+    }
+
+    // The server actually disconnects but isConnected still returns True.
+    // Even when adding a delay, it still returns True.
+    // There is probably something else broken with this test.
+    @Disabled
+    @Test
+    void logoutShouldWorkInAuthenticatedState() throws IOException, InterruptedException {
+        this.authenticatePlain();
+
+        this.client.sendCommand("LOGOUT");
+        ManageSieveClient.ServerResponse response = this.client.readResponse();
+        Assertions.assertThat(response.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        Assertions.assertThat(this.client.isConnected()).isFalse();
+    }
+
+    void authenticatePlain() throws IOException {
+        String initialClientResponse = ("\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD);
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+    }
+}
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
new file mode 100644
index 0000000000..f13ffe3d8f
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/CapabilityTest.java
@@ -0,0 +1,74 @@
+/****************************************************************
+ * 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.managesieveserver;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+class CapabilityTest {
+    private final ManageSieveServerTestSystem testSystem;
+
+    public CapabilityTest() throws Exception {
+        this.testSystem = new ManageSieveServerTestSystem();
+    }
+
+    @AfterEach
+    void tearDown() {
+        this.testSystem.manageSieveServer.destroy();
+    }
+
+    @Test
+    void shouldAnnounceOnlyPlainAuthenticationWithDefaultConfig() throws Exception {
+        this.testSystem.setUp();
+
+        ManageSieveClient client = new ManageSieveClient();
+        client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort());
+        ManageSieveClient.ServerResponse initialGreeting = client.readResponse();
+        Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN");
+
+        client.sendCommand("CAPABILITY");
+        ManageSieveClient.ServerResponse capabilityResponse = client.readResponse();
+        Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN");
+    }
+
+    @Test
+    void shouldAnnouncePlainAndOauthWhenConfigured() throws Exception {
+        this.testSystem.setUp("managesieveserver-oidc.xml");
+
+        ManageSieveClient client = new ManageSieveClient();
+        client.connect(this.testSystem.getBindedIP(), this.testSystem.getBindedPort());
+        ManageSieveClient.ServerResponse initialGreeting = client.readResponse();
+        Assertions.assertThat(getSASLMechanisms(initialGreeting)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER");
+
+        client.sendCommand("CAPABILITY");
+        ManageSieveClient.ServerResponse capabilityResponse = client.readResponse();
+        Assertions.assertThat(getSASLMechanisms(capabilityResponse)).containsExactlyInAnyOrder("PLAIN", "XOAUTH2", "OAUTHBEARER");
+    }
+
+    private String[] getSASLMechanisms(ManageSieveClient.ServerResponse response) {
+        String saslLine = Assertions.assertThat(response.responseLines())
+            .filteredOn(line -> line.startsWith("\"SASL\""))
+            .hasSize(1)
+            .first()
+            .actual();
+        return saslLine.substring("\"SASL\" \"".length(), saslLine.length() - 1).split(" ");
+    }
+}
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
new file mode 100644
index 0000000000..e712489b70
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
@@ -0,0 +1,104 @@
+/****************************************************************
+ * 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.managesieveserver;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Optional;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.commons.net.SocketClient;
+import org.apache.commons.net.io.CRLFLineReader;
+
+public class ManageSieveClient extends SocketClient {
+    private static final String ENCODING = StandardCharsets.UTF_8.name();
+
+    enum ResponseType {
+        BYE,
+        NO,
+        OK;
+    }
+
+    record ServerResponse(
+        ResponseType responseType,
+        Optional<String> responseCode,
+        Optional<String> explanation,
+        ArrayList<String> responseLines
+    ) {}
+
+    private BufferedReader reader;
+    private BufferedWriter writer;
+
+    @Override
+    protected void _connectAction_() throws IOException {
+        super._connectAction_();
+        this.reader = new CRLFLineReader(new InputStreamReader(_input_, ENCODING));
+        this.writer = new BufferedWriter(new OutputStreamWriter(_output_, ENCODING));
+    }
+
+    @Override
+    public void disconnect() throws IOException {
+        super.disconnect();
+        this.reader = null;
+        this.writer = null;
+    }
+
+    public ServerResponse readResponse() throws IOException {
+        ServerResponse response = null;
+        ArrayList<String> lines = new ArrayList<>();
+        while (response == null) {
+            String line = this.reader.readLine();
+            String[] tokens = line.split(" ", 3);
+            if (EnumUtils.isValidEnumIgnoreCase(ResponseType.class, tokens[0])) {
+                ResponseType responseType = EnumUtils.getEnumIgnoreCase(ResponseType.class, tokens[0]);
+                Optional<String> responseCode = Optional.empty();
+                Optional<String> explanation = Optional.empty();
+                if (tokens.length == 2 && tokens[1].startsWith("(")) {
+                    responseCode = Optional.of(tokens[1].substring(1, tokens[1].length() - 1));
+                } else if (tokens.length == 2 && !tokens[1].startsWith("(")) {
+                    explanation = Optional.of(tokens[1]);
+                } else if (tokens.length == 3 && tokens[1].startsWith("(")) {
+                    responseCode = Optional.of(tokens[1].substring(1, tokens[1].length() - 1));
+                    explanation = Optional.of(tokens[2]);
+                } else if (tokens.length == 3 && !tokens[1].startsWith("(")) {
+                    explanation = Optional.of(tokens[1] + " " + tokens[2]);
+                }
+                if (explanation.isPresent() && explanation.get().charAt(0) == '"' && explanation.get().charAt(explanation.get().length() - 1) == '"') {
+                    explanation = Optional.of(explanation.get().substring(1, explanation.get().length() - 1));
+                }
+
+                response = new ServerResponse(responseType, responseCode, explanation, lines);
+            } else {
+                lines.addLast(line);
+            }
+        }
+        return response;
+    }
+
+    public void sendCommand(String command) throws IOException {
+        this.writer.write(command + "\r\n");
+        this.writer.flush();
+    }
+}
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
new file mode 100644
index 0000000000..185c40e48e
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveServerTestSystem.java
@@ -0,0 +1,93 @@
+/****************************************************************
+ * 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.managesieveserver;
+
+import java.net.InetAddress;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.core.Username;
+import org.apache.james.domainlist.api.DomainList;
+import org.apache.james.filesystem.api.mock.MockFileSystem;
+import org.apache.james.managesieve.core.CoreProcessor;
+import org.apache.james.managesieve.jsieve.Parser;
+import org.apache.james.managesieve.transcode.ArgumentParser;
+import org.apache.james.managesieve.transcode.ManageSieveProcessor;
+import org.apache.james.managesieveserver.netty.ManageSieveServer;
+import org.apache.james.protocols.api.utils.ProtocolServerUtils;
+import org.apache.james.server.core.configuration.FileConfigurationProvider;
+import org.apache.james.sieverepository.file.SieveFileRepository;
+import org.apache.james.user.memory.MemoryUsersRepository;
+
+class ManageSieveServerTestSystem {
+    private static final int MAX_LINE_LENGTH = 8000;
+    private static final DomainList NO_DOMAIN_LIST = null;
+    public static final String PASSWORD = "bobpwd";
+    public static final Username USERNAME = Username.of("bob");
+
+
+    private ManageSieveProcessor manageSieveProcessor;
+    public ManageSieveServer manageSieveServer;
+    private MemoryUsersRepository usersRepository;
+    private MockFileSystem fileSystem;
+
+    public ManageSieveServerTestSystem() throws Exception {
+        this.usersRepository = MemoryUsersRepository.withoutVirtualHosting(NO_DOMAIN_LIST);
+        this.usersRepository.addUser(USERNAME, PASSWORD);
+        this.fileSystem = new MockFileSystem();
+        this.manageSieveProcessor = new ManageSieveProcessor(
+            new ArgumentParser(
+                new CoreProcessor(
+                    new SieveFileRepository(this.fileSystem),
+                    this.usersRepository,
+                    new Parser()
+                )
+            )
+        );
+    }
+
+    public void setUp(HierarchicalConfiguration<ImmutableNode> configuration) throws Exception {
+        this.fileSystem.clear();
+        this.manageSieveServer = new ManageSieveServer(
+            MAX_LINE_LENGTH,
+            this.manageSieveProcessor
+        );
+        this.manageSieveServer.setFileSystem(this.fileSystem);
+        this.manageSieveServer.configure(configuration);
+        this.manageSieveServer.init();
+    }
+
+    public void setUp(String configFilePath) throws Exception {
+        HierarchicalConfiguration<ImmutableNode> configuration = FileConfigurationProvider.getConfig(ClassLoader.getSystemResourceAsStream(configFilePath));
+        setUp(configuration);
+    }
+
+    public void setUp() throws Exception {
+        setUp("managesieveserver.xml");
+    }
+
+    public InetAddress getBindedIP() {
+        return new ProtocolServerUtils(this.manageSieveServer).retrieveBindedAddress().getAddress();
+    }
+
+    public int getBindedPort() {
+        return new ProtocolServerUtils(this.manageSieveServer).retrieveBindedAddress().getPort();
+    }
+}
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
new file mode 100644
index 0000000000..d331c24f71
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
@@ -0,0 +1,568 @@
+/****************************************************************
+ * 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.managesieveserver;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.jwt.OidcTokenFixture;
+import org.apache.james.protocols.api.OIDCSASLHelper;
+import org.apache.james.protocols.lib.mock.ConfigLoader;
+import org.apache.james.util.ClassLoaderUtils;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.mockserver.integration.ClientAndServer;
+import org.mockserver.model.HttpRequest;
+import org.mockserver.model.HttpResponse;
+
+public class OIDCTest {
+    private static final String DISCOVERY_URI_PATH = "/oidc/.well-known/openid-configuration";
+    private static final String JWKS_URI_PATH = "/oidc/jwks";
+    private static final String INTROSPECTION_URI_PATH = "/oidc/introspect";
+    private static final String SCOPE = "scope";
+    private static final String USERINFO_URI_PATH = "/oidc/userinfo";
+    public static final String VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE = OIDCSASLHelper.generateEncodedXOauth2InitialClientResponse(
+        OidcTokenFixture.USER_EMAIL_ADDRESS,
+        OidcTokenFixture.VALID_TOKEN
+    );
+    public static final String VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE = OIDCSASLHelper.generateEncodedXOauth2InitialClientResponse(
+        OidcTokenFixture.USER_EMAIL_ADDRESS,
+        OidcTokenFixture.VALID_TOKEN
+    );
+    public static final String INVALID_XOAUTH2_INITIAL_CLIENT_RESPONSE = OIDCSASLHelper.generateEncodedXOauth2InitialClientResponse(
+        OidcTokenFixture.USER_EMAIL_ADDRESS,
+        OidcTokenFixture.INVALID_TOKEN
+    );
+    public static final String INVALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE = OIDCSASLHelper.generateEncodedXOauth2InitialClientResponse(
+        OidcTokenFixture.USER_EMAIL_ADDRESS,
+        OidcTokenFixture.INVALID_TOKEN
+    );
+
+    @Nested
+    @TestInstance(Lifecycle.PER_CLASS)
+    public class LocalValidation {
+        private ClientAndServer authServer;
+        private ManageSieveClient client;
+        private final ManageSieveServerTestSystem testSystem;
+        private final HierarchicalConfiguration<ImmutableNode> configuration;
+
+        public LocalValidation() throws Exception {
+            this.testSystem = new ManageSieveServerTestSystem();
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.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));
+            this.configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            this.configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            this.configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            this.configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            this.configuration.addProperty("oidc.scope", SCOPE);
+        }
+
+        @BeforeEach
+        void setUp() throws Exception {
+            this.testSystem.setUp(this.configuration);
+            this.client = new ManageSieveClient();
+            this.client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            this.client.readResponse();
+        }
+
+        @AfterEach
+        void tearDown() {
+            this.testSystem.manageSieveServer.destroy();
+        }
+
+        @AfterAll
+        void finalTearDown() {
+            this.authServer.stop();
+        }
+
+        @Test
+        void oauthbearerLoginWithValidTokenShouldSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void oauthbearerLoginWithValidTokenAndContinuationShouldSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\"");
+            ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+            this.client.sendCommand("\"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void oauthbearerLoginWithValidTokenAndContinuationCanBeAborted() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\"");
+            ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+            this.client.sendCommand("\"*\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+        }
+
+        @Test
+        void oauthbearerLoginWithInvalidTokenShouldNotSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + INVALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void xoauth2LoginWithValidTokenShouldSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"XOAUTH2\" \"" + VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void xoauth2LoginWithValidTokenAndContinuationShouldSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"XOAUTH2\"");
+            ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+            this.client.sendCommand("\"" + VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void xoauth2LoginWithValidTokenAndContinuationCanBeAborted() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"XOAUTH2\"");
+            ManageSieveClient.ServerResponse continuationResponse = this.client.readResponse();
+            Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+            Assertions.assertThat(continuationResponse.responseLines()).containsExactly("\"\"");
+
+            this.client.sendCommand("\"*\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+            Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("authentication aborted");
+        }
+
+        @Test
+        void xoauth2LoginWithInvalidTokenShouldNotSucceed() throws Exception {
+            this.client.sendCommand("AUTHENTICATE \"XOAUTH2\" \"" + INVALID_XOAUTH2_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+    }
+
+    @Nested
+    public class Introspection {
+        private final ManageSieveServerTestSystem testSystem;
+        private ClientAndServer authServer;
+
+        public Introspection() throws Exception {
+            this.testSystem = new ManageSieveServerTestSystem();
+        }
+
+        @AfterEach
+        void tearDown() {
+            this.testSystem.manageSieveServer.destroy();
+            this.authServer.stop();
+        }
+
+        @Test
+        void oauthbearerShouldSucceedWhenIntrospectReturnsActiveUser() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenIntrospectReturnsInactiveUser() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"active\": false, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenIntrospectReturnsWrongActiveUser() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s-wrong\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenIntrospectDoesNotContainActiveField() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenIntrospectDoesNotContainUserField() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody("{\"active\": true}", StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenIntrospectEndpointErrors() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(500));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerIntrospectionValidationShouldFailWhenLocalValidationFails() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.authServer
+                .when(HttpRequest.request().withPath(JWKS_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(500));
+            HierarchicalConfiguration<ImmutableNode> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+    }
+
+    @Nested
+    public class Userinfo {
+        private final ManageSieveServerTestSystem testSystem;
+        private ClientAndServer authServer;
+
+        public Userinfo() throws Exception {
+            this.testSystem = new ManageSieveServerTestSystem();
+        }
+
+        @AfterEach
+        void tearDown() {
+            this.testSystem.manageSieveServer.destroy();
+            this.authServer.stop();
+        }
+
+        @Test
+        void oauthbearerShouldSucceedWhenUserinfoClaimMatches() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(USERINFO_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), USERINFO_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenUserinfoClaimDiffers() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(USERINFO_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"%s\": \"test\"}", OidcTokenFixture.CLAIM), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), USERINFO_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenUserinfoClaimIsMissing() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(USERINFO_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{}"), StandardCharsets.UTF_8));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), USERINFO_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerShouldFailWhenUserinfoErrors() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(USERINFO_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(500));
+            this.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> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), USERINFO_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+
+        @Test
+        void oauthbearerUserinfoValidationShouldFailWhenLocalValidationFails() throws Exception {
+            this.authServer = ClientAndServer.startClientAndServer(0);
+            this.authServer
+                .when(HttpRequest.request().withPath(USERINFO_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(200)
+                    .withHeader("Content-Type", "application/json")
+                    .withBody(String.format("{\"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+            this.authServer
+                .when(HttpRequest.request().withPath(JWKS_URI_PATH))
+                .respond(HttpResponse.response().withStatusCode(500));
+            HierarchicalConfiguration<ImmutableNode> configuration = ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream("managesieveserver.xml"));
+            configuration.addProperty("oidc.jwksURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), JWKS_URI_PATH));
+            configuration.addProperty("oidc.claim", OidcTokenFixture.CLAIM);
+            configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
+            configuration.addProperty("oidc.scope", SCOPE);
+            configuration.addProperty("oidc.userinfo.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), USERINFO_URI_PATH));
+            testSystem.setUp(configuration);
+
+            ManageSieveClient client = new ManageSieveClient();
+            client.connect(testSystem.getBindedIP(), testSystem.getBindedPort());
+            client.readResponse();
+
+            client.sendCommand("AUTHENTICATE \"OAUTHBEARER\" \"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            ManageSieveClient.ServerResponse authenticationResponse = client.readResponse();
+            Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
+        }
+    }
+}
diff --git a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
new file mode 100644
index 0000000000..9125d16891
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
@@ -0,0 +1,16 @@
+<managesieveserver enabled="true">
+    <jmxName>managesieveserver</jmxName>
+    <bind>0.0.0.0:4190</bind>
+
+    <connectionBacklog>200</connectionBacklog>
+    <connectiontimeout>360</connectiontimeout>
+    <connectionLimit>0</connectionLimit>
+    <connectionLimitPerIP>0</connectionLimitPerIP>
+
+    <oidc>
+        <jwksURL>http://127.0.0.1/realms/test/protocol/openid-connect/certs</jwksURL>
+        <claim>sub</claim>
+        <oidcConfigurationURL>https://127.0.0.1/realms/test/.well-known/openid-configuration</oidcConfigurationURL>
+        <scope>email</scope>
+    </oidc>
+</managesieveserver>
diff --git a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver.xml b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver.xml
new file mode 100644
index 0000000000..77cc6f3a2a
--- /dev/null
+++ b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver.xml
@@ -0,0 +1,9 @@
+<managesieveserver enabled="true">
+    <jmxName>managesieveserver</jmxName>
+    <bind>0.0.0.0:4190</bind>
+
+    <connectionBacklog>200</connectionBacklog>
+    <connectiontimeout>360</connectiontimeout>
+    <connectionLimit>0</connectionLimit>
+    <connectionLimitPerIP>0</connectionLimitPerIP>
+</managesieveserver>


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


(james-project) 17/17: refactor(managesieve): rebase on new oidc token validation

Posted by bt...@apache.org.
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 67f879c5975843f4c0ed30775d3b02fee3162aed
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Jan 5 13:45:24 2026 +0100

    refactor(managesieve): rebase on new oidc token validation
---
 .../org/apache/james/managesieve/api/Session.java  |  2 +-
 .../core/OAUTHAuthenticationProcessor.java         | 38 ++------------------
 .../james/managesieve/util/SettableSession.java    |  2 +-
 .../org/apache/james/jwt/OidcJwtTokenVerifier.java |  2 +-
 .../apache/james/jwt/OidcSASLConfiguration.java    |  7 ++--
 .../org/apache/james/jwt/OidcTokenFixture.java     |  1 +
 .../netty/ManageSieveChannelUpstreamHandler.java   |  2 +-
 .../managesieveserver/netty/ManageSieveServer.java |  2 +-
 .../james/managesieveserver/ManageSieveClient.java |  4 +--
 .../apache/james/managesieveserver/OIDCTest.java   | 42 ++++++++++++++++++----
 .../src/test/resources/managesieveserver-oidc.xml  |  4 +++
 11 files changed, 51 insertions(+), 55 deletions(-)

diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
index ca5ed2b2fa..9f1a058f19 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
@@ -23,8 +23,8 @@ package org.apache.james.managesieve.api;
 import java.util.Optional;
 
 import org.apache.james.core.Username;
+import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.api.commands.Authenticate;
-import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 public interface Session {
 
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
index ba925141a8..ebdfe25c33 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
@@ -24,16 +24,13 @@ import java.util.Optional;
 
 import org.apache.james.core.Username;
 import org.apache.james.jwt.OidcJwtTokenVerifier;
-import org.apache.james.jwt.introspection.IntrospectionEndpoint;
+import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.api.AuthenticationException;
 import org.apache.james.managesieve.api.AuthenticationProcessor;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.SyntaxException;
 import org.apache.james.protocols.api.OIDCSASLParser;
 import org.apache.james.protocols.api.OIDCSASLParser.OIDCInitialResponse;
-import org.apache.james.protocols.api.OidcSASLConfiguration;
-
-import reactor.core.publisher.Mono;
 
 public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
@@ -58,7 +55,7 @@ public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
         Optional<Username> authenticatedUserResult = Optional.empty();
         try {
-            authenticatedUserResult = validateToken(oidcInitialResponse.getToken());
+            authenticatedUserResult = new OidcJwtTokenVerifier(this.oidcConfiguration).validateToken(oidcInitialResponse.getToken());
         } catch (Exception e) {
             throw new AuthenticationException("Could not validate the JWT");
         }
@@ -75,35 +72,4 @@ public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
         return authenticatedUser;
     }
-
-    private Optional<Username> validateToken(String token) {
-        if (this.oidcConfiguration.isCheckTokenByIntrospectionEndpoint()) {
-            return validTokenWithIntrospection(token);
-        } else if (this.oidcConfiguration.isCheckTokenByUserinfoEndpoint()) {
-            return validTokenWithUserInfo(token);
-        } else {
-            return OidcJwtTokenVerifier.verifySignatureAndExtractClaim(token, this.oidcConfiguration.getJwksURL(), this.oidcConfiguration.getClaim())
-                .map(Username::of);
-        }
-    }
-
-    private Optional<Username> validTokenWithUserInfo(String token) {
-        return Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(token,
-                this.oidcConfiguration.getJwksURL(),
-                this.oidcConfiguration.getClaim(),
-                this.oidcConfiguration.getUserInfoEndpoint().orElseThrow()))
-            .blockOptional()
-            .map(Username::of);
-    }
-
-    private Optional<Username> validTokenWithIntrospection(String token) {
-        return Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(token,
-                this.oidcConfiguration.getJwksURL(),
-                this.oidcConfiguration.getClaim(),
-                this.oidcConfiguration.getIntrospectionEndpoint()
-                    .map(endpoint -> new IntrospectionEndpoint(endpoint, this.oidcConfiguration.getIntrospectionEndpointAuthorization()))
-                    .orElseThrow()))
-            .blockOptional()
-            .map(Username::of);
-    }
 }
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
index f689ffcc2e..204a39e688 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
@@ -23,9 +23,9 @@ package org.apache.james.managesieve.util;
 import java.util.Optional;
 
 import org.apache.james.core.Username;
+import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.commands.Authenticate;
-import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 public class SettableSession implements Session {
 
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 7f87132bb7..28af294b64 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
@@ -104,7 +104,7 @@ public class OidcJwtTokenVerifier {
     }
 
     @VisibleForTesting
-   Publisher<String> verifyWithUserinfo(String jwtToken, URL userinfoEndpoint) {
+    Publisher<String> verifyWithUserinfo(String jwtToken, URL userinfoEndpoint) {
         return Mono.fromCallable(() -> verifySignatureAndExtractClaim(jwtToken))
             .flatMap(optional -> optional.map(Mono::just).orElseGet(Mono::empty))
             .flatMap(claimResult -> Mono.from(CHECK_TOKEN_CLIENT.userInfo(userinfoEndpoint, jwtToken))
diff --git a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
index cb59ef1811..0fbc52f46e 100644
--- a/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
+++ b/server/protocols/jwt/src/main/java/org/apache/james/jwt/OidcSASLConfiguration.java
@@ -37,9 +37,6 @@ import com.google.common.base.Preconditions;
 public class OidcSASLConfiguration {
     private static final Logger LOGGER = LoggerFactory.getLogger(OidcSASLConfiguration.class);
 
-    private static final boolean FORCE_INTROSPECT = Boolean.parseBoolean(System.getProperty("james.sasl.oidc.force.introspect", "true"));
-    private static final boolean VALIDATE_AUD = Boolean.parseBoolean(System.getProperty("james.sasl.oidc.validate.aud", "true"));
-
     @VisibleForTesting
     static Builder builder() {
         return new Builder();
@@ -140,7 +137,7 @@ public class OidcSASLConfiguration {
         String aud = configuration.getString("aud", null);
 
         if (introspectionUrl == null) {
-            if (FORCE_INTROSPECT) {
+            if (Boolean.parseBoolean(System.getProperty("james.sasl.oidc.force.introspect", "true"))) {
                 throw new IllegalArgumentException("'introspection.url' is mandatory for secure set up. Disable this check with -Djames.sasl.oidc.force.introspect=false.");
             } else {
                 LOGGER.warn("'introspection.url' is mandatory for secure set up. This check was disabled with -Djames.sasl.oidc.force.introspect=false.");
@@ -148,7 +145,7 @@ public class OidcSASLConfiguration {
         }
 
         if (aud == null) {
-            if (VALIDATE_AUD) {
+            if (Boolean.parseBoolean(System.getProperty("james.sasl.oidc.validate.aud", "true"))) {
                 throw new IllegalArgumentException("'aud' is mandatory for secure set up. Disable this check with -Djames.sasl.oidc.validate.aud=false.");
             } else {
                 LOGGER.warn("'aud' is mandatory for secure set up. This check was disabled with -Djames.sasl.oidc.validate.aud=false.");
diff --git a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
index dcf09137ba..26b1d186e0 100644
--- a/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
+++ b/server/protocols/jwt/src/test/java/org/apache/james/jwt/OidcTokenFixture.java
@@ -108,6 +108,7 @@ public class OidcTokenFixture {
 
     public static final String CLAIM = "email_address";
     public static final String USER_EMAIL_ADDRESS = "user@domain.org";
+    public static final String AUDIENCE = "account";
     public static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Inc4MFBzNUlhc24tYUdXbXcyVHJ4RGlOY2FocEgyc1h6NXBxZGhBbDlIWGMifQ.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5 [...]
     public static final String VALID_TOKEN_HAS_NOT_KID = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5vbmNlIjoiNWUyOGJjNTAtODE5NS00NjM3LThmMWEtYWUzNWFlYTk0NTc1I [...]
     public static final String VALID_TOKEN_HAS_NOT_FOUND_KID = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5vdEZvdW5kIn0.eyJleHAiOjM5Mzk1MDYxNjcsImlhdCI6MTYzOTUwNTg2NywiYXV0aF90aW1lIjozNjM5NTA1ODQxLCJqdGkiOiJjMjQ5ZTBkNi1jY2JiLTRmZDAtODI5Yi04OTM1MjczN2YzZGIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvYXV0aC9yZWFsbXMvcmVhbG0xIiwiYXVkIjoiYWNjb3VudCIsInN1YiI6IjIwNDUyNzFiLWMxYmItNDJiOC1hMTkwLThlYWI1MmYzYmEwOSIsInR5cCI6IkJlYXJlciIsImF6cCI6ImFjY291bnQtY29uc29sZSIsIm5vbmNlIjoiNWUyOGJjNTAtODE5NS00 [...]
diff --git a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
index 0e6454ad80..134d1b375c 100644
--- a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
+++ b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
@@ -23,12 +23,12 @@ import java.io.Closeable;
 import java.net.InetSocketAddress;
 import java.util.Optional;
 
+import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.SessionTerminatedException;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
 import org.apache.james.managesieve.transcode.NotEnoughDataException;
 import org.apache.james.managesieve.util.SettableSession;
-import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.apache.james.protocols.api.ProxyInformation;
 import org.apache.james.protocols.netty.Encryption;
 import org.slf4j.Logger;
diff --git a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
index 3c52a0f6c7..7e1f55af7c 100644
--- a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
+++ b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
@@ -26,8 +26,8 @@ import java.util.Optional;
 import org.apache.commons.configuration2.HierarchicalConfiguration;
 import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.tree.ImmutableNode;
+import org.apache.james.jwt.OidcSASLConfiguration;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
-import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.netty.AbstractChannelPipelineFactory;
 import org.apache.james.protocols.netty.AllButStartTlsLineChannelHandlerFactory;
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
index c4f042b7ba..0f1e7cb9e1 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/ManageSieveClient.java
@@ -92,8 +92,8 @@ public class ManageSieveClient extends SocketClient {
 
                 response = new ServerResponse(responseType, responseCode, explanation, lines);
             } else if (tokens[0].equals("+")) {
-                Optional explanation = Optional.of(tokens[1].substring(1, tokens[1].length() - 1));
-                response = new ServerResponse(ResponseType.CONTINUATION, Optional.empty(), explanation, new ArrayList());
+                Optional<String> explanation = Optional.of(tokens[1].substring(1, tokens[1].length() - 1));
+                response = new ServerResponse(ResponseType.CONTINUATION, Optional.empty(), explanation, new ArrayList<String>());
             } else {
                 lines.addLast(line);
             }
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
index 87ca3dbffe..fd2cb0b282 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
@@ -30,6 +30,7 @@ import org.apache.james.util.ClassLoaderUtils;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
@@ -85,6 +86,12 @@ public class OIDCTest {
             this.configuration.addProperty("oidc.scope", SCOPE);
         }
 
+        @BeforeAll
+        void initialSetup() {
+            System.setProperty("james.sasl.oidc.force.introspect", "false");
+            System.setProperty("james.sasl.oidc.validate.aud", "false");
+        }
+
         @BeforeEach
         void setUp() throws Exception {
             this.testSystem.setUp(this.configuration);
@@ -99,8 +106,10 @@ public class OIDCTest {
         }
 
         @AfterAll
-        void finalTearDown() {
+        void finalTeardown() {
             this.authServer.stop();
+            System.clearProperty("james.sasl.oidc.force.introspect");
+            System.clearProperty("james.sasl.oidc.validate.aud");
         }
 
         @Test
@@ -204,7 +213,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\", \"aud\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS, OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
@@ -216,6 +225,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -234,7 +244,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody(String.format("{\"active\": false, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"active\": false, \"%s\": \"%s\", \"aud\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS, OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
@@ -246,6 +256,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -264,7 +275,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody(String.format("{\"active\": true, \"%s\": \"%s-wrong\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s-wrong\", \"aud\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS, OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
@@ -276,6 +287,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -294,7 +306,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody(String.format("{\"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"%s\": \"%s\", \"aud\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS, OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
@@ -306,6 +318,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -324,7 +337,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody("{\"active\": true}", StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"active\": true, \"aud\": \"%s\"}", OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
@@ -336,6 +349,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -364,6 +378,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -382,7 +397,7 @@ public class OIDCTest {
                 .when(HttpRequest.request().withPath(INTROSPECTION_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(200)
                     .withHeader("Content-Type", "application/json")
-                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS), StandardCharsets.UTF_8));
+                    .withBody(String.format("{\"active\": true, \"%s\": \"%s\", \"aud\": \"%s\"}", OidcTokenFixture.CLAIM, OidcTokenFixture.USER_EMAIL_ADDRESS, OidcTokenFixture.AUDIENCE), StandardCharsets.UTF_8));
             this.authServer
                 .when(HttpRequest.request().withPath(JWKS_URI_PATH))
                 .respond(HttpResponse.response().withStatusCode(500));
@@ -392,6 +407,7 @@ public class OIDCTest {
             configuration.addProperty("oidc.oidcConfigurationURL", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), DISCOVERY_URI_PATH));
             configuration.addProperty("oidc.scope", SCOPE);
             configuration.addProperty("oidc.introspection.url", String.format("http://127.0.0.1:%s%s", this.authServer.getLocalPort(), INTROSPECTION_URI_PATH));
+            configuration.addProperty("oidc.aud", OidcTokenFixture.AUDIENCE);
             testSystem.setUp(configuration);
 
             ManageSieveClient client = new ManageSieveClient();
@@ -413,12 +429,24 @@ public class OIDCTest {
             this.testSystem = new ManageSieveServerTestSystem();
         }
 
+        @BeforeAll
+        static void initialSetup() {
+            System.setProperty("james.sasl.oidc.force.introspect", "false");
+            System.setProperty("james.sasl.oidc.validate.aud", "false");
+        }
+
         @AfterEach
         void tearDown() {
             this.testSystem.manageSieveServer.destroy();
             this.authServer.stop();
         }
 
+        @AfterAll
+        static void finalTeardown() {
+            System.clearProperty("james.sasl.oidc.force.introspect");
+            System.clearProperty("james.sasl.oidc.validate.aud");
+        }
+
         @Test
         void oauthbearerShouldSucceedWhenUserinfoClaimMatches() throws Exception {
             this.authServer = ClientAndServer.startClientAndServer(0);
diff --git a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
index 9125d16891..9ed26d0140 100644
--- a/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
+++ b/server/protocols/protocols-managesieve/src/test/resources/managesieveserver-oidc.xml
@@ -12,5 +12,9 @@
         <claim>sub</claim>
         <oidcConfigurationURL>https://127.0.0.1/realms/test/.well-known/openid-configuration</oidcConfigurationURL>
         <scope>email</scope>
+        <introspection>
+            <url>https://127.0.0.1/oidc/introspect</url>
+        </introspection>
+        <aud>james</aud>
     </oidc>
 </managesieveserver>


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


(james-project) 09/17: refactor(examples): small improvements for oidc example

Posted by bt...@apache.org.
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 24edc560f16c56dfddc6eca0852a85f808ba1235
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Sep 8 15:08:46 2025 +0200

    refactor(examples): small improvements for oidc example
    
    - More modern syntax in compose file.
    - Remove non-working links from readme.
    - Use consistent container names (always ending in .example.com).
    - Reduce output of test script.
---
 examples/oidc/README.md                            | 14 +++----
 examples/oidc/apisix/conf/apisix.yaml              |  2 +-
 examples/oidc/{docker-compose.yml => compose.yaml} | 43 ++++++++++------------
 examples/oidc/james/usersrepository.xml            |  2 +-
 examples/oidc/test.sh                              | 33 +++++++++--------
 5 files changed, 46 insertions(+), 48 deletions(-)

diff --git a/examples/oidc/README.md b/examples/oidc/README.md
index 08d7eae74e..8fe5e13e92 100644
--- a/examples/oidc/README.md
+++ b/examples/oidc/README.md
@@ -6,12 +6,10 @@ This is example of an OIDC setup with James.
 
 The API Gateway for example is [Apisix](https://apisix.apache.org/), we can use Apisix for websocket gateway, horizontal scaling, etc...
 
-This [docker-compose](docker-compose.yml) will start the following services:
+This [docker compose](./compose.yaml) will start the following services:
 
 - apisix: The image `linagora/apisix:3.2.0-debian-javaplugin` was created by Linagora. It based on `apisix:3.2.0-debian`, it already contain apisix plugin for 
   SLO (Single Logout) and rewrite the `X-User` header. 
-  - Dockerfile: [here](https://github.com/linagora/tmail-backend/blob/master/demo/apisix/Dockerfile)
-  - Project `tmail-apisix-plugin-runner`: [here](https://github.com/linagora/tmail-backend/tree/master/demo/apisix/tmail-apisix-plugin-runner)
   - Apisix being the OIDC gateway against James by exposing two endpoints:
       - `POST /jmap` for JMAP requests against James with normal authentication
       - `POST /oidc/jmap` for JMAP request against James with a JWT token issued by the LemonLDAP
@@ -161,21 +159,21 @@ Use websocket with endpoint `ws://apisix.example.com:9080/oidc/jmap/ws` and the
 
 We would use Thunderbird version 91.4.1 as a mail client (above versions should work).
 * Open `/thunderbird/omni.ja` in your host, find and modify `OAuth2Providers.jsm`:
-  * Add James hostname in kHostnames: `["localhost", ["james.local", "email"]],`
+  * Add James hostname in kHostnames: `["localhost", ["james.example.com", "email"]],`
   * Register using `james-thunderbird` Keycloak client in kIssuers:
   ```
   [
-    "james.local",
+    "james.example.com",
     [
       "james-thunderbird", //client_id from keycloak
       "Xw9ht1veTu0Tk5sMMy03PdzY3AiFvssw", // client_secret from keycloak
-      "http://keycloak.local:8080/auth/realms/oidc/protocol/openid-connect/auth",
-      "http://keycloak.local:8080/auth/realms/oidc/protocol/openid-connect/token",
+      "http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/auth",
+      "http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/token",
     ],
   ] 
   ```
 
-* Adding a line `127.0.0.1 keycloak.local` to your `/etc/hosts` so Thunderbird can resolve the address of keycloak.
+* Adding a line `127.0.0.1 sso.example.com` to your `/etc/hosts` so Thunderbird can resolve the address of keycloak.
 * Run Thunderbird, configure it using `james-user@localhost` account against these IMAP/SMTP settings:
   * IMAP: server: localhost, port: 143, connection security: No, authentication method: OAUTH2
     ![](_media/imap-setting.png)
diff --git a/examples/oidc/apisix/conf/apisix.yaml b/examples/oidc/apisix/conf/apisix.yaml
index cc8d14db34..cf27c1afe2 100644
--- a/examples/oidc/apisix/conf/apisix.yaml
+++ b/examples/oidc/apisix/conf/apisix.yaml
@@ -219,7 +219,7 @@ upstreams:
   -
     id: jmap_upstream
     nodes:
-      "james:80": 1
+      "james.example.com:80": 1
     type: roundrobin
 
 plugin_configs:
diff --git a/examples/oidc/docker-compose.yml b/examples/oidc/compose.yaml
similarity index 71%
rename from examples/oidc/docker-compose.yml
rename to examples/oidc/compose.yaml
index 31e0261d84..dc39fd20ba 100644
--- a/examples/oidc/docker-compose.yml
+++ b/examples/oidc/compose.yaml
@@ -1,5 +1,3 @@
-version: "3"
-
 services:
   apisix:
     container_name: apisix.example.com
@@ -8,11 +6,11 @@ services:
       - ./apisix/conf/apisix.yaml:/usr/local/apisix/conf/apisix.yaml
       - ./apisix/conf/config.yaml:/usr/local/apisix/conf/config.yaml
     environment:
-      - X_USER_SECRET=xusersecret123
+      X_USER_SECRET: xusersecret123
     networks:
       - james
     ports:
-      - "9080:9080/tcp"
+      - "127.0.0.1:9080:9080"
 
   james:
     depends_on:
@@ -20,10 +18,9 @@ services:
     networks:
       - james
     image: apache/james:memory-latest
-    container_name: james
-    hostname: james.local
-    command:
-      - --generate-keystore
+    container_name: james.example.com
+    hostname: james.example.com
+    command: [--generate-keystore]
     volumes:
       - ./james/usersrepository.xml:/root/conf/usersrepository.xml
       - ./james/jmap.properties:/root/conf/jmap.properties
@@ -31,14 +28,14 @@ services:
       - ./james/smtpserver.xml:/root/conf/smtpserver.xml
       - ./james/managesieveserver.xml:/root/conf/managesieveserver.xml
     ports:
-      - "8000:8000"
-      - "143:143"
-      - "587:587"
-      - "4190:4190"
+      - "127.0.0.1:8000:8000"
+      - "127.0.0.1:143:143"
+      - "127.0.0.1:587:587"
+      - "127.0.0.1:4190:4190"
     healthcheck:
       test: ["CMD", "curl", "-f", "http://james:8000/domains"]
 
-  sso.example.com:
+  sso:
     depends_on:
       - ldap
     image: quay.io/keycloak/keycloak:16.1.0
@@ -46,25 +43,25 @@ services:
     volumes:
       - ./keycloak/realm-oidc.json:/tmp/realm-oidc.json
     ports:
-      - "8080:8080"
+      - "127.0.0.1:8080:8080"
     environment:
-      - KEYCLOAK_USER=admin
-      - KEYCLOAK_PASSWORD=admin
-      - KEYCLOAK_IMPORT=/tmp/realm-oidc.json
+      KEYCLOAK_USER: admin
+      KEYCLOAK_PASSWORD: admin
+      KEYCLOAK_IMPORT: /tmp/realm-oidc.json
     networks:
       - james
 
   ldap:
-    container_name: ldap
+    container_name: ldap.example.com
     image: osixia/openldap:1.5.0
     ports:
-      - "389:389"
-      - "636:636"
+      - "127.0.0.1:389:389"
+      - "127.0.0.1:636:636"
     command: [--copy-service]
     volumes:
       - ./ldap/populate.ldif:/container/service/slapd/assets/config/bootstrap/ldif/data.ldif
     environment:
-      - LDAP_DOMAIN=localhost
+      LDAP_DOMAIN: localhost
     networks:
       - james
 
@@ -75,7 +72,7 @@ services:
     networks:
       - james
     ports:
-      - "6379:6379"
+      - "127.0.0.1:6379:6379"
 
 networks:
-  james:
\ No newline at end of file
+  james:
diff --git a/examples/oidc/james/usersrepository.xml b/examples/oidc/james/usersrepository.xml
index a0c316db38..3f348fd41c 100644
--- a/examples/oidc/james/usersrepository.xml
+++ b/examples/oidc/james/usersrepository.xml
@@ -22,7 +22,7 @@
 
 <usersrepository name="LocalUsers"
                  class="org.apache.james.user.ldap.ReadOnlyUsersLDAPRepository"
-                 ldapHost="ldap://ldap:389"
+                 ldapHost="ldap://ldap.example.com:389"
                  principal="cn=admin,dc=localhost"
                  credentials="admin"
                  userBase="ou=people,dc=localhost"
diff --git a/examples/oidc/test.sh b/examples/oidc/test.sh
index 5477975955..c956aea0e3 100755
--- a/examples/oidc/test.sh
+++ b/examples/oidc/test.sh
@@ -1,8 +1,10 @@
 #!/bin/sh
 
-set -eux
+# You need to start the compose project before running this script!
 
-docker-compose up -d
+set -eu
+# Uncomment the following line to print more information.
+# set -x
 
 GET_TOKEN_RESPONSE=`curl --location 'http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/token' \
                                   --header 'Content-Type: application/x-www-form-urlencoded' \
@@ -14,7 +16,9 @@ GET_TOKEN_RESPONSE=`curl --location 'http://sso.example.com:8080/auth/realms/oid
                                   --data-urlencode 'password=secret' 2>/dev/null`
 
 ACCESS_TOKEN=`echo $GET_TOKEN_RESPONSE 2>/dev/null |perl -pe 's/^.*"access_token"\s*:\s*"(.*?)".*$/$1/'`
+echo "Access token: $ACCESS_TOKEN"
 REFRESH_TOKEN=`echo $GET_TOKEN_RESPONSE 2>/dev/null |perl -pe 's/^.*"refresh_token"\s*:\s*"(.*?)".*$/$1/'`
+echo "Refresh token: $REFRESH_TOKEN"
 
 echo "Got an access_token"
 if curl -H "Authorization: Bearer $ACCESS_TOKEN" http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/userinfo 2>/dev/null| grep james-user >/dev/null; then
@@ -23,8 +27,7 @@ else
 	echo "ACCESS_TOKEN VERIFICATION FAILED"
 fi
 
-echo -n "Trying James: "
-
+echo -n "Trying James:"
 APISIX_JMAP_ENDPOINT=apisix.example.com:9080/oidc/jmap/session
 if curl -v -H 'Accept: application/json; jmapVersion=rfc-8621' -H "Authorization: Bearer $ACCESS_TOKEN" $APISIX_JMAP_ENDPOINT 2>/dev/null | grep uploadUrl >/dev/null; then
 	echo "OK"
@@ -33,45 +36,45 @@ else
 fi
 
 XOAUTH2_INITIAL_CLIENT_RESPONSE=`echo -n -e "user=james-user@localhost\x01auth=Bearer ${ACCESS_TOKEN}\x01\x01" | base64 -w 0`
+echo "XOAUTH2: $XOAUTH2_INITIAL_CLIENT_RESPONSE"
 OAUTHBEARER_INITIAL_CLIENT_RESPONSE=`echo -n -e "n,a=james-user@localhost\x01auth=Bearer ${ACCESS_TOKEN}\x01\x01" | base64 -w 0`
+echo "OAUTHBEARER: $OAUTHBEARER_INITIAL_CLIENT_RESPONSE"
 
-set +x
-MANAGESIEVE_XOAUTH2_RESPONSE=`(echo "AUTHENTICATE \"XOAUTH2\" \"${XOAUTH2_INITIAL_CLIENT_RESPONSE}\""; echo "CAPABILITY"; echo "LOGOUT"; sleep 3) | telnet localhost 4190`
-if echo $MANAGESIEVE_XOAUTH2_RESPONSE | grep "\"OWNER\" \"james-user@localhost\"" > /dev/null; then
+MANAGESIEVE_XOAUTH2_RESPONSE=`(echo "AUTHENTICATE \"XOAUTH2\" \"${XOAUTH2_INITIAL_CLIENT_RESPONSE}\""; echo "CAPABILITY"; echo "LOGOUT"; sleep 3) | telnet 127.0.0.1 4190`
+if echo "$MANAGESIEVE_XOAUTH2_RESPONSE" | grep "\"OWNER\" \"james-user@localhost\"" > /dev/null; then
 	echo "Success: Managesieve XOAUTH2 login"
 else
 	echo "Error: Managesieve XOAUTH2 login"
 fi
-if echo $MANAGESIEVE_XOAUTH2_RESPONSE | grep "OK channel is closing" > /dev/null; then
+if echo "$MANAGESIEVE_XOAUTH2_RESPONSE" | grep "OK channel is closing" > /dev/null; then
 	echo "Success: Managesieve XOAUTH2 logout"
 else
 	echo "Error: Managesieve XOAUTH2 logout"
 fi
 
-IMAP_XOAUTH2_RESPONSE=`(echo "a AUTHENTICATE XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "c LOGOUT"; sleep 3) | telnet localhost 143`
-if echo $IMAP_XOAUTH2_RESPONSE | grep "a OK AUTHENTICATE completed" > /dev/null; then
+IMAP_XOAUTH2_RESPONSE=`(echo "a AUTHENTICATE XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "c LOGOUT"; sleep 3) | telnet 127.0.0.1 143`
+if echo "$IMAP_XOAUTH2_RESPONSE" | grep "a OK AUTHENTICATE completed" > /dev/null; then
 	echo "Success: IMAP XOAUTH2 login"
 else
 	echo "Error: IMAP XOAUTH2 login"
 fi
-if echo $IMAP_XOAUTH2_RESPONSE | grep "c OK LOGOUT completed" > /dev/null; then
+if echo "$IMAP_XOAUTH2_RESPONSE" | grep "c OK LOGOUT completed" > /dev/null; then
 	echo "Success: IMAP XOAUTH2 logout"
 else
 	echo "Error: IMAP XOAUTH2 logout"
 fi
 
-SMTP_XOAUTH2_RESPONSE=`(echo "AUTH XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "QUIT"; sleep 3) | telnet localhost 587`
-if echo $SMTP_XOAUTH2_RESPONSE | grep "235 Authentication successful" > /dev/null; then
+SMTP_XOAUTH2_RESPONSE=`(echo "AUTH XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "QUIT"; sleep 3) | telnet 127.0.0.1 587`
+if echo "$SMTP_XOAUTH2_RESPONSE" | grep "235 Authentication successful" > /dev/null; then
 	echo "Success: SMTP XOAUTH2 login"
 else
 	echo "Error: SMTP XOAUTH2 login"
 fi
-if echo $SMTP_XOAUTH2_RESPONSE | grep "221 2.0.0 james.local Service closing transmission channel" > /dev/null; then
+if echo "$SMTP_XOAUTH2_RESPONSE" | grep "221 2.0.0 james.example.com Service closing transmission channel" > /dev/null; then
 	echo "Success: SMTP XOAUTH2 logout"
 else
 	echo "Error: SMTP XOAUTH2 logout"
 fi
-set -x
 
 # Logout
 


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


(james-project) 04/17: fix(managesieve): small fixes in authentication logic

Posted by bt...@apache.org.
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 82fa8fce18c9ee11e36fbdc0c4a571606dfdcb16
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Fri Aug 15 07:57:19 2025 +0200

    fix(managesieve): small fixes in authentication logic
    
    - Authentication is now aborted when session is already authenticated
    - Authentication is now aborted when client sends "*"
    - Arguments by the client are now consistently unquoted
    - If unquotation fails, authentication is aborted to prevent later NullPointerExceptions
    - Fixed NullPointerException on AuthenticationException
    - Server now sends correctly formatted response (including code) when waiting for
      additional authentication arguments
    - When client sends authentication credentials in second message, the first argument
      (command) is now interpreted as argument
    - When choosing mechanism failed, a proper error is returned to the client
    - When argument is not quoted, authentication is aborted
    
    I think with these changes, the server now behaves more like described in the RFC:
    https://datatracker.ietf.org/doc/html/rfc5804#section-4
---
 .../org/apache/james/managesieve/core/CoreProcessor.java   | 14 ++++++++++++--
 .../managesieve/core/OAUTHAuthenticationProcessor.java     |  2 +-
 .../managesieve/core/PlainAuthenticationProcessor.java     |  2 +-
 .../james/managesieve/transcode/ManageSieveProcessor.java  | 13 ++++++++++---
 4 files changed, 24 insertions(+), 7 deletions(-)

diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index 3417fff3cd..ebb8b7e36d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -210,6 +210,9 @@ public class CoreProcessor implements CoreCommands {
 
     @Override
     public String chooseMechanism(Session session, String mechanism) {
+        if (session.isAuthenticated()) {
+            return "NO \"already authenticated\"";
+        }
         try {
             if (Strings.isNullOrEmpty(mechanism)) {
                 return "NO ManageSieve syntax is incorrect : You must specify a SASL mechanism as an argument of AUTHENTICATE command";
@@ -234,7 +237,14 @@ public class CoreProcessor implements CoreCommands {
         try {
             SupportedMechanism currentAuthenticationMechanism = session.getChoosedAuthenticationMechanism();
             AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(currentAuthenticationMechanism);
-            Username authenticatedUsername = authenticationProcessor.isAuthenticationSuccesfull(session, suppliedData);
+            String unquotedSuppliedData = ParserUtils.unquoteFirst(suppliedData);
+            if (unquotedSuppliedData == null) {
+                return "NO \"authentication failed\"";
+            }
+            if (unquotedSuppliedData.equals("*")) {
+                return "NO \"authentication aborted\"";
+            }
+            Username authenticatedUsername = authenticationProcessor.isAuthenticationSuccesfull(session, unquotedSuppliedData);
             if (authenticatedUsername != null) {
                 session.setUser(authenticatedUsername);
                 session.setState(Session.State.AUTHENTICATED);
@@ -245,7 +255,7 @@ public class CoreProcessor implements CoreCommands {
                 return "NO authentication failed";
             }
         } catch (AuthenticationException e) {
-            return "NO Authentication failed with " + e.getCause().getClass() + " : " + e.getMessage();
+            return "NO Authentication failed with: " + e.getMessage();
         } catch (SyntaxException e) {
             return "NO ManageSieve syntax is incorrect : " + e.getMessage();
         }
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
index ba925141a8..e424d2759e 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/OAUTHAuthenticationProcessor.java
@@ -45,7 +45,7 @@ public class OAUTHAuthenticationProcessor implements AuthenticationProcessor {
 
     @Override
     public String initialServerResponse(Session session) {
-        return "+ \"\"";
+        return "\"\"\r\nOK";
     }
 
     @Override
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
index f59daedb9a..72837b37e6 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/PlainAuthenticationProcessor.java
@@ -50,7 +50,7 @@ public class PlainAuthenticationProcessor implements AuthenticationProcessor {
 
     @Override
     public String initialServerResponse(Session session) {
-        return "+ \"\"";
+        return "\"\"\r\nOK";
     }
 
 
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
index 94708c1761..d07b323c4d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
@@ -86,17 +86,24 @@ public class ManageSieveProcessor {
 
     private String matchCommandWithImplementation(Session session, String arguments, String command) throws SessionTerminatedException {
         if (session.getState() == Session.State.AUTHENTICATION_IN_PROGRESS) {
-            return argumentParser.authenticate(session, arguments);
+            return argumentParser.authenticate(session, command);
         }
         if (command.equalsIgnoreCase(AUTHENTICATE)) {
             if (StringUtils.countMatches(arguments, "\"") == 4) {
-                argumentParser.chooseMechanism(session, arguments);
+                String result = argumentParser.chooseMechanism(session, arguments);
+                if (session.getState() != Session.State.AUTHENTICATION_IN_PROGRESS) {
+                    return result;
+                }
                 int bracket1 = arguments.indexOf('\"');
                 int bracket2 = arguments.indexOf('\"', bracket1 + 1);
                 int bracket3 = arguments.indexOf('\"', bracket2 + 1);
                 int bracket4 = arguments.indexOf('\"', bracket3 + 1);
 
-                return argumentParser.authenticate(session, arguments.substring(bracket3 + 1, bracket4));
+                return argumentParser.authenticate(session, arguments.substring(bracket3, bracket4 + 1));
+            } else if (arguments.split(" ").length != 1) {
+                // The client send additional arguments but didn't quote them. It probably thinks that it does not need
+                // to send more, but the server expects more. Reject this authentication now to solve this conflict.
+                return "NO \"unquoted argument found\"";
             }
             return argumentParser.chooseMechanism(session, arguments);
         } else if (command.equalsIgnoreCase(CAPABILITY)) {


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


(james-project) 06/17: docs(managesieve): document usage of oidc authentication

Posted by bt...@apache.org.
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 b946db9fdd03dfee4b6c829afaef37c344d475d6
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Aug 25 16:22:00 2025 +0200

    docs(managesieve): document usage of oidc authentication
---
 docs/modules/servers/partials/configure/sieve.adoc | 26 +++++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/docs/modules/servers/partials/configure/sieve.adoc b/docs/modules/servers/partials/configure/sieve.adoc
index 7ecd4c452f..25f2219aff 100644
--- a/docs/modules/servers/partials/configure/sieve.adoc
+++ b/docs/modules/servers/partials/configure/sieve.adoc
@@ -86,4 +86,28 @@ Optional integer, defaults to 2 times the count of CPUs.
 | maxExecutorCount
 | Set the maximum count of worker threads. Worker threads takes care of potentially blocking tasks like executing ManageSieve commands.
 Optional integer, defaults to 16.
-|===
\ No newline at end of file
+
+| oidc
+| If this property is present, OIDC will be configured and the following properties are mandatory (unless otherwise specified).
+
+| oidc.oidcConfigurationURL
+| Your identity provider's OIDC discovery URL. This is currently not used for managesieve but is still required when OIDC is configured.
+
+| oidc.jwksURL
+| URL to the endpoint for the JSON Web Key Set of your provider. This is used to locally validate tokens.
+
+| oidc.claim
+| Name of the claim in the token you want to use as the identifier for the user (e.g. "email_address").
+
+| oidc.scope
+| OIDC scope. This is currently not used for managesieve but is still required when OIDC is configured.
+
+| oidc.introspection.url
+| URL to your identity provider's introspection endpoint. It is optional and if specified James will use the endpoint to validate the token in addition to local validation.
+
+| oidc.introspection.auth
+| Provide Authorization header for introspection requests (optional, e.g. `Basic xyz`).
+
+| oidc.userinfo.url
+| URL to your identity provider's userinfo endpoint. It is optional and if specified James will use the endpoint to validate the token in addition to local validation. James will ignore this option if `oidc.introspection.url` is already configured.
+|===


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


(james-project) 02/17: fix(oidc): correct differentiation between XOAUTH2 and OAUTHBEARER

Posted by bt...@apache.org.
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 06445aef14010bf788a6e8dac75867d320915b2e
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Thu Aug 14 20:42:27 2025 +0200

    fix(oidc): correct differentiation between XOAUTH2 and OAUTHBEARER
    
    XOAUTH2 is described here: https://developers.google.com/workspace/gmail/imap/xoauth2-protocol
    OAUTHBERER is described here: https://datatracker.ietf.org/doc/html/rfc5801#section-4
    
    There is a small difference in how the user argument is called and
    OAUTHBEARER also includes a GS2 header.
---
 .../apache/james/protocols/api/OIDCSASLParser.java | 13 ++++---
 .../apache/james/protocols/api/OIDCSASLHelper.java | 12 ++++++-
 .../james/imapserver/netty/IMAPServerTest.java     | 26 +++++++-------
 .../org/apache/james/smtpserver/SMTPSaslTest.java  | 40 +++++++++++-----------
 4 files changed, 53 insertions(+), 38 deletions(-)

diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java b/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
index eb2f556f75..72df061214 100644
--- a/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
+++ b/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
@@ -33,9 +33,11 @@ public class OIDCSASLParser {
     public static final char SASL_SEPARATOR = 1;
     public static final String PREFIX_TOKEN = "Bearer ";
     public static final String TOKEN_PART_PREFIX = "auth=";
-    public static final String USER_PART_PREFIX = "user=";
+    public static final String XOAUTH2_USER_PART_PREFIX = "user=";
+    public static final String OAUTHBEARER_USER_PART_PREFIX = "a=";
     public static final int TOKEN_PART_INDEX = TOKEN_PART_PREFIX.length();
-    public static final int USER_PART_INDEX = USER_PART_PREFIX.length();
+    public static final int XOAUTH2_USER_PART_INDEX = XOAUTH2_USER_PART_PREFIX.length();
+    public static final int OAUTHBEARER_USER_PART_INDEX = OAUTHBEARER_USER_PART_PREFIX.length();
 
     public static class OIDCInitialResponse {
         private final String associatedUser;
@@ -74,8 +76,11 @@ public class OIDCSASLParser {
                 if (stringToken.startsWith(TOKEN_PART_PREFIX)) {
                     tokenPart = StringUtils.replace(stringToken.substring(TOKEN_PART_INDEX), PREFIX_TOKEN, "");
                     tokenPartCounter++;
-                } else if (stringToken.startsWith(USER_PART_PREFIX)) {
-                    userPart = stringToken.substring(USER_PART_INDEX);
+                } else if (stringToken.startsWith(XOAUTH2_USER_PART_PREFIX)) {
+                    userPart = stringToken.substring(XOAUTH2_USER_PART_INDEX);
+                    userPartCounter++;
+                } else if (stringToken.startsWith(OAUTHBEARER_USER_PART_PREFIX)) {
+                    userPart = stringToken.substring(OAUTHBEARER_USER_PART_INDEX);
                     userPartCounter++;
                 }
             }
diff --git a/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
index 1436649a18..b806a40acb 100644
--- a/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
+++ b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
@@ -25,9 +25,19 @@ import java.util.Base64;
 import com.google.common.collect.ImmutableList;
 
 public class OIDCSASLHelper {
-    public static String generateOauthBearer(String username, String token) {
+    // See the XOAUTH2 specification athttps://developers.google.com/workspace/gmail/imap/xoauth2-protocol
+    // for details.
+    public static String generateEncodedXOauth2InitialClientResponse(String username, String token) {
         return Base64.getEncoder().encodeToString(String.join("" + OIDCSASLParser.SASL_SEPARATOR,
                 ImmutableList.of("user=" + username, "auth=Bearer " + token, "", ""))
             .getBytes(StandardCharsets.US_ASCII));
     }
+
+    // See the OAUTHBEARER specification at https://datatracker.ietf.org/doc/html/rfc5801#section-4
+    // for details.
+    public static String generateEncodedOauthbearerInitialClientResponse(String username, String token) {
+        return Base64.getEncoder().encodeToString(String.join("" + OIDCSASLParser.SASL_SEPARATOR,
+                ImmutableList.of("n,a=" + username, "auth=Bearer " + token, "", ""))
+            .getBytes(StandardCharsets.US_ASCII));
+    }
 }
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 e46c354152..5c390bf09c 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
@@ -1379,7 +1379,7 @@ class IMAPServerTest {
 
         @Test
         void oauthShouldSuccessWhenValidToken() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
@@ -1420,9 +1420,9 @@ class IMAPServerTest {
 
         @Test
         void oauthShouldSupportOAUTH2Type() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String xoauth2 = OIDCSASLHelper.generateEncodedXOauth2InitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
-            client.sendCommand("AUTHENTICATE XOAUTH2 " + oauthBearer);
+            client.sendCommand("AUTHENTICATE XOAUTH2 " + xoauth2);
             assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
         }
 
@@ -1441,7 +1441,7 @@ class IMAPServerTest {
 
         @Test
         void shouldNotOauthWhenAuthIsReady() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
@@ -1450,7 +1450,7 @@ class IMAPServerTest {
 
         @Test
         void appendShouldSuccessWhenAuthenticated() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient imapsClient = imapsClient(port);
             imapsClient.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             imapsClient.create("INBOX");
@@ -1487,7 +1487,7 @@ class IMAPServerTest {
 
             int port = imapServer.getListenAddresses().get(0).getPort();
 
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("NO AUTHENTICATE failed.");
@@ -1514,7 +1514,7 @@ class IMAPServerTest {
 
             int port = imapServer.getListenAddresses().get(0).getPort();
 
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
@@ -1539,7 +1539,7 @@ class IMAPServerTest {
 
             int port = imapServer.getListenAddresses().get(0).getPort();
 
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("NO AUTHENTICATE processing failed.");
@@ -1566,7 +1566,7 @@ class IMAPServerTest {
 
             int port = imapServer.getListenAddresses().get(0).getPort();
 
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
@@ -1592,7 +1592,7 @@ class IMAPServerTest {
 
             int port = imapServer.getListenAddresses().get(0).getPort();
 
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("NO AUTHENTICATE processing failed.");
@@ -1600,7 +1600,7 @@ class IMAPServerTest {
 
         @Test
         void oauthShouldImpersonateFailWhenNOTDelegated() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER3.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER3.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("NO AUTHENTICATE");
@@ -1608,7 +1608,7 @@ class IMAPServerTest {
 
         @Test
         void oauthShouldImpersonateSuccessWhenDelegated() throws Exception {
-            String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
+            String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
             IMAPSClient client = imapsClient(port);
             client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
             assertThat(client.getReplyString()).contains("OK AUTHENTICATE completed.");
@@ -1624,7 +1624,7 @@ class IMAPServerTest {
 
             // USER1 authenticate and impersonate as USER2
             try (TestIMAPClient client = new TestIMAPClient(imapsClient(port))) {
-                String oauthBearer = OIDCSASLHelper.generateOauthBearer(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
+                String oauthBearer = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
                 String authenticateResponse = client.sendCommand("AUTHENTICATE OAUTHBEARER " + oauthBearer);
                 assertThat(authenticateResponse).contains("OK AUTHENTICATE completed.");
 
diff --git a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java
index 0e8b515312..45bac66783 100644
--- a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java
+++ b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/SMTPSaslTest.java
@@ -61,8 +61,8 @@ class SMTPSaslTest {
     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);
+    public static final String VALID_OAUTHBEARER_TOKEN = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN);
+    public static final String INVALID_OAUTHBEARER_TOKEN = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.INVALID_TOKEN);
 
 
     private final SMTPServerTestSystem testSystem = new SMTPServerTestSystem();
@@ -115,7 +115,7 @@ class SMTPSaslTest {
     void oauthShouldSuccessWhenValidToken() throws Exception {
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
 
@@ -129,7 +129,7 @@ class SMTPSaslTest {
 
         client.sendCommand("AUTH OAUTHBEARER");
         assertThat(client.getReplyString()).contains("334");
-        client.sendCommand(VALID_TOKEN);
+        client.sendCommand(VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
 
@@ -143,7 +143,7 @@ class SMTPSaslTest {
 
         client.sendCommand("AUTH XOAUTH2");
         assertThat(client.getReplyString()).contains("334");
-        client.sendCommand(VALID_TOKEN);
+        client.sendCommand(OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN));
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
 
@@ -155,7 +155,7 @@ class SMTPSaslTest {
     void oauthShouldSupportXOAUTH2Type() throws Exception {
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH XOAUTH2 " + VALID_TOKEN);
+        client.sendCommand("AUTH XOAUTH2 " + OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER.asString(), OidcTokenFixture.VALID_TOKEN));
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
     }
@@ -171,7 +171,7 @@ class SMTPSaslTest {
             .as("Should not advertise OAUTHBEARER when no TLS connect.")
             .doesNotContain("OAUTHBEARER");
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
         assertThat(client.getReplyString()).contains("504 Unrecognized Authentication Type");
     }
 
@@ -179,7 +179,7 @@ class SMTPSaslTest {
     void oauthShouldFailWhenInvalidToken() throws Exception {
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + INVALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + INVALID_OAUTHBEARER_TOKEN);
         assertThat(client.getReplyString()).contains("334 " + FAIL_RESPONSE_TOKEN);
 
         client.sendCommand("AQ==");
@@ -192,7 +192,7 @@ class SMTPSaslTest {
 
         client.sendCommand("EHLO localhost");
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         client.setSender(USER.asString());
         client.addRecipient("mail@domain.org");
@@ -224,8 +224,8 @@ class SMTPSaslTest {
     void shouldNotOauthWhenAlreadyAuthenticated() throws Exception {
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("503 5.5.0 User has previously authenticated.  Further authentication is not required!");
     }
@@ -239,7 +239,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
         assertThat(client.getReplyString()).contains("504 Unrecognized Authentication Type");
     }
 
@@ -323,7 +323,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("334 " + FAIL_RESPONSE_TOKEN);
 
@@ -352,7 +352,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
     }
@@ -377,7 +377,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("451 Unable to process request");
     }
@@ -402,7 +402,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
     }
@@ -426,7 +426,7 @@ class SMTPSaslTest {
 
         SMTPSClient client = initSMTPSClient();
 
-        client.sendCommand("AUTH OAUTHBEARER " + VALID_TOKEN);
+        client.sendCommand("AUTH OAUTHBEARER " + VALID_OAUTHBEARER_TOKEN);
 
         assertThat(client.getReplyString()).contains("451 Unable to process request");
     }
@@ -434,7 +434,7 @@ class SMTPSaslTest {
     @Test
     void oauthShouldImpersonateFailWhenNOTDelegated() throws Exception {
         SMTPSClient client = initSMTPSClient();
-        String tokenWithImpersonation = OIDCSASLHelper.generateOauthBearer("another@domain.org", OidcTokenFixture.VALID_TOKEN);
+        String tokenWithImpersonation = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse("another@domain.org", OidcTokenFixture.VALID_TOKEN);
         client.sendCommand("AUTH OAUTHBEARER " + tokenWithImpersonation);
 
         assertThat(client.getReplyString()).contains("334 ");
@@ -446,7 +446,7 @@ class SMTPSaslTest {
     @Test
     void oauthShouldImpersonateSuccessWhenDelegated() throws Exception {
         SMTPSClient client = initSMTPSClient();
-        String tokenWithImpersonation = OIDCSASLHelper.generateOauthBearer(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
+        String tokenWithImpersonation = OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER2.asString(), OidcTokenFixture.VALID_TOKEN);
         client.sendCommand("AUTH OAUTHBEARER " + tokenWithImpersonation);
 
         assertThat(client.getReplyString()).contains("235 Authentication successful.");
@@ -458,7 +458,7 @@ class SMTPSaslTest {
 
         client.sendCommand("EHLO localhost");
 
-        client.sendCommand("AUTH OAUTHBEARER " + OIDCSASLHelper.generateOauthBearer(USER2.asString(), OidcTokenFixture.VALID_TOKEN));
+        client.sendCommand("AUTH OAUTHBEARER " + OIDCSASLHelper.generateEncodedOauthbearerInitialClientResponse(USER2.asString(), OidcTokenFixture.VALID_TOKEN));
 
         client.setSender(USER2.asString());
         client.addRecipient("mail@domain.org");


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


(james-project) 16/17: feat(managesieve): add additional test for usage of spaces as delimiters in SASL PLAIN

Posted by bt...@apache.org.
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 5c2ab8502e757c6e630c19a5ec215fb8f9fdfda8
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Dec 8 12:54:51 2025 +0100

    feat(managesieve): add additional test for usage of spaces as delimiters in SASL PLAIN
---
 .../org/apache/james/managesieveserver/AuthenticateTest.java     | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index dc25cc5f64..63f9b86ec0 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -94,6 +94,15 @@ public class AuthenticateTest {
     // James is more lenient and also supports a space as the delimiter if the message is not base64-encoded.
     @Test
     void plainLoginWithSpaceAsDelimiterShouldSucceed() throws IOException {
+        String initialClientResponse = " " + ManageSieveServerTestSystem.USERNAME.asString() + " " + ManageSieveServerTestSystem.PASSWORD;
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + initialClientResponse + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+    }
+
+    // This tests the combination of both lenient behaviors above.
+    @Test
+    void plainLoginWithSpaceAsDelimiterWithoutLeadingSpaceShouldSucceed() throws IOException {
         String initialClientResponse = ManageSieveServerTestSystem.USERNAME.asString() + " " + ManageSieveServerTestSystem.PASSWORD;
         this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + initialClientResponse + "\"");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();


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


(james-project) 15/17: refactor(managesieve): only accept unquoted initial client response during continuation

Posted by bt...@apache.org.
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 ddc1be1b8d450a5aa69560103a9528f429a700c1
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Dec 1 11:08:37 2025 +0100

    refactor(managesieve): only accept unquoted initial client response during continuation
---
 .../org/apache/james/managesieve/scripts/authenticate.test    | 11 ++++++++---
 .../org/apache/james/managesieve/scripts/capability.test      |  2 +-
 .../org/apache/james/managesieve/scripts/checkscript.test     |  2 +-
 .../org/apache/james/managesieve/scripts/deletescript.test    |  2 +-
 .../org/apache/james/managesieve/scripts/getscript.test       |  2 +-
 .../org/apache/james/managesieve/scripts/havespace.test       |  2 +-
 .../org/apache/james/managesieve/scripts/listscripts.test     |  2 +-
 .../org/apache/james/managesieve/scripts/putscript.test       |  2 +-
 .../org/apache/james/managesieve/scripts/renamescript.test    |  2 +-
 .../org/apache/james/managesieve/scripts/setactive.test       |  2 +-
 .../org/apache/james/managesieve/scripts/starttls.test        |  2 +-
 .../org/apache/james/managesieve/scripts/unauthenticate.test  |  2 +-
 .../java/org/apache/james/managesieve/core/CoreProcessor.java |  2 +-
 .../james/managesieve/transcode/ManageSieveProcessor.java     |  4 +++-
 .../org/apache/james/managesieveserver/AuthenticateTest.java  |  4 ++--
 .../java/org/apache/james/managesieveserver/OIDCTest.java     |  8 ++++----
 16 files changed, 29 insertions(+), 22 deletions(-)

diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
index 54c8a6140c..1b03d55e31 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/authenticate.test
@@ -26,14 +26,19 @@ S: NO "Unknown SASL mechanism UNKNOWN"
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
 C: GETSCRIPT toto.sieve
-S: NO "ManageSieve syntax is incorrect: quoted authentication data must be supplied"
+S: NO "Authentication failed with: Verification of credentials failed"
+
+C: AUTHENTICATE "PLAIN"
+S: \+ ""
+C:
+S: NO "ManageSieve syntax is incorrect: authentication data must be supplied"
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "tin password"
+C: tin password
 S: NO "Authentication failed with: Verification of credentials failed"
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
index 095c34db56..5ae0e4344a 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/capability.test
@@ -39,7 +39,7 @@ S: OK
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: CAPABILITY
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
index 768909990c..b2df42bd63 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/checkscript.test
@@ -35,7 +35,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: CHECKSCRIPT {99+}
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
index be80044cac..6fc30c9620 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/deletescript.test
@@ -25,7 +25,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: DELETESCRIPT "foo"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
index a04c277122..9042c1d925 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/getscript.test
@@ -25,7 +25,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: GETSCRIPT "foo"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
index aaa2d3bfe9..2e742f9912 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/havespace.test
@@ -28,7 +28,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: HAVESPACE "scriptname" 49
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
index 9f048b42f9..f470c37fc0 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/listscripts.test
@@ -22,7 +22,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: LISTSCRIPTS
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
index b8048ce22a..e481bfe634 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/putscript.test
@@ -40,7 +40,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: PUTSCRIPT "mysievescript" {97+}
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
index 7b5ff18344..2472f3b81c 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/renamescript.test
@@ -28,7 +28,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: PUTSCRIPT "mysievescript" {99+}
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
index c233498a47..e232ef0d60 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/setactive.test
@@ -25,7 +25,7 @@ S: NO
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: SETACTIVE "foo"
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
index 39fdbf3f1d..6e1526c13d 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/starttls.test
@@ -25,7 +25,7 @@ S: NO You can't enable two time SSL encryption
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: STARTTLS
diff --git a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
index 92576fdba8..b2da7480c0 100644
--- a/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
+++ b/mpt/impl/managesieve/core/src/main/resources/org/apache/james/managesieve/scripts/unauthenticate.test
@@ -28,7 +28,7 @@ S: NO UNAUTHENTICATE command must be issued in authenticated state
 
 C: AUTHENTICATE "PLAIN"
 S: \+ ""
-C: "user password"
+C: user password
 S: OK
 
 C: GETSCRIPT any
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index 33cc055fb1..0e44c9c33d 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -239,7 +239,7 @@ public class CoreProcessor implements CoreCommands {
             SupportedMechanism currentAuthenticationMechanism = session.getChoosedAuthenticationMechanism();
             AuthenticationProcessor authenticationProcessor = authenticationProcessorMap.get(currentAuthenticationMechanism);
             if (Strings.isNullOrEmpty(suppliedData)) {
-                throw new SyntaxException("quoted authentication data must be supplied");
+                throw new SyntaxException("authentication data must be supplied");
             }
             if (suppliedData.equals("*")) {
                 throw new AuthenticationException("authentication aborted by client");
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
index 83cca6f8a0..391e9203b9 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
@@ -115,10 +115,12 @@ public class ManageSieveProcessor {
                 if (arguments.isEmpty()) {
                     return result;
                 }
+                // Unquote the argument in this case because continuation is not used.
+                arguments = ParserUtils.unquoteFirst(arguments);
             }
 
             // The authentication is in progress, the mechanism has been chosen, and the arguments contain an initial client response.
-            return argumentParser.authenticate(session, ParserUtils.unquoteFirst(arguments));
+            return argumentParser.authenticate(session, arguments);
         } else if (command.equalsIgnoreCase(CAPABILITY)) {
             return argumentParser.capability(session, arguments);
         } else if (command.equalsIgnoreCase(CHECKSCRIPT)) {
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index 05b3a83862..dc25cc5f64 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -124,7 +124,7 @@ public class AuthenticateTest {
         Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
         String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;
-        this.client.sendCommand("\"" + Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)) + "\"");
+        this.client.sendCommand(Base64.getEncoder().encodeToString(initialClientResponse.getBytes(StandardCharsets.UTF_8)));
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
     }
@@ -136,7 +136,7 @@ public class AuthenticateTest {
         Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
         Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
-        this.client.sendCommand("\"*\"");
+        this.client.sendCommand("*");
         ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
         Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");
diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
index a07785e77d..87ca3dbffe 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/OIDCTest.java
@@ -117,7 +117,7 @@ public class OIDCTest {
             Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
             Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
-            this.client.sendCommand("\"" + VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE + "\"");
+            this.client.sendCommand(VALID_OAUTHBEARER_INITIAL_CLIENT_RESPONSE);
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
         }
@@ -129,7 +129,7 @@ public class OIDCTest {
             Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
             Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
-            this.client.sendCommand("\"*\"");
+            this.client.sendCommand("*");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
             Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");
@@ -156,7 +156,7 @@ public class OIDCTest {
             Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
             Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
-            this.client.sendCommand("\"" + VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE + "\"");
+            this.client.sendCommand(VALID_XOAUTH2_INITIAL_CLIENT_RESPONSE);
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
         }
@@ -168,7 +168,7 @@ public class OIDCTest {
             Assertions.assertThat(continuationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.CONTINUATION);
             Assertions.assertThat(continuationResponse.explanation().get()).isEqualTo("");
 
-            this.client.sendCommand("\"*\"");
+            this.client.sendCommand("*");
             ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
             Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.NO);
             Assertions.assertThat(authenticationResponse.explanation()).get().isEqualTo("Authentication failed with: authentication aborted by client");


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


(james-project) 14/17: feat(managesieve): add test for old SASL PLAIN parsing behavior

Posted by bt...@apache.org.
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 79c6c8ebb245f73e9fd4f1190ab7ee3744427a85
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Nov 24 09:29:32 2025 +0100

    feat(managesieve): add test for old SASL PLAIN parsing behavior
---
 .../org/apache/james/managesieveserver/AuthenticateTest.java   | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
index dfdce97777..05b3a83862 100644
--- a/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
+++ b/server/protocols/protocols-managesieve/src/test/java/org/apache/james/managesieveserver/AuthenticateTest.java
@@ -90,6 +90,16 @@ public class AuthenticateTest {
         Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
     }
 
+    // The SASL PLAIN standard defines UTF8NUL as separator. To stay compatible with older versions of James,
+    // James is more lenient and also supports a space as the delimiter if the message is not base64-encoded.
+    @Test
+    void plainLoginWithSpaceAsDelimiterShouldSucceed() throws IOException {
+        String initialClientResponse = ManageSieveServerTestSystem.USERNAME.asString() + " " + ManageSieveServerTestSystem.PASSWORD;
+        this.client.sendCommand("AUTHENTICATE \"PLAIN\" \"" + initialClientResponse + "\"");
+        ManageSieveClient.ServerResponse authenticationResponse = this.client.readResponse();
+        Assertions.assertThat(authenticationResponse.responseType()).isEqualTo(ManageSieveClient.ResponseType.OK);
+    }
+
     @Test
     void plainLoginWithoutMechanismQuotesShouldNotSucceed() throws IOException {
         String initialClientResponse = "\0" + ManageSieveServerTestSystem.USERNAME.asString() + "\0" + ManageSieveServerTestSystem.PASSWORD;


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


(james-project) 01/17: feat(managesieve): add XOAUTH2 authentication mechanism

Posted by bt...@apache.org.
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 6512a4804847c522926182830c6c21ac616f1248
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Jul 21 14:39:04 2025 +0200

    feat(managesieve): add XOAUTH2 authentication mechanism
---
 protocols/managesieve/pom.xml                      |   8 ++
 .../managesieve/api/CapabilityAdvertiser.java      |  27 -----
 .../org/apache/james/managesieve/api/Session.java  |   6 ++
 .../managesieve/api/commands/Authenticate.java     |   2 +-
 .../managesieve/api/commands/CoreCommands.java     |   5 +-
 .../james/managesieve/core/CoreProcessor.java      |  25 ++---
 .../core/XOAUTH2AuthenticationProcessor.java       | 110 +++++++++++++++++++++
 .../managesieve/transcode/ArgumentParser.java      |   4 -
 .../transcode/ManageSieveProcessor.java            |   4 +-
 .../james/managesieve/util/SettableSession.java    |  14 +++
 .../netty/ManageSieveChannelUpstreamHandler.java   |   9 +-
 .../managesieveserver/netty/ManageSieveServer.java |  17 +++-
 12 files changed, 178 insertions(+), 53 deletions(-)

diff --git a/protocols/managesieve/pom.xml b/protocols/managesieve/pom.xml
index fb367c75f5..5522b6fa88 100644
--- a/protocols/managesieve/pom.xml
+++ b/protocols/managesieve/pom.xml
@@ -41,11 +41,19 @@
             <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-jwt</artifactId>
+        </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
             <artifactId>testing-base</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>${james.protocols.groupId}</groupId>
+            <artifactId>protocols-api</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/CapabilityAdvertiser.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/CapabilityAdvertiser.java
deleted file mode 100644
index de8c12ad10..0000000000
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/CapabilityAdvertiser.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- *   Licensed to the Apache Software Foundation (ASF) under one
- *   or more contributor license agreements.  See the NOTICE file
- *   distributed with this work for additional information
- *   regarding copyright ownership.  The ASF licenses this file
- *   to you under the Apache License, Version 2.0 (the
- *   "License"); you may not use this file except in compliance
- *   with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- *   Unless required by applicable law or agreed to in writing,
- *   software distributed under the License is distributed on an
- *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *   KIND, either express or implied.  See the License for the
- *   specific language governing permissions and limitations
- *   under the License.
- *
- */
-
-package org.apache.james.managesieve.api;
-
-public interface CapabilityAdvertiser {
-
-    String getAdvertisedCapabilities();
-
-}
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
index c33293d8ee..ca5ed2b2fa 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/Session.java
@@ -20,8 +20,11 @@
 
 package org.apache.james.managesieve.api;
 
+import java.util.Optional;
+
 import org.apache.james.core.Username;
 import org.apache.james.managesieve.api.commands.Authenticate;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 public interface Session {
 
@@ -51,4 +54,7 @@ public interface Session {
 
     boolean isSslEnabled();
 
+    Optional<OidcSASLConfiguration> getOidcSASLConfiguration();
+
+    void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration);
 }
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
index 02c05311d8..6912348283 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/Authenticate.java
@@ -30,7 +30,7 @@ import org.apache.james.managesieve.api.UnknownSaslMechanism;
 public interface Authenticate {
 
     enum SupportedMechanism {
-        PLAIN;
+        PLAIN, XOAUTH2;
 
         public static SupportedMechanism retrieveMechanism(String serializedData) throws UnknownSaslMechanism {
             for (SupportedMechanism supportedMechanism : SupportedMechanism.values()) {
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
index 092472a955..6016b9c192 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/api/commands/CoreCommands.java
@@ -20,15 +20,12 @@
 
 package org.apache.james.managesieve.api.commands;
 
-import org.apache.james.managesieve.api.CapabilityAdvertiser;
-
 /**
  * Core RFC 5804 Commands common to all transports
  * 
  * @see <a href=http://tools.ietf.org/html/rfc5804#section-2>RFC 5804 Commands</a>
  */
 public interface CoreCommands extends Capability, CheckScript, DeleteScript, GetScript, HaveSpace,
-        ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, Authenticate, StartTLS,
-        CapabilityAdvertiser {
+        ListScripts, PutScript, RenameScript, SetActive, Noop, Unauthenticate, Logout, Authenticate, StartTLS {
 
 }
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
index c4bbe10691..c8f6aab73b 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/CoreProcessor.java
@@ -22,7 +22,6 @@ package org.apache.james.managesieve.core;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -57,7 +56,6 @@ import org.apache.james.user.api.UsersRepository;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 
 public class CoreProcessor implements CoreCommands {
@@ -83,11 +81,6 @@ public class CoreProcessor implements CoreCommands {
         this.authenticationProcessorMap.put(SupportedMechanism.PLAIN, new PlainAuthenticationProcessor(usersRepository));
     }
 
-    @Override
-    public String getAdvertisedCapabilities() {
-        return convertCapabilityMapToString(capabilitiesBase) + "\r\n";
-    }
-
     @Override
     public String capability(Session session) {
         return convertCapabilityMapToString(computeCapabilityMap(session)) + "\r\nOK";
@@ -106,6 +99,10 @@ public class CoreProcessor implements CoreCommands {
         if (session.isAuthenticated()) {
             capabilities.put(Capabilities.OWNER, session.getUser().asString());
         }
+        session.getOidcSASLConfiguration().ifPresent(oidcConfiguration -> {
+            this.authenticationProcessorMap.putIfAbsent(SupportedMechanism.XOAUTH2, new XOAUTH2AuthenticationProcessor(oidcConfiguration));
+        });
+        capabilities.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
         return capabilities;
     }
 
@@ -218,6 +215,9 @@ public class CoreProcessor implements CoreCommands {
             }
             String unquotedMechanism = ParserUtils.unquoteFirst(mechanism);
             SupportedMechanism supportedMechanism = SupportedMechanism.retrieveMechanism(unquotedMechanism);
+            if (!this.authenticationProcessorMap.containsKey(supportedMechanism)) {
+                throw new UnknownSaslMechanism("SASL mechanism disabled: " + unquotedMechanism);
+            }
 
             session.setChoosedAuthenticationMechanism(supportedMechanism);
             session.setState(Session.State.AUTHENTICATION_IN_PROGRESS);
@@ -328,7 +328,6 @@ public class CoreProcessor implements CoreCommands {
         Map<Capabilities, String> capabilitiesBase = new HashMap<>();
         capabilitiesBase.put(Capabilities.IMPLEMENTATION, IMPLEMENTATION_DESCRIPTION);
         capabilitiesBase.put(Capabilities.VERSION, MANAGE_SIEVE_VERSION);
-        capabilitiesBase.put(Capabilities.SASL, constructSaslSupportedAuthenticationMechanisms());
         capabilitiesBase.put(Capabilities.STARTTLS, null);
         if (!extensions.isEmpty()) {
             capabilitiesBase.put(Capabilities.SIEVE, extensions);
@@ -337,10 +336,12 @@ public class CoreProcessor implements CoreCommands {
     }
 
     private String constructSaslSupportedAuthenticationMechanisms() {
-        return Joiner.on(' ')
-            .join(Lists.transform(
-                Arrays.asList(SupportedMechanism.values()),
-                Enum::toString));
+        return Joiner.on(' ').join(this.authenticationProcessorMap
+            .keySet()
+            .stream()
+            .map(Enum::toString)
+            .iterator()
+        );
     }
 
     private String sanitizeString(String message) {
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java
new file mode 100644
index 0000000000..8a65202ac5
--- /dev/null
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/core/XOAUTH2AuthenticationProcessor.java
@@ -0,0 +1,110 @@
+/*
+ *   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.managesieve.core;
+
+import java.util.Optional;
+
+import org.apache.james.core.Username;
+import org.apache.james.jwt.OidcJwtTokenVerifier;
+import org.apache.james.jwt.introspection.IntrospectionEndpoint;
+import org.apache.james.managesieve.api.AuthenticationException;
+import org.apache.james.managesieve.api.AuthenticationProcessor;
+import org.apache.james.managesieve.api.Session;
+import org.apache.james.managesieve.api.SyntaxException;
+import org.apache.james.protocols.api.OIDCSASLParser;
+import org.apache.james.protocols.api.OIDCSASLParser.OIDCInitialResponse;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
+
+import reactor.core.publisher.Mono;
+
+public class XOAUTH2AuthenticationProcessor implements AuthenticationProcessor {
+
+    private final OidcSASLConfiguration oidcConfiguration;
+
+    public XOAUTH2AuthenticationProcessor(OidcSASLConfiguration oidcConfiguration) {
+        this.oidcConfiguration = oidcConfiguration;
+    }
+
+    @Override
+    public String initialServerResponse(Session session) {
+        return "+ \"\"";
+    }
+
+    @Override
+    public Username isAuthenticationSuccesfull(Session session, String suppliedClientData) throws SyntaxException, AuthenticationException {
+        Optional<OIDCInitialResponse> oidcInitialResponseResult = OIDCSASLParser.parse(suppliedClientData);
+        if (oidcInitialResponseResult.isEmpty()) {
+            throw new SyntaxException("Could not parse the given JWT");
+        }
+        OIDCInitialResponse oidcInitialResponse = oidcInitialResponseResult.get();
+
+        Optional<Username> authenticatedUserResult = Optional.empty();
+        try {
+            authenticatedUserResult = validateToken(oidcInitialResponse.getToken());
+        } catch (Exception e) {
+            throw new AuthenticationException("Could not validate the JWT");
+        }
+        if (authenticatedUserResult.isEmpty()) {
+            throw new AuthenticationException("Could not validate the JWT");
+        }
+        Username authenticatedUser = authenticatedUserResult.get();
+
+        // The user from the managesieve AUTHENTICATE command must match the username in the token.
+        Username associatedUser = Username.of(oidcInitialResponse.getAssociatedUser());
+        if (!authenticatedUser.equals(associatedUser)) {
+            throw new AuthenticationException("Mismatch between user from command and JWT");
+        }
+
+        return authenticatedUser;
+    }
+
+    private Optional<Username> validateToken(String token) {
+        if (this.oidcConfiguration.isCheckTokenByIntrospectionEndpoint()) {
+            return validTokenWithIntrospection(token);
+        } else if (this.oidcConfiguration.isCheckTokenByUserinfoEndpoint()) {
+            return validTokenWithUserInfo(token);
+        } else {
+            return OidcJwtTokenVerifier.verifySignatureAndExtractClaim(token, this.oidcConfiguration.getJwksURL(), this.oidcConfiguration.getClaim())
+                .map(Username::of);
+        }
+    }
+
+    private Optional<Username> validTokenWithUserInfo(String token) {
+        return Mono.from(OidcJwtTokenVerifier.verifyWithUserinfo(token,
+                this.oidcConfiguration.getJwksURL(),
+                this.oidcConfiguration.getClaim(),
+                this.oidcConfiguration.getUserInfoEndpoint().orElseThrow()))
+            .blockOptional()
+            .map(Username::of);
+    }
+
+    private Optional<Username> validTokenWithIntrospection(String token) {
+        return Mono.from(OidcJwtTokenVerifier.verifyWithIntrospection(token,
+                this.oidcConfiguration.getJwksURL(),
+                this.oidcConfiguration.getClaim(),
+                this.oidcConfiguration.getIntrospectionEndpoint()
+                    .map(endpoint -> new IntrospectionEndpoint(endpoint, this.oidcConfiguration.getIntrospectionEndpointAuthorization()))
+                    .orElseThrow()))
+            .blockOptional()
+            .map(Username::of);
+    }
+}
+
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
index e9f78f2629..c6d2cb16f6 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ArgumentParser.java
@@ -53,10 +53,6 @@ public class ArgumentParser {
         this.validatePutSize = validatePutSize;
     }
 
-    public String getAdvertisedCapabilities() {
-        return core.getAdvertisedCapabilities();
-    }
-
     public String capability(Session session, String args) {
         if (!args.trim().isEmpty()) {
             return "NO \"Too many arguments: " + args + "\"";
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
index a68359ce8f..94708c1761 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/transcode/ManageSieveProcessor.java
@@ -129,8 +129,8 @@ public class ManageSieveProcessor {
         return "NO unknown " + command + " command";
     }
 
-    public String getAdvertisedCapabilities() {
-        return argumentParser.getAdvertisedCapabilities();
+    public String getAdvertisedCapabilities(Session session) {
+        return argumentParser.capability(session, "") + "\r\n";
     }
 
 }
diff --git a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
index 09b49dea6e..f689ffcc2e 100644
--- a/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
+++ b/protocols/managesieve/src/main/java/org/apache/james/managesieve/util/SettableSession.java
@@ -20,9 +20,12 @@
 
 package org.apache.james.managesieve.util;
 
+import java.util.Optional;
+
 import org.apache.james.core.Username;
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.commands.Authenticate;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 
 public class SettableSession implements Session {
 
@@ -30,6 +33,7 @@ public class SettableSession implements Session {
     private State state;
     private Authenticate.SupportedMechanism choosedAuthenticationMechanism;
     private boolean sslEnabled;
+    private Optional<OidcSASLConfiguration> oidcSASLConfiguration = Optional.empty();
 
     public SettableSession() {
         this.state = State.UNAUTHENTICATED;
@@ -80,4 +84,14 @@ public class SettableSession implements Session {
     public boolean isSslEnabled() {
         return sslEnabled;
     }
+
+    @Override
+    public Optional<OidcSASLConfiguration> getOidcSASLConfiguration() {
+        return this.oidcSASLConfiguration;
+    }
+
+    @Override
+    public void setOidcSASLConfiguration(Optional<OidcSASLConfiguration> configuration) {
+        this.oidcSASLConfiguration = configuration;
+    }
 }
diff --git a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
index 444d426351..0e6454ad80 100644
--- a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
+++ b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveChannelUpstreamHandler.java
@@ -21,12 +21,14 @@ package org.apache.james.managesieveserver.netty;
 
 import java.io.Closeable;
 import java.net.InetSocketAddress;
+import java.util.Optional;
 
 import org.apache.james.managesieve.api.Session;
 import org.apache.james.managesieve.api.SessionTerminatedException;
 import org.apache.james.managesieve.transcode.ManageSieveProcessor;
 import org.apache.james.managesieve.transcode.NotEnoughDataException;
 import org.apache.james.managesieve.util.SettableSession;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.apache.james.protocols.api.ProxyInformation;
 import org.apache.james.protocols.netty.Encryption;
 import org.slf4j.Logger;
@@ -51,12 +53,14 @@ public class ManageSieveChannelUpstreamHandler extends ChannelInboundHandlerAdap
     private final ManageSieveProcessor manageSieveProcessor;
     private final Encryption secure;
     private final int maxLineLength;
+    private final Optional<OidcSASLConfiguration> oidcConfiguration;
 
     public ManageSieveChannelUpstreamHandler(
-        ManageSieveProcessor manageSieveProcessor, Encryption secure, int maxLineLength) {
+        ManageSieveProcessor manageSieveProcessor, Encryption secure, int maxLineLength, Optional<OidcSASLConfiguration> oidcConfiguration) {
         this.manageSieveProcessor = manageSieveProcessor;
         this.secure = secure;
         this.maxLineLength = maxLineLength;
+        this.oidcConfiguration = oidcConfiguration;
     }
 
     private boolean isSSL() {
@@ -146,13 +150,14 @@ public class ManageSieveChannelUpstreamHandler extends ChannelInboundHandlerAdap
             LOGGER.info("Connection established from {}", address.getAddress().getHostAddress());
 
             Session session = new SettableSession();
+            session.setOidcSASLConfiguration(this.oidcConfiguration);
             if (isSSL()) {
                 session.setSslEnabled(true);
             }
             ctx.channel().attr(NettyConstants.SESSION_ATTRIBUTE_KEY).set(session);
             ctx.channel().attr(NettyConstants.RESPONSE_WRITER_ATTRIBUTE_KEY).set(new ChannelManageSieveResponseWriter(ctx.channel()));
             super.channelActive(ctx);
-            ctx.channel().attr(NettyConstants.RESPONSE_WRITER_ATTRIBUTE_KEY).get().write(manageSieveProcessor.getAdvertisedCapabilities() + "OK\r\n");
+            ctx.channel().attr(NettyConstants.RESPONSE_WRITER_ATTRIBUTE_KEY).get().write(manageSieveProcessor.getAdvertisedCapabilities(session));
         }
     }
 
diff --git a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
index 114b082217..3c52a0f6c7 100644
--- a/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
+++ b/server/protocols/protocols-managesieve/src/main/java/org/apache/james/managesieveserver/netty/ManageSieveServer.java
@@ -19,12 +19,15 @@
 
 package org.apache.james.managesieveserver.netty;
 
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
 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.managesieve.transcode.ManageSieveProcessor;
+import org.apache.james.protocols.api.OidcSASLConfiguration;
 import org.apache.james.protocols.lib.netty.AbstractConfigurableAsyncServer;
 import org.apache.james.protocols.netty.AbstractChannelPipelineFactory;
 import org.apache.james.protocols.netty.AllButStartTlsLineChannelHandlerFactory;
@@ -48,11 +51,13 @@ import io.netty.util.CharsetUtil;
 public class ManageSieveServer extends AbstractConfigurableAsyncServer implements ManageSieveServerMBean {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ManageSieveServer.class);
+    static final String OIDC_PATH = "oidc";
 
     private final int maxLineLength;
     private final ManageSieveProcessor manageSieveProcessor;
     private Optional<ConnectionLimitUpstreamHandler> connectionLimitUpstreamHandler = Optional.empty();
     private Optional<ConnectionPerIpLimitUpstreamHandler> connectionPerIpLimitUpstreamHandler = Optional.empty();
+    private Optional<OidcSASLConfiguration> oidcConfiguration;
 
     public ManageSieveServer(int maxLineLength, ManageSieveProcessor manageSieveProcessor) {
         this.maxLineLength = maxLineLength;
@@ -70,6 +75,16 @@ public class ManageSieveServer extends AbstractConfigurableAsyncServer implement
 
         connectionLimitUpstreamHandler = ConnectionLimitUpstreamHandler.forCount(connectionLimit);
         connectionPerIpLimitUpstreamHandler = ConnectionPerIpLimitUpstreamHandler.forCount(connPerIP);
+
+        if (config.immutableChildConfigurationsAt(OIDC_PATH).isEmpty()) {
+            this.oidcConfiguration = Optional.empty();
+        } else {
+            try {
+                this.oidcConfiguration = Optional.of(OidcSASLConfiguration.parse(config.configurationAt(OIDC_PATH)));
+            } catch (MalformedURLException | NullPointerException | URISyntaxException exception) {
+                throw new ConfigurationException("Failed to parse OIDC configuration", exception);
+            }
+        }
     }
 
     @Override
@@ -79,7 +94,7 @@ public class ManageSieveServer extends AbstractConfigurableAsyncServer implement
 
     @Override
     protected ChannelInboundHandlerAdapter createCoreHandler() {
-        return new ManageSieveChannelUpstreamHandler(manageSieveProcessor, getEncryption(), maxLineLength);
+        return new ManageSieveChannelUpstreamHandler(manageSieveProcessor, getEncryption(), maxLineLength, this.oidcConfiguration);
     }
 
     @Override


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


(james-project) 08/17: feature(examples): configure managesieve and test oidc authentication in oidc example

Posted by bt...@apache.org.
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 6b35fedb6aba4f097d81487d3767e0d9514bcc09
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Sep 8 14:11:03 2025 +0200

    feature(examples): configure managesieve and test oidc authentication in oidc example
---
 examples/oidc/README.md                   | 33 ++++++++++++++++++++++++-
 examples/oidc/docker-compose.yml          |  4 +++
 examples/oidc/james/managesieveserver.xml | 21 ++++++++++++++++
 examples/oidc/test.sh                     | 41 +++++++++++++++++++++++++++++++
 4 files changed, 98 insertions(+), 1 deletion(-)

diff --git a/examples/oidc/README.md b/examples/oidc/README.md
index ea9b617863..08d7eae74e 100644
--- a/examples/oidc/README.md
+++ b/examples/oidc/README.md
@@ -189,4 +189,35 @@ We would use Thunderbird version 91.4.1 as a mail client (above versions should
   ![](_media/receive-mail.png)
 
 A remark here is that if you generate a new client_secret for `james-thunderbird` client in Keycloak, you have to modify
-it accordingly in `OAuth2Providers.jsm`.
\ No newline at end of file
+it accordingly in `OAuth2Providers.jsm`.
+
+### IMAP on the CLI
+
+You can test logging into IMAP on the CLI by connecting with `telnet localhost 143`. Here are some commands that can be tried:
+
+- `a AUTHENTICATE XOAUTH2 <initial response>` (unauthenticated state)
+- `b AUTHENTICATE OAUTHBEARER <initial response>` (unauthenticated state)
+- `c LOGOUT` (any state)
+
+You can get the initial response from the [test script](./test.sh).
+
+### ManageSieve on the CLI
+
+You can test logging into IMAP on the CLI by connecting with `telnet localhost 4190`. Here are some commands that can be tried:
+
+- `AUTHENTICATE "XOAUTH2" "<initial response>"` (unauthenticated state)
+- `AUTHENTICATE "OAUTHBEARER" "<initial response>"` (unauthenticated state)
+- `CAPABILITY` (any state)
+- `LOGOUT` (any state)
+
+You can get the initial response from the [test script](./test.sh).
+
+### SMTP on the CLI
+
+You can test logging into IMAP on the CLI by connecting with `telnet localhost 587`. Here are some commands that can be tried:
+
+- `AUTH XOAUTH2 <initial response>` (unauthenticated state)
+- `AUTH OAUTHBEARER <initial response>` (unauthenticated state)
+- `QUIT` (any state)
+
+You can get the initial response from the [test script](./test.sh).
diff --git a/examples/oidc/docker-compose.yml b/examples/oidc/docker-compose.yml
index 65659d06f4..31e0261d84 100644
--- a/examples/oidc/docker-compose.yml
+++ b/examples/oidc/docker-compose.yml
@@ -29,8 +29,12 @@ services:
       - ./james/jmap.properties:/root/conf/jmap.properties
       - ./james/imapserver.xml:/root/conf/imapserver.xml
       - ./james/smtpserver.xml:/root/conf/smtpserver.xml
+      - ./james/managesieveserver.xml:/root/conf/managesieveserver.xml
     ports:
       - "8000:8000"
+      - "143:143"
+      - "587:587"
+      - "4190:4190"
     healthcheck:
       test: ["CMD", "curl", "-f", "http://james:8000/domains"]
 
diff --git a/examples/oidc/james/managesieveserver.xml b/examples/oidc/james/managesieveserver.xml
new file mode 100644
index 0000000000..a0e2b79439
--- /dev/null
+++ b/examples/oidc/james/managesieveserver.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<managesieveservers>
+    <managesieveserver enabled="true">
+        <jmxName>managesieveserver</jmxName>
+        <bind>0.0.0.0:4190</bind>
+        <connectionBacklog>200</connectionBacklog>
+        <connectiontimeout>360</connectiontimeout>
+        <connectionLimit>0</connectionLimit>
+        <connectionLimitPerIP>0</connectionLimitPerIP>
+        <oidc>
+            <oidcConfigurationURL>http://sso.example.com:8080/auth/realms/oidc/.well-known/openid-configuration</oidcConfigurationURL>
+            <jwksURL>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/certs</jwksURL>
+            <claim>email</claim>
+            <scope>openid profile email</scope>
+            <introspection>
+                <url>http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/token/introspect</url>
+                <auth>Basic amFtZXMtdGh1bmRlcmJpcmQ6WHc5aHQxdmVUdTBUazVzTU15MDNQZHpZM0FpRnZzc3c=</auth>
+            </introspection>
+        </oidc>
+    </managesieveserver>
+</managesieveservers>
diff --git a/examples/oidc/test.sh b/examples/oidc/test.sh
old mode 100644
new mode 100755
index 5c9665c3ef..5477975955
--- a/examples/oidc/test.sh
+++ b/examples/oidc/test.sh
@@ -32,6 +32,47 @@ else
 	echo "Not OK"
 fi
 
+XOAUTH2_INITIAL_CLIENT_RESPONSE=`echo -n -e "user=james-user@localhost\x01auth=Bearer ${ACCESS_TOKEN}\x01\x01" | base64 -w 0`
+OAUTHBEARER_INITIAL_CLIENT_RESPONSE=`echo -n -e "n,a=james-user@localhost\x01auth=Bearer ${ACCESS_TOKEN}\x01\x01" | base64 -w 0`
+
+set +x
+MANAGESIEVE_XOAUTH2_RESPONSE=`(echo "AUTHENTICATE \"XOAUTH2\" \"${XOAUTH2_INITIAL_CLIENT_RESPONSE}\""; echo "CAPABILITY"; echo "LOGOUT"; sleep 3) | telnet localhost 4190`
+if echo $MANAGESIEVE_XOAUTH2_RESPONSE | grep "\"OWNER\" \"james-user@localhost\"" > /dev/null; then
+	echo "Success: Managesieve XOAUTH2 login"
+else
+	echo "Error: Managesieve XOAUTH2 login"
+fi
+if echo $MANAGESIEVE_XOAUTH2_RESPONSE | grep "OK channel is closing" > /dev/null; then
+	echo "Success: Managesieve XOAUTH2 logout"
+else
+	echo "Error: Managesieve XOAUTH2 logout"
+fi
+
+IMAP_XOAUTH2_RESPONSE=`(echo "a AUTHENTICATE XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "c LOGOUT"; sleep 3) | telnet localhost 143`
+if echo $IMAP_XOAUTH2_RESPONSE | grep "a OK AUTHENTICATE completed" > /dev/null; then
+	echo "Success: IMAP XOAUTH2 login"
+else
+	echo "Error: IMAP XOAUTH2 login"
+fi
+if echo $IMAP_XOAUTH2_RESPONSE | grep "c OK LOGOUT completed" > /dev/null; then
+	echo "Success: IMAP XOAUTH2 logout"
+else
+	echo "Error: IMAP XOAUTH2 logout"
+fi
+
+SMTP_XOAUTH2_RESPONSE=`(echo "AUTH XOAUTH2 ${XOAUTH2_INITIAL_CLIENT_RESPONSE}"; echo "QUIT"; sleep 3) | telnet localhost 587`
+if echo $SMTP_XOAUTH2_RESPONSE | grep "235 Authentication successful" > /dev/null; then
+	echo "Success: SMTP XOAUTH2 login"
+else
+	echo "Error: SMTP XOAUTH2 login"
+fi
+if echo $SMTP_XOAUTH2_RESPONSE | grep "221 2.0.0 james.local Service closing transmission channel" > /dev/null; then
+	echo "Success: SMTP XOAUTH2 logout"
+else
+	echo "Error: SMTP XOAUTH2 logout"
+fi
+set -x
+
 # Logout
 
 curl --location 'http://sso.example.com:8080/auth/realms/oidc/protocol/openid-connect/logout' \


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


(james-project) 12/17: fix(oidc): wrong format of gs2-header

Posted by bt...@apache.org.
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 c6bd40ff0bc26dd95808aeafc12530cf4fffcc0f
Author: Felix Auringer <fe...@protonmail.com>
AuthorDate: Mon Nov 17 11:04:36 2025 +0100

    fix(oidc): wrong format of gs2-header
---
 .../main/java/org/apache/james/protocols/api/OIDCSASLParser.java  | 5 +++++
 .../test/java/org/apache/james/protocols/api/OIDCSASLHelper.java  | 8 ++++----
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java b/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
index 72df061214..1623998c95 100644
--- a/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
+++ b/protocols/api/src/main/java/org/apache/james/protocols/api/OIDCSASLParser.java
@@ -61,6 +61,7 @@ public class OIDCSASLParser {
         Optional<String> decodeResult = decodeBase64(initialResponse);
 
         if (decodeResult.isPresent()) {
+            // See the format of the gs2-header in https://www.rfc-editor.org/rfc/rfc5801#section-4.
             String decodeValueWithoutDanglingPart = decodeResult.filter(value -> value.startsWith("n,"))
                 .map(value -> value.substring(2))
                 .orElse(decodeResult.get());
@@ -81,6 +82,10 @@ public class OIDCSASLParser {
                     userPartCounter++;
                 } else if (stringToken.startsWith(OAUTHBEARER_USER_PART_PREFIX)) {
                     userPart = stringToken.substring(OAUTHBEARER_USER_PART_INDEX);
+                    // See the format of the gs2-header in https://www.rfc-editor.org/rfc/rfc5801#section-4.
+                    if (userPart.endsWith(",")) {
+                        userPart = userPart.substring(0, userPart.length() - 1);
+                    }
                     userPartCounter++;
                 }
             }
diff --git a/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
index b806a40acb..8440a51de0 100644
--- a/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
+++ b/protocols/api/src/test/java/org/apache/james/protocols/api/OIDCSASLHelper.java
@@ -25,7 +25,7 @@ import java.util.Base64;
 import com.google.common.collect.ImmutableList;
 
 public class OIDCSASLHelper {
-    // See the XOAUTH2 specification athttps://developers.google.com/workspace/gmail/imap/xoauth2-protocol
+    // See the XOAUTH2 specification at https://developers.google.com/workspace/gmail/imap/xoauth2-protocol
     // for details.
     public static String generateEncodedXOauth2InitialClientResponse(String username, String token) {
         return Base64.getEncoder().encodeToString(String.join("" + OIDCSASLParser.SASL_SEPARATOR,
@@ -33,11 +33,11 @@ public class OIDCSASLHelper {
             .getBytes(StandardCharsets.US_ASCII));
     }
 
-    // See the OAUTHBEARER specification at https://datatracker.ietf.org/doc/html/rfc5801#section-4
-    // for details.
+    // See the OAUTHBEARER specification at https://www.rfc-editor.org/rfc/rfc7628.html#section-3.1
+    // and the GSS-API specification at https://www.rfc-editor.org/rfc/rfc5801#section-4 for details.
     public static String generateEncodedOauthbearerInitialClientResponse(String username, String token) {
         return Base64.getEncoder().encodeToString(String.join("" + OIDCSASLParser.SASL_SEPARATOR,
-                ImmutableList.of("n,a=" + username, "auth=Bearer " + token, "", ""))
+                ImmutableList.of("n,a=" + username + ",", "auth=Bearer " + token, "", ""))
             .getBytes(StandardCharsets.US_ASCII));
     }
 }


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