This is an automated email from the ASF dual-hosted git repository. quantranhong1999 pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 9827c4b455ed135086dbc34e56b0cd7701340f35 Author: Quan Tran <[email protected]> AuthorDate: Tue Jul 7 09:20:57 2026 +0700 JAMES-4210 Introduce protocol-neutral SASL SPI Add the shared SASL exchange model: mechanism factories, initial requests, exchange lifecycle, typed steps, identities, authentication results, and failures. Add contract tests documenting one-step, multi-step, delegation, payload copy, and lifecycle behavior. --- .../api/sasl/SaslAuthenticationResult.java | 32 +++ .../protocols/api/sasl/SaslAuthenticator.java | 43 ++++ .../james/protocols/api/sasl/SaslExchange.java | 45 ++++ .../james/protocols/api/sasl/SaslFailure.java | 66 ++++++ .../james/protocols/api/sasl/SaslIdentity.java | 31 +++ .../protocols/api/sasl/SaslInitialRequest.java | 42 ++++ .../james/protocols/api/sasl/SaslMechanism.java | 44 ++++ .../protocols/api/sasl/SaslMechanismFactory.java | 31 +++ .../protocols/api/sasl/SaslMechanismNames.java | 29 +++ .../apache/james/protocols/api/sasl/SaslStep.java | 66 ++++++ .../api/sasl/SaslMechanismContractTest.java | 249 +++++++++++++++++++++ 11 files changed, 678 insertions(+) diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticationResult.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticationResult.java new file mode 100644 index 0000000000..f3c2c10a69 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticationResult.java @@ -0,0 +1,32 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +/** + * Result of protocol-neutral James authentication or authorization performed for a SASL mechanism. + */ +public sealed interface SaslAuthenticationResult permits SaslAuthenticationResult.Success, SaslAuthenticationResult.Failure { + + record Success(SaslIdentity identity) implements SaslAuthenticationResult { + } + + record Failure(SaslFailure failure) implements SaslAuthenticationResult { + } +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticator.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticator.java new file mode 100644 index 0000000000..18b21d7810 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslAuthenticator.java @@ -0,0 +1,43 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import java.util.Optional; + +import org.apache.james.core.Username; + +/** + * Protocol-neutral authentication service available to SASL mechanisms. + */ +public interface SaslAuthenticator { + /** + * Verifies password credentials and, when present, validates that the authenticated user may act as the + * requested authorization identity. + */ + SaslAuthenticationResult authenticatePassword(Username authenticationId, + Optional<Username> authorizationId, + String password); + + /** + * Validates an already-authenticated identity, typically for token or Kerberos mechanisms that verified + * credentials inside the SASL exchange. + */ + SaslAuthenticationResult authorize(SaslIdentity identity); +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslExchange.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslExchange.java new file mode 100644 index 0000000000..49921941d2 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslExchange.java @@ -0,0 +1,45 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +/** + * Stateful SASL authentication exchange. + */ +public interface SaslExchange extends AutoCloseable { + /** + * Starts the exchange and returns the first server step. + */ + SaslStep firstStep(); + + /** + * Continues the exchange with a decoded client response. + */ + SaslStep onResponse(byte[] clientResponse); + + /** + * Aborts the exchange after a client cancellation or protocol-level failure, and releases associated resources. + */ + default void abort() { + close(); + } + + @Override + void close(); +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslFailure.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslFailure.java new file mode 100644 index 0000000000..385ef2140b --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslFailure.java @@ -0,0 +1,66 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import java.util.Optional; + +import org.apache.james.core.Username; + +/** + * Protocol-neutral SASL failure with enough metadata for protocol-specific response mapping and audit logging. + */ +public record SaslFailure(Type type, + Optional<Username> authenticationId, + Optional<Username> authorizationId, + String reason, + Optional<Throwable> cause) { + public enum Type { + MALFORMED, + INVALID_CREDENTIALS, + AUTHENTICATION_FAILED, + USER_DOES_NOT_EXIST, + DELEGATION_FORBIDDEN, + SERVER_ERROR + } + + public static SaslFailure malformed(String reason) { + return new SaslFailure(Type.MALFORMED, Optional.empty(), Optional.empty(), reason, Optional.empty()); + } + + public static SaslFailure invalidCredentials(Username authenticationId, Optional<Username> authorizationId, String reason) { + return new SaslFailure(Type.INVALID_CREDENTIALS, Optional.of(authenticationId), authorizationId, reason, Optional.empty()); + } + + public static SaslFailure authenticationFailed(Optional<Username> authenticationId, Optional<Username> authorizationId, String reason) { + return new SaslFailure(Type.AUTHENTICATION_FAILED, authenticationId, authorizationId, reason, Optional.empty()); + } + + public static SaslFailure userDoesNotExist(Username authenticationId, Username authorizationId, String reason) { + return new SaslFailure(Type.USER_DOES_NOT_EXIST, Optional.of(authenticationId), Optional.of(authorizationId), reason, Optional.empty()); + } + + public static SaslFailure delegationForbidden(Username authenticationId, Username authorizationId, String reason) { + return new SaslFailure(Type.DELEGATION_FORBIDDEN, Optional.of(authenticationId), Optional.of(authorizationId), reason, Optional.empty()); + } + + public static SaslFailure serverError(Optional<Username> authenticationId, Optional<Username> authorizationId, String reason, Throwable cause) { + return new SaslFailure(Type.SERVER_ERROR, authenticationId, authorizationId, reason, Optional.ofNullable(cause)); + } +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslIdentity.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslIdentity.java new file mode 100644 index 0000000000..7038651fcd --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslIdentity.java @@ -0,0 +1,31 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import org.apache.james.core.Username; + +/** + * SASL authentication and authorization identities. + * + * @param authenticationId identity proven by the SASL mechanism + * @param authorizationId identity requested for protocol access + */ +public record SaslIdentity(Username authenticationId, Username authorizationId) { +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslInitialRequest.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslInitialRequest.java new file mode 100644 index 0000000000..4e245eebd1 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslInitialRequest.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import java.util.Optional; + +/** + * Protocol-neutral initial SASL request. + * + * @param mechanismName requested SASL mechanism name + * @param initialResponse decoded initial client response, when supplied by the client + */ +public record SaslInitialRequest(String mechanismName, Optional<byte[]> initialResponse) { + public SaslInitialRequest(String mechanismName, Optional<byte[]> initialResponse) { + this.mechanismName = mechanismName; + this.initialResponse = initialResponse.map(byte[]::clone); + } + + /** + * Returns a defensive copy of the decoded initial client response. + */ + public Optional<byte[]> initialResponse() { + return initialResponse.map(byte[]::clone); + } +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanism.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanism.java new file mode 100644 index 0000000000..3be6de623b --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanism.java @@ -0,0 +1,44 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +/** + * Protocol-neutral SASL mechanism. + */ +public interface SaslMechanism { + /** + * Returns the SASL mechanism name advertised to clients. + */ + String name(); + + /** + * Starts a new SASL exchange for one client authentication attempt. + */ + SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator); + + /** + * Whether this mechanism may be used over the current transport. + * + * @param channelEncrypted whether the underlying transport is encrypted, for example with TLS. + */ + default boolean isAvailableOnTransport(boolean channelEncrypted) { + return true; + } +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismFactory.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismFactory.java new file mode 100644 index 0000000000..77fb9e5e70 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismFactory.java @@ -0,0 +1,31 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import org.apache.commons.configuration2.HierarchicalConfiguration; +import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.commons.configuration2.tree.ImmutableNode; + +/** + * Creates a SASL mechanism for one server configuration block. + */ +public interface SaslMechanismFactory { + SaslMechanism create(HierarchicalConfiguration<ImmutableNode> serverConfiguration) throws ConfigurationException; +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismNames.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismNames.java new file mode 100644 index 0000000000..70e76d0503 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslMechanismNames.java @@ -0,0 +1,29 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +public final class SaslMechanismNames { + public static final String PLAIN = "PLAIN"; + public static final String OAUTHBEARER = "OAUTHBEARER"; + public static final String XOAUTH2 = "XOAUTH2"; + + private SaslMechanismNames() { + } +} diff --git a/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslStep.java b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslStep.java new file mode 100644 index 0000000000..37608f30d5 --- /dev/null +++ b/protocols/api/src/main/java/org/apache/james/protocols/api/sasl/SaslStep.java @@ -0,0 +1,66 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import java.util.Optional; + +/** + * Server step produced by a SASL exchange. + */ +public sealed interface SaslStep permits SaslStep.Challenge, SaslStep.Success, SaslStep.Failure { + /** + * Server challenge to send back to the client. + */ + record Challenge(Optional<byte[]> payload) implements SaslStep { + public Challenge(Optional<byte[]> payload) { + this.payload = payload.map(byte[]::clone); + } + + /** + * Returns a defensive copy of the decoded challenge payload. + */ + public Optional<byte[]> payload() { + return payload.map(byte[]::clone); + } + } + + /** + * Successful SASL exchange result. + */ + record Success(SaslIdentity identity, Optional<byte[]> serverData) implements SaslStep { + public Success(SaslIdentity identity, Optional<byte[]> serverData) { + this.identity = identity; + this.serverData = serverData.map(byte[]::clone); + } + + /** + * Returns a defensive copy of the decoded final server data. + */ + public Optional<byte[]> serverData() { + return serverData.map(byte[]::clone); + } + } + + /** + * Failed SASL exchange result. + */ + record Failure(SaslFailure failure) implements SaslStep { + } +} diff --git a/protocols/api/src/test/java/org/apache/james/protocols/api/sasl/SaslMechanismContractTest.java b/protocols/api/src/test/java/org/apache/james/protocols/api/sasl/SaslMechanismContractTest.java new file mode 100644 index 0000000000..2b4054284a --- /dev/null +++ b/protocols/api/src/test/java/org/apache/james/protocols/api/sasl/SaslMechanismContractTest.java @@ -0,0 +1,249 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.protocols.api.sasl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import org.apache.james.core.Username; +import org.junit.jupiter.api.Test; + +/** + * Validates the shared SASL SPI shape with fake mechanisms before wiring real protocol mechanisms to it. + */ +class SaslMechanismContractTest { + private static final Username AUTHENTICATION_ID = Username.of("[email protected]"); + private static final Username AUTHORIZATION_ID = Username.of("[email protected]"); + private static final SaslIdentity SAME_USER_IDENTITY = new SaslIdentity(AUTHENTICATION_ID, AUTHENTICATION_ID); + private static final SaslIdentity DELEGATED_IDENTITY = new SaslIdentity(AUTHENTICATION_ID, AUTHORIZATION_ID); + private static final SaslAuthenticator NOOP_AUTHENTICATOR = new SaslAuthenticator() { + @Override + public SaslAuthenticationResult authenticatePassword(Username authenticationId, Optional<Username> authorizationId, String password) { + return new SaslAuthenticationResult.Failure(SaslFailure.invalidCredentials(authenticationId, authorizationId, "unused")); + } + + @Override + public SaslAuthenticationResult authorize(SaslIdentity identity) { + return new SaslAuthenticationResult.Success(identity); + } + }; + + /** + * Models one-step mechanisms that can immediately succeed or fail on the first server step. + */ + private static class FixedStepMechanism implements SaslMechanism { + private final SaslStep firstStep; + + private FixedStepMechanism(SaslStep firstStep) { + this.firstStep = firstStep; + } + + @Override + public String name() { + return "FIXED"; + } + + @Override + public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) { + return new FixedStepExchange(firstStep); + } + } + + private static class FixedStepExchange implements SaslExchange { + private final SaslStep firstStep; + private boolean aborted; + private boolean closed; + + private FixedStepExchange(SaslStep firstStep) { + this.firstStep = firstStep; + } + + @Override + public SaslStep firstStep() { + return firstStep; + } + + @Override + public SaslStep onResponse(byte[] clientResponse) { + return firstStep; + } + + @Override + public void abort() { + aborted = true; + } + + @Override + public void close() { + closed = true; + } + } + + /** + * Models challenge/response mechanisms where state must survive between client lines. + */ + private static class TwoStepMechanism implements SaslMechanism { + @Override + public String name() { + return "TWO_STEP"; + } + + @Override + public SaslExchange start(SaslInitialRequest request, SaslAuthenticator authenticator) { + return new TwoStepExchange(); + } + } + + private static class TwoStepExchange implements SaslExchange { + private boolean challenged; + + @Override + public SaslStep firstStep() { + challenged = true; + return new SaslStep.Challenge(Optional.of(bytes("continue"))); + } + + @Override + public SaslStep onResponse(byte[] clientResponse) { + if (!challenged) { + return new SaslStep.Failure(SaslFailure.malformed("response received before challenge")); + } + if (new String(clientResponse, StandardCharsets.UTF_8).equals("accepted")) { + return new SaslStep.Success(SAME_USER_IDENTITY, Optional.empty()); + } + return new SaslStep.Failure(SaslFailure.invalidCredentials(AUTHENTICATION_ID, Optional.empty(), "rejected")); + } + + @Override + public void abort() { + } + + @Override + public void close() { + } + } + + @Test + void oneStepMechanismShouldReturnSuccess() { + // GIVEN a one-step mechanism configured to immediately succeed + SaslStep.Success success = new SaslStep.Success(SAME_USER_IDENTITY, Optional.empty()); + SaslExchange exchange = new FixedStepMechanism(success).start(initialRequest(Optional.empty()), NOOP_AUTHENTICATOR); + + // WHEN the exchange starts + SaslStep firstStep = exchange.firstStep(); + + // THEN the mechanism can complete without a client continuation + assertThat(firstStep).isEqualTo(success); + } + + @Test + void oneStepMechanismShouldReturnFailure() { + // GIVEN a one-step mechanism configured to immediately fail + SaslStep.Failure failure = new SaslStep.Failure(SaslFailure.malformed("failure")); + SaslExchange exchange = new FixedStepMechanism(failure).start(initialRequest(Optional.empty()), NOOP_AUTHENTICATOR); + + // WHEN the exchange starts + SaslStep firstStep = exchange.firstStep(); + + // THEN the mechanism can fail without a client continuation + assertThat(firstStep).isEqualTo(failure); + } + + @Test + void multiStepMechanismShouldKeepStateAcrossResponses() { + // GIVEN a mechanism that requires one challenge before accepting a response + SaslExchange exchange = new TwoStepMechanism().start(initialRequest(Optional.empty()), NOOP_AUTHENTICATOR); + + // WHEN the server sends a challenge and later receives the expected client response + SaslStep firstStep = exchange.firstStep(); + SaslStep success = exchange.onResponse(bytes("accepted")); + + // THEN the exchange keeps enough state to complete after the continuation + assertThat(firstStep).isInstanceOf(SaslStep.Challenge.class); + assertThat(((SaslStep.Success) success).identity()).isEqualTo(SAME_USER_IDENTITY); + } + + @Test + void successStepShouldPreserveDelegatedIdentity() { + // GIVEN a self-authenticating mechanism returning a delegated identity + SaslExchange exchange = new FixedStepMechanism(new SaslStep.Success(DELEGATED_IDENTITY, Optional.empty())) + .start(initialRequest(Optional.empty()), NOOP_AUTHENTICATOR); + + // WHEN the exchange starts + SaslStep firstStep = exchange.firstStep(); + + // THEN the identity keeps both authentication and authorization users + assertThat(((SaslStep.Success) firstStep).identity()).isEqualTo(DELEGATED_IDENTITY); + } + + @Test + void initialRequestShouldDefensivelyCopyInitialResponse() { + // GIVEN an initial response backed by a mutable byte array + byte[] initialResponse = bytes("initial"); + SaslInitialRequest request = initialRequest(Optional.of(initialResponse)); + + // WHEN the caller mutates the original array + initialResponse[0] = 'I'; + + // THEN the request keeps the original payload + assertThat(request.initialResponse()).hasValueSatisfying(value -> assertThat(value).containsExactly(bytes("initial"))); + } + + @Test + void saslStepsShouldDefensivelyCopyPayloads() { + // GIVEN challenge and success steps backed by mutable byte arrays + byte[] challengePayload = bytes("challenge"); + byte[] serverData = bytes("server"); + SaslStep.Challenge challenge = new SaslStep.Challenge(Optional.of(challengePayload)); + SaslStep.Success success = new SaslStep.Success(SAME_USER_IDENTITY, Optional.of(serverData)); + + // WHEN the caller mutates the original arrays + challengePayload[0] = 'C'; + serverData[0] = 'S'; + + // THEN the SASL steps keep their original payloads + assertThat(challenge.payload()).hasValueSatisfying(value -> assertThat(value).containsExactly(bytes("challenge"))); + assertThat(success.serverData()).hasValueSatisfying(value -> assertThat(value).containsExactly(bytes("server"))); + } + + @Test + void exchangeShouldExposeAbortAndCloseLifecycle() { + // GIVEN an active exchange + FixedStepExchange exchange = new FixedStepExchange(new SaslStep.Failure(SaslFailure.malformed("failure"))); + + // WHEN the protocol aborts and then closes it + exchange.abort(); + exchange.close(); + + // THEN mechanisms can observe both lifecycle events + assertThat(exchange.aborted).isTrue(); + assertThat(exchange.closed).isTrue(); + } + + private static SaslInitialRequest initialRequest(Optional<byte[]> initialResponse) { + return new SaslInitialRequest("TEST", initialResponse); + } + + private static byte[] bytes(String value) { + return value.getBytes(StandardCharsets.UTF_8); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
