/*
* 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.kafka.common.security.authenticator;
import org.apache.kafka.clients.NetworkClient;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.config.SaslConfigs;
import org.apache.kafka.common.config.types.Password;
import org.apache.kafka.common.network.CertStores;
import org.apache.kafka.common.network.ChannelBuilder;
import org.apache.kafka.common.network.ChannelBuilders;
import org.apache.kafka.common.network.ChannelState;
import org.apache.kafka.common.network.ListenerName;
import org.apache.kafka.common.network.NetworkSend;
import org.apache.kafka.common.network.NetworkTestUtils;
import org.apache.kafka.common.network.NioEchoServer;
import org.apache.kafka.common.network.Selector;
import org.apache.kafka.common.network.Send;
import org.apache.kafka.common.protocol.ApiKeys;
import org.apache.kafka.common.protocol.Errors;
import org.apache.kafka.common.protocol.SecurityProtocol;
import org.apache.kafka.common.requests.AbstractRequest;
import org.apache.kafka.common.requests.AbstractResponse;
import org.apache.kafka.common.requests.ApiVersionsRequest;
import org.apache.kafka.common.requests.ApiVersionsResponse;
import org.apache.kafka.common.requests.MetadataRequest;
import org.apache.kafka.common.requests.RequestHeader;
import org.apache.kafka.common.requests.ResponseHeader;
import org.apache.kafka.common.requests.SaslHandshakeRequest;
import org.apache.kafka.common.requests.SaslHandshakeResponse;
import org.apache.kafka.common.security.JaasContext;
import org.apache.kafka.common.security.TestSecurityConfig;
import org.apache.kafka.common.security.plain.PlainLoginModule;
import org.apache.kafka.common.security.scram.ScramCredential;
import org.apache.kafka.common.security.scram.ScramFormatter;
import org.apache.kafka.common.security.scram.ScramLoginModule;
import org.apache.kafka.common.security.scram.ScramMechanism;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.security.auth.login.Configuration;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* Tests for the Sasl authenticator. These use a test harness that runs a simple socket server that echos back responses.
*/
public class SaslAuthenticatorTest {
private static final int BUFFER_SIZE = 4 * 1024;
private NioEchoServer server;
private Selector selector;
private ChannelBuilder channelBuilder;
private CertStores serverCertStores;
private CertStores clientCertStores;
private Map<String, Object> saslClientConfigs;
private Map<String, Object> saslServerConfigs;
@Before
public void setup() throws Exception {
serverCertStores = new CertStores(true, "localhost");
clientCertStores = new CertStores(false, "localhost");
saslServerConfigs = serverCertStores.getTrustingConfig(clientCertStores);
saslClientConfigs = clientCertStores.getTrustingConfig(serverCertStores);
}
@After
public void teardown() throws Exception {
if (server != null)
this.server.close();
if (selector != null)
this.selector.close();
}
/**
* Tests good path SASL/PLAIN client and server channels using SSL transport layer.
*/
@Test
public void testValidSaslPlainOverSsl() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
createAndCheckClientConnection(securityProtocol, node);
}
/**
* Tests good path SASL/PLAIN client and server channels using PLAINTEXT transport layer.
*/
@Test
public void testValidSaslPlainOverPlaintext() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
createAndCheckClientConnection(securityProtocol, node);
}
/**
* Tests that SASL/PLAIN clients with invalid password fail authentication.
*/
@Test
public void testInvalidPasswordSaslPlain() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
TestJaasConfig jaasConfig = configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
jaasConfig.setPlainClientOptions(TestJaasConfig.USERNAME, "invalidpassword");
server = createEchoServer(securityProtocol);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests that SASL/PLAIN clients with invalid username fail authentication.
*/
@Test
public void testInvalidUsernameSaslPlain() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
TestJaasConfig jaasConfig = configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
jaasConfig.setPlainClientOptions("invaliduser", TestJaasConfig.PASSWORD);
server = createEchoServer(securityProtocol);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests that SASL/PLAIN clients without valid username fail authentication.
*/
@Test
public void testMissingUsernameSaslPlain() throws Exception {
String node = "0";
TestJaasConfig jaasConfig = configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
jaasConfig.setPlainClientOptions(null, "mypassword");
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
server = createEchoServer(securityProtocol);
createSelector(securityProtocol, saslClientConfigs);
InetSocketAddress addr = new InetSocketAddress("127.0.0.1", server.port());
try {
selector.connect(node, addr, BUFFER_SIZE, BUFFER_SIZE);
fail("SASL/PLAIN channel created without username");
} catch (KafkaException e) {
// Expected exception
}
}
/**
* Tests that SASL/PLAIN clients with missing password in JAAS configuration fail authentication.
*/
@Test
public void testMissingPasswordSaslPlain() throws Exception {
String node = "0";
TestJaasConfig jaasConfig = configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
jaasConfig.setPlainClientOptions("myuser", null);
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
server = createEchoServer(securityProtocol);
createSelector(securityProtocol, saslClientConfigs);
InetSocketAddress addr = new InetSocketAddress("127.0.0.1", server.port());
try {
selector.connect(node, addr, BUFFER_SIZE, BUFFER_SIZE);
fail("SASL/PLAIN channel created without password");
} catch (KafkaException e) {
// Expected exception
}
}
/**
* Tests that mechanisms that are not supported in Kafka can be plugged in without modifying
* Kafka code if Sasl client and server providers are available.
*/
@Test
public void testMechanismPluggability() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("DIGEST-MD5", Arrays.asList("DIGEST-MD5"));
server = createEchoServer(securityProtocol);
createAndCheckClientConnection(securityProtocol, node);
}
/**
* Tests that servers supporting multiple SASL mechanisms work with clients using
* any of the enabled mechanisms.
*/
@Test
public void testMultipleServerMechanisms() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("DIGEST-MD5", Arrays.asList("DIGEST-MD5", "PLAIN", "SCRAM-SHA-256"));
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
String node1 = "1";
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "PLAIN");
createAndCheckClientConnection(securityProtocol, node1);
String node2 = "2";
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "DIGEST-MD5");
createSelector(securityProtocol, saslClientConfigs);
InetSocketAddress addr = new InetSocketAddress("127.0.0.1", server.port());
selector.connect(node2, addr, BUFFER_SIZE, BUFFER_SIZE);
NetworkTestUtils.checkClientConnection(selector, node2, 100, 10);
String node3 = "3";
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256");
createSelector(securityProtocol, saslClientConfigs);
selector.connect(node3, new InetSocketAddress("127.0.0.1", server.port()), BUFFER_SIZE, BUFFER_SIZE);
NetworkTestUtils.checkClientConnection(selector, node3, 100, 10);
}
/**
* Tests good path SASL/SCRAM-SHA-256 client and server channels.
*/
@Test
public void testValidSaslScramSha256() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("SCRAM-SHA-256", Arrays.asList("SCRAM-SHA-256"));
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
createAndCheckClientConnection(securityProtocol, "0");
}
/**
* Tests all supported SCRAM client and server channels. Also tests that all
* supported SCRAM mechanisms can be supported simultaneously on a server.
*/
@Test
public void testValidSaslScramMechanisms() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("SCRAM-SHA-256", new ArrayList<>(ScramMechanism.mechanismNames()));
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
for (String mechanism : ScramMechanism.mechanismNames()) {
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, mechanism);
createAndCheckClientConnection(securityProtocol, "node-" + mechanism);
}
}
/**
* Tests that SASL/SCRAM clients fail authentication if password is invalid.
*/
@Test
public void testInvalidPasswordSaslScram() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
TestJaasConfig jaasConfig = configureMechanisms("SCRAM-SHA-256", Arrays.asList("SCRAM-SHA-256"));
Map<String, Object> options = new HashMap<>();
options.put("username", TestJaasConfig.USERNAME);
options.put("password", "invalidpassword");
jaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_CLIENT, ScramLoginModule.class.getName(), options);
String node = "0";
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests that SASL/SCRAM clients without valid username fail authentication.
*/
@Test
public void testUnknownUserSaslScram() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
TestJaasConfig jaasConfig = configureMechanisms("SCRAM-SHA-256", Arrays.asList("SCRAM-SHA-256"));
Map<String, Object> options = new HashMap<>();
options.put("username", "unknownUser");
options.put("password", TestJaasConfig.PASSWORD);
jaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_CLIENT, ScramLoginModule.class.getName(), options);
String node = "0";
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests that SASL/SCRAM clients fail authentication if credentials are not available for
* the specific SCRAM mechanism.
*/
@Test
public void testUserCredentialsUnavailableForScramMechanism() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("SCRAM-SHA-256", new ArrayList<>(ScramMechanism.mechanismNames()));
server = createEchoServer(securityProtocol);
updateScramCredentialCache(TestJaasConfig.USERNAME, TestJaasConfig.PASSWORD);
server.credentialCache().cache(ScramMechanism.SCRAM_SHA_256.mechanismName(), ScramCredential.class).remove(TestJaasConfig.USERNAME);
String node = "1";
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-256");
createAndCheckClientConnectionFailure(securityProtocol, node);
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "SCRAM-SHA-512");
createAndCheckClientConnection(securityProtocol, "2");
}
/**
* Tests SASL/SCRAM with username containing characters that need
* to be encoded.
*/
@Test
public void testScramUsernameWithSpecialCharacters() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
String username = "special user= test,scram";
String password = username + "-password";
TestJaasConfig jaasConfig = configureMechanisms("SCRAM-SHA-256", Arrays.asList("SCRAM-SHA-256"));
Map<String, Object> options = new HashMap<>();
options.put("username", username);
options.put("password", password);
jaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_CLIENT, ScramLoginModule.class.getName(), options);
server = createEchoServer(securityProtocol);
updateScramCredentialCache(username, password);
createAndCheckClientConnection(securityProtocol, "0");
}
/**
* Tests that Kafka ApiVersionsRequests are handled by the SASL server authenticator
* prior to SASL handshake flow and that subsequent authentication succeeds
* when transport layer is PLAINTEXT. This test simulates SASL authentication using a
* (non-SASL) PLAINTEXT client and sends ApiVersionsRequest straight after
* connection to the server is established, before any SASL-related packets are sent.
*/
@Test
public void testUnauthenticatedApiVersionsRequestOverPlaintext() throws Exception {
testUnauthenticatedApiVersionsRequest(SecurityProtocol.SASL_PLAINTEXT);
}
/**
* Tests that Kafka ApiVersionsRequests are handled by the SASL server authenticator
* prior to SASL handshake flow and that subsequent authentication succeeds
* when transport layer is SSL. This test simulates SASL authentication using a
* (non-SASL) SSL client and sends ApiVersionsRequest straight after
* SSL handshake, before any SASL-related packets are sent.
*/
@Test
public void testUnauthenticatedApiVersionsRequestOverSsl() throws Exception {
testUnauthenticatedApiVersionsRequest(SecurityProtocol.SASL_SSL);
}
/**
* Tests that unsupported version of ApiVersionsRequest before SASL handshake request
* returns error response and does not result in authentication failure. This test
* is similar to {@link #testUnauthenticatedApiVersionsRequest(SecurityProtocol)}
* where a non-SASL client is used to send requests that are processed by
* {@link SaslServerAuthenticator} of the server prior to client authentication.
*/
@Test
public void testApiVersionsRequestWithUnsupportedVersion() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send ApiVersionsRequest with unsupported version and validate error response.
String node = "1";
createClientConnection(SecurityProtocol.PLAINTEXT, node);
RequestHeader header = new RequestHeader(ApiKeys.API_VERSIONS.id, Short.MAX_VALUE, "someclient", 1);
ApiVersionsRequest request = new ApiVersionsRequest.Builder().build();
selector.send(request.toSend(node, header));
ByteBuffer responseBuffer = waitForResponse();
ResponseHeader.parse(responseBuffer);
ApiVersionsResponse response = ApiVersionsResponse.parse(responseBuffer, (short) 0);
assertEquals(Errors.UNSUPPORTED_VERSION, response.error());
// Send ApiVersionsRequest with a supported version. This should succeed.
sendVersionRequestReceiveResponse(node);
// Test that client can authenticate successfully
sendHandshakeRequestReceiveResponse(node);
authenticateUsingSaslPlainAndCheckConnection(node);
}
/**
* Tests that unsupported version of SASL handshake request returns error
* response and fails authentication. This test is similar to
* {@link #testUnauthenticatedApiVersionsRequest(SecurityProtocol)}
* where a non-SASL client is used to send requests that are processed by
* {@link SaslServerAuthenticator} of the server prior to client authentication.
*/
@Test
public void testSaslHandshakeRequestWithUnsupportedVersion() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send ApiVersionsRequest and validate error response.
String node1 = "invalid1";
createClientConnection(SecurityProtocol.PLAINTEXT, node1);
SaslHandshakeRequest request = new SaslHandshakeRequest("PLAIN");
RequestHeader header = new RequestHeader(ApiKeys.SASL_HANDSHAKE.id, Short.MAX_VALUE, "someclient", 2);
selector.send(request.toSend(node1, header));
NetworkTestUtils.waitForChannelClose(selector, node1, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good1");
}
/**
* Tests that any invalid data during Kafka SASL handshake request flow
* or the actual SASL authentication flow result in authentication failure
* and do not cause any failures in the server.
*/
@Test
public void testInvalidSaslPacket() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send invalid SASL packet after valid handshake request
String node1 = "invalid1";
createClientConnection(SecurityProtocol.PLAINTEXT, node1);
sendHandshakeRequestReceiveResponse(node1);
Random random = new Random();
byte[] bytes = new byte[1024];
random.nextBytes(bytes);
selector.send(new NetworkSend(node1, ByteBuffer.wrap(bytes)));
NetworkTestUtils.waitForChannelClose(selector, node1, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good1");
// Send invalid SASL packet before handshake request
String node2 = "invalid2";
createClientConnection(SecurityProtocol.PLAINTEXT, node2);
random.nextBytes(bytes);
selector.send(new NetworkSend(node2, ByteBuffer.wrap(bytes)));
NetworkTestUtils.waitForChannelClose(selector, node2, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good2");
}
/**
* Tests that ApiVersionsRequest after Kafka SASL handshake request flow,
* but prior to actual SASL authentication, results in authentication failure.
* This is similar to {@link #testUnauthenticatedApiVersionsRequest(SecurityProtocol)}
* where a non-SASL client is used to send requests that are processed by
* {@link SaslServerAuthenticator} of the server prior to client authentication.
*/
@Test
public void testInvalidApiVersionsRequestSequence() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send handshake request followed by ApiVersionsRequest
String node1 = "invalid1";
createClientConnection(SecurityProtocol.PLAINTEXT, node1);
sendHandshakeRequestReceiveResponse(node1);
ApiVersionsRequest request = new ApiVersionsRequest.Builder().build();
RequestHeader versionsHeader = new RequestHeader(ApiKeys.API_VERSIONS.id,
request.version(), "someclient", 2);
selector.send(request.toSend(node1, versionsHeader));
NetworkTestUtils.waitForChannelClose(selector, node1, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good1");
}
/**
* Tests that packets that are too big during Kafka SASL handshake request flow
* or the actual SASL authentication flow result in authentication failure
* and do not cause any failures in the server.
*/
@Test
public void testPacketSizeTooBig() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send SASL packet with large size after valid handshake request
String node1 = "invalid1";
createClientConnection(SecurityProtocol.PLAINTEXT, node1);
sendHandshakeRequestReceiveResponse(node1);
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.putInt(Integer.MAX_VALUE);
buffer.put(new byte[buffer.capacity() - 4]);
buffer.rewind();
selector.send(new NetworkSend(node1, buffer));
NetworkTestUtils.waitForChannelClose(selector, node1, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good1");
// Send packet with large size before handshake request
String node2 = "invalid2";
createClientConnection(SecurityProtocol.PLAINTEXT, node2);
buffer.clear();
buffer.putInt(Integer.MAX_VALUE);
buffer.put(new byte[buffer.capacity() - 4]);
buffer.rewind();
selector.send(new NetworkSend(node2, buffer));
NetworkTestUtils.waitForChannelClose(selector, node2, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good2");
}
/**
* Tests that Kafka requests that are forbidden until successful authentication result
* in authentication failure and do not cause any failures in the server.
*/
@Test
public void testDisallowedKafkaRequestsBeforeAuthentication() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Send metadata request before Kafka SASL handshake request
String node1 = "invalid1";
createClientConnection(SecurityProtocol.PLAINTEXT, node1);
MetadataRequest metadataRequest1 =
new MetadataRequest.Builder(Collections.singletonList("sometopic")).build();
RequestHeader metadataRequestHeader1 = new RequestHeader(ApiKeys.METADATA.id,
metadataRequest1.version(), "someclient", 1);
selector.send(metadataRequest1.toSend(node1, metadataRequestHeader1));
NetworkTestUtils.waitForChannelClose(selector, node1, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good1");
// Send metadata request after Kafka SASL handshake request
String node2 = "invalid2";
createClientConnection(SecurityProtocol.PLAINTEXT, node2);
sendHandshakeRequestReceiveResponse(node2);
MetadataRequest metadataRequest2 =
new MetadataRequest.Builder(Collections.singletonList("sometopic")).build();
RequestHeader metadataRequestHeader2 = new RequestHeader(ApiKeys.METADATA.id,
metadataRequest2.version(), "someclient", 2);
selector.send(metadataRequest2.toSend(node2, metadataRequestHeader2));
NetworkTestUtils.waitForChannelClose(selector, node2, ChannelState.READY);
selector.close();
// Test good connection still works
createAndCheckClientConnection(securityProtocol, "good2");
}
/**
* Tests that connections cannot be created if the login module class is unavailable.
*/
@Test
public void testInvalidLoginModule() throws Exception {
TestJaasConfig jaasConfig = configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
jaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_CLIENT, "InvalidLoginModule", TestJaasConfig.defaultClientOptions());
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
server = createEchoServer(securityProtocol);
try {
createSelector(securityProtocol, saslClientConfigs);
fail("SASL/PLAIN channel created without valid login module");
} catch (KafkaException e) {
// Expected exception
}
}
/**
* Tests that mechanisms with default implementation in Kafka may be disabled in
* the Kafka server by removing from the enabled mechanism list.
*/
@Test
public void testDisabledMechanism() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("PLAIN", Arrays.asList("DIGEST-MD5"));
server = createEchoServer(securityProtocol);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests that clients using invalid SASL mechanisms fail authentication.
*/
@Test
public void testInvalidMechanism() throws Exception {
String node = "0";
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "INVALID");
server = createEchoServer(securityProtocol);
createAndCheckClientConnectionFailure(securityProtocol, node);
}
/**
* Tests dynamic JAAS configuration property for SASL clients. Invalid client credentials
* are set in the static JVM-wide configuration instance to ensure that the dynamic
* property override is used during authentication.
*/
@Test
public void testDynamicJaasConfiguration() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_SSL;
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "PLAIN");
saslServerConfigs.put(SaslConfigs.SASL_ENABLED_MECHANISMS, Arrays.asList("PLAIN"));
Map<String, Object> serverOptions = new HashMap<>();
serverOptions.put("user_user1", "user1-secret");
serverOptions.put("user_user2", "user2-secret");
TestJaasConfig staticJaasConfig = new TestJaasConfig();
staticJaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_SERVER, PlainLoginModule.class.getName(),
serverOptions);
staticJaasConfig.setPlainClientOptions("user1", "invalidpassword");
Configuration.setConfiguration(staticJaasConfig);
server = createEchoServer(securityProtocol);
// Check that client using static Jaas config does not connect since password is invalid
createAndCheckClientConnectionFailure(securityProtocol, "1");
// Check that 'user1' can connect with a Jaas config property override
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG, TestJaasConfig.jaasConfigProperty("PLAIN", "user1", "user1-secret"));
createAndCheckClientConnection(securityProtocol, "2");
// Check that invalid password specified as Jaas config property results in connection failure
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG, TestJaasConfig.jaasConfigProperty("PLAIN", "user1", "user2-secret"));
createAndCheckClientConnectionFailure(securityProtocol, "3");
// Check that another user 'user2' can also connect with a Jaas config override without any changes to static configuration
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG, TestJaasConfig.jaasConfigProperty("PLAIN", "user2", "user2-secret"));
createAndCheckClientConnection(securityProtocol, "4");
// Check that clients specifying multiple login modules fail even if the credentials are valid
String module1 = TestJaasConfig.jaasConfigProperty("PLAIN", "user1", "user1-secret").value();
String module2 = TestJaasConfig.jaasConfigProperty("PLAIN", "user2", "user2-secret").value();
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG, new Password(module1 + " " + module2));
try {
createClientConnection(securityProtocol, "1");
fail("Connection created with multiple login modules in sasl.jaas.config");
} catch (IllegalArgumentException e) {
// Expected
}
}
@Test
public void testJaasConfigurationForListener() throws Exception {
SecurityProtocol securityProtocol = SecurityProtocol.SASL_PLAINTEXT;
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, "PLAIN");
saslServerConfigs.put(SaslConfigs.SASL_ENABLED_MECHANISMS, Arrays.asList("PLAIN"));
TestJaasConfig staticJaasConfig = new TestJaasConfig();
Map<String, Object> globalServerOptions = new HashMap<>();
globalServerOptions.put("user_global1", "gsecret1");
globalServerOptions.put("user_global2", "gsecret2");
staticJaasConfig.createOrUpdateEntry(TestJaasConfig.LOGIN_CONTEXT_SERVER, PlainLoginModule.class.getName(),
globalServerOptions);
Map<String, Object> clientListenerServerOptions = new HashMap<>();
clientListenerServerOptions.put("user_client1", "csecret1");
clientListenerServerOptions.put("user_client2", "csecret2");
String clientJaasEntryName = "client." + TestJaasConfig.LOGIN_CONTEXT_SERVER;
staticJaasConfig.createOrUpdateEntry(clientJaasEntryName, PlainLoginModule.class.getName(), clientListenerServerOptions);
Configuration.setConfiguration(staticJaasConfig);
// Listener-specific credentials
server = createEchoServer(new ListenerName("client"), securityProtocol);
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG,
TestJaasConfig.jaasConfigProperty("PLAIN", "client1", "csecret1"));
createAndCheckClientConnection(securityProtocol, "1");
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG,
TestJaasConfig.jaasConfigProperty("PLAIN", "global1", "gsecret1"));
createAndCheckClientConnectionFailure(securityProtocol, "2");
server.close();
// Global credentials as there is no listener-specific JAAS entry
server = createEchoServer(new ListenerName("other"), securityProtocol);
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG,
TestJaasConfig.jaasConfigProperty("PLAIN", "global1", "gsecret1"));
createAndCheckClientConnection(securityProtocol, "3");
saslClientConfigs.put(SaslConfigs.SASL_JAAS_CONFIG,
TestJaasConfig.jaasConfigProperty("PLAIN", "client1", "csecret1"));
createAndCheckClientConnectionFailure(securityProtocol, "4");
}
/**
* Tests that Kafka ApiVersionsRequests are handled by the SASL server authenticator
* prior to SASL handshake flow and that subsequent authentication succeeds
* when transport layer is PLAINTEXT/SSL. This test uses a non-SASL client that simulates
* SASL authentication after ApiVersionsRequest.
* <p>
* Test sequence (using <tt>securityProtocol=PLAINTEXT</tt> as an example):
* <ol>
* <li>Starts a SASL_PLAINTEXT test server that simply echoes back client requests after authentication.</li>
* <li>A (non-SASL) PLAINTEXT test client connects to the SASL server port. Client is now unauthenticated.<./li>
* <li>The unauthenticated non-SASL client sends an ApiVersionsRequest and validates the response.
* A valid response indicates that {@link SaslServerAuthenticator} of the test server responded to
* the ApiVersionsRequest even though the client is not yet authenticated.</li>
* <li>The unauthenticated non-SASL client sends a SaslHandshakeRequest and validates the response. A valid response
* indicates that {@link SaslServerAuthenticator} of the test server responded to the SaslHandshakeRequest
* after processing ApiVersionsRequest.</li>
* <li>The unauthenticated non-SASL client sends the SASL/PLAIN packet containing username/password to authenticate
* itself. The client is now authenticated by the server. At this point this test client is at the
* same state as a regular SASL_PLAINTEXT client that is <tt>ready</tt>.</li>
* <li>The authenticated client sends random data to the server and checks that the data is echoed
* back by the test server (ie, not Kafka request-response) to ensure that the client now
* behaves exactly as a regular SASL_PLAINTEXT client that has completed authentication.</li>
* </ol>
*/
private void testUnauthenticatedApiVersionsRequest(SecurityProtocol securityProtocol) throws Exception {
configureMechanisms("PLAIN", Arrays.asList("PLAIN"));
server = createEchoServer(securityProtocol);
// Create non-SASL connection to manually authenticate after ApiVersionsRequest
String node = "1";
SecurityProtocol clientProtocol;
switch (securityProtocol) {
case SASL_PLAINTEXT:
clientProtocol = SecurityProtocol.PLAINTEXT;
break;
case SASL_SSL:
clientProtocol = SecurityProtocol.SSL;
break;
default:
throw new IllegalArgumentException("Server protocol " + securityProtocol + " is not SASL");
}
createClientConnection(clientProtocol, node);
NetworkTestUtils.waitForChannelReady(selector, node);
// Send ApiVersionsRequest and check response
ApiVersionsResponse versionsResponse = sendVersionRequestReceiveResponse(node);
assertEquals(ApiKeys.SASL_HANDSHAKE.oldestVersion(), versionsResponse.apiVersion(ApiKeys.SASL_HANDSHAKE.id).minVersion);
assertEquals(ApiKeys.SASL_HANDSHAKE.latestVersion(), versionsResponse.apiVersion(ApiKeys.SASL_HANDSHAKE.id).maxVersion);
// Send SaslHandshakeRequest and check response
SaslHandshakeResponse handshakeResponse = sendHandshakeRequestReceiveResponse(node);
assertEquals(Collections.singletonList("PLAIN"), handshakeResponse.enabledMechanisms());
// Complete manual authentication and check send/receive succeed
authenticateUsingSaslPlainAndCheckConnection(node);
}
private void authenticateUsingSaslPlainAndCheckConnection(String node) throws Exception {
// Authenticate using PLAIN username/password
String authString = "\u0000" + TestJaasConfig.USERNAME + "\u0000" + TestJaasConfig.PASSWORD;
selector.send(new NetworkSend(node, ByteBuffer.wrap(authString.getBytes("UTF-8"))));
waitForResponse();
// Check send/receive on the manually authenticated connection
NetworkTestUtils.checkClientConnection(selector, node, 100, 10);
}
private TestJaasConfig configureMechanisms(String clientMechanism, List<String> serverMechanisms) {
saslClientConfigs.put(SaslConfigs.SASL_MECHANISM, clientMechanism);
saslServerConfigs.put(SaslConfigs.SASL_ENABLED_MECHANISMS, serverMechanisms);
return TestJaasConfig.createConfiguration(clientMechanism, serverMechanisms);
}
private void createSelector(SecurityProtocol securityProtocol, Map<String, Object> clientConfigs) {
if (selector != null) {
selector.close();
selector = null;
}
String saslMechanism = (String) saslClientConfigs.get(SaslConfigs.SASL_MECHANISM);
this.channelBuilder = ChannelBuilders.clientChannelBuilder(securityProtocol, JaasContext.Type.CLIENT,
new TestSecurityConfig(clientConfigs), null, saslMechanism, true);
this.selector = NetworkTestUtils.createSelector(channelBuilder);
}
private NioEchoServer createEchoServer(SecurityProtocol securityProtocol) throws Exception {
return createEchoServer(ListenerName.forSecurityProtocol(securityProtocol), securityProtocol);
}
private NioEchoServer createEchoServer(ListenerName listenerName, SecurityProtocol securityProtocol) throws Exception {
return NetworkTestUtils.createEchoServer(listenerName, securityProtocol,
new TestSecurityConfig(saslServerConfigs));
}
private void createClientConnection(SecurityProtocol securityProtocol, String node) throws Exception {
createSelector(securityProtocol, saslClientConfigs);
InetSocketAddress addr = new InetSocketAddress("127.0.0.1", server.port());
selector.connect(node, addr, BUFFER_SIZE, BUFFER_SIZE);
}
private void createAndCheckClientConnection(SecurityProtocol securityProtocol, String node) throws Exception {
createClientConnection(securityProtocol, node);
NetworkTestUtils.checkClientConnection(selector, node, 100, 10);
selector.close();
selector = null;
}
private void createAndCheckClientConnectionFailure(SecurityProtocol securityProtocol, String node) throws Exception {
createClientConnection(securityProtocol, node);
NetworkTestUtils.waitForChannelClose(selector, node, ChannelState.AUTHENTICATE);
selector.close();
selector = null;
}
private AbstractResponse sendKafkaRequestReceiveResponse(String node, ApiKeys apiKey, AbstractRequest request) throws IOException {
RequestHeader header =
new RequestHeader(apiKey.id, request.version(), "someclient", 1);
Send send = request.toSend(node, header);
selector.send(send);
ByteBuffer responseBuffer = waitForResponse();
return NetworkClient.parseResponse(responseBuffer, header);
}
private SaslHandshakeResponse sendHandshakeRequestReceiveResponse(String node) throws Exception {
SaslHandshakeRequest handshakeRequest = new SaslHandshakeRequest("PLAIN");
SaslHandshakeResponse response = (SaslHandshakeResponse) sendKafkaRequestReceiveResponse(node, ApiKeys.SASL_HANDSHAKE, handshakeRequest);
assertEquals(Errors.NONE, response.error());
return response;
}
private ApiVersionsResponse sendVersionRequestReceiveResponse(String node) throws Exception {
ApiVersionsRequest handshakeRequest = new ApiVersionsRequest.Builder().build();
ApiVersionsResponse response = (ApiVersionsResponse) sendKafkaRequestReceiveResponse(node, ApiKeys.API_VERSIONS, handshakeRequest);
assertEquals(Errors.NONE, response.error());
return response;
}
private ByteBuffer waitForResponse() throws IOException {
int waitSeconds = 10;
do {
selector.poll(1000);
} while (selector.completedReceives().isEmpty() && waitSeconds-- > 0);
assertEquals(1, selector.completedReceives().size());
return selector.completedReceives().get(0).payload();
}
@SuppressWarnings("unchecked")
private void updateScramCredentialCache(String username, String password) throws NoSuchAlgorithmException {
for (String mechanism : (List<String>) saslServerConfigs.get(SaslConfigs.SASL_ENABLED_MECHANISMS)) {
ScramMechanism scramMechanism = ScramMechanism.forMechanismName(mechanism);
if (scramMechanism != null) {
ScramFormatter formatter = new ScramFormatter(scramMechanism);
ScramCredential credential = formatter.generateCredential(password, 4096);
server.credentialCache().cache(scramMechanism.mechanismName(), ScramCredential.class).put(username, credential);
}
}
}
}