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 <[email protected]> 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": "[email protected]" + public static final String USER_EMAIL_ADDRESS = "[email protected]"; 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: [email protected] For additional commands, e-mail: [email protected]
