ClientAuthTestBase.java
package redis.clients.jedis.tls;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import io.redis.test.annotations.ConditionalOnEnv;
import io.redis.test.utils.RedisVersion;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.extension.RegisterExtension;
import redis.clients.jedis.EndpointConfig;
import redis.clients.jedis.Endpoints;
import redis.clients.jedis.SslOptions;
import redis.clients.jedis.SslVerifyMode;
import redis.clients.jedis.UnifiedJedis;
import redis.clients.jedis.util.EnvCondition;
import redis.clients.jedis.util.RedisVersionUtil;
import redis.clients.jedis.util.TestEnvUtil;
import redis.clients.jedis.util.TlsUtil;
/**
* Abstract base class for mTLS (mutual TLS) authentication tests.
* <p>
* This class provides common setup for tests that verify certificate-based client authentication.
* It configures both truststore (for server verification) and keystore (for client authentication).
* <p>
* The mTLS setup requires: - A truststore containing the CA certificate to verify the Redis server
* - A keystore containing the client certificate and private key for client authentication
*/
@ConditionalOnEnv(value = TestEnvUtil.ENV_OSS_SOURCE, enabled = false)
public abstract class ClientAuthTestBase {
private static final String TRUSTSTORE_PASSWORD = "changeit";
private static final String KEYSTORE_PASSWORD = "changeit";
/** Default mTLS user for testing */
protected static final String MTLS_USER_1 = "mtls-user1";
protected static final String MTLS_USER_2 = "mtls-user2";
protected static final String MTLS_USER_WITHOUT_ACL = "mtls-user-without-acl";
@RegisterExtension
public static EnvCondition envCondition = new EnvCondition();
protected static EndpointConfig endpoint;
protected static Path trustStorePath;
protected static Path keyStorePath1;
protected static Path keyStorePath2;
protected static Path keyStorePathUserWithoutAcl;
/**
* Sets up mTLS stores for a specific targetEndpioint. Should be called by subclasses in
* their @BeforeAll method.
* @param targetEndpioint the targetEndpioint to configure mTLS for
* @param testClassName the test class name for truststore naming
*/
protected static void setUpMtlsStoresForEndpoint(EndpointConfig targetEndpioint,
String testClassName) {
// Create truststore with CA certificate for server verification
List<Path> trustedCertLocation = Collections
.singletonList(targetEndpioint.getCertificatesLocation());
trustStorePath = TlsUtil.createAndSaveTestTruststore(testClassName, trustedCertLocation,
TRUSTSTORE_PASSWORD);
TlsUtil.setCustomTrustStore(trustStorePath, TRUSTSTORE_PASSWORD);
// Use pre-generated PKCS12 keystores from Docker container
// The container generates .p12 files with password "changeit" for each TLS_CLIENT_CNS entry
Path certLocation = targetEndpioint.getCertificatesLocation();
keyStorePath1 = TlsUtil.clientKeystorePath(certLocation, MTLS_USER_1);
keyStorePath2 = TlsUtil.clientKeystorePath(certLocation, MTLS_USER_2);
keyStorePathUserWithoutAcl = TlsUtil.clientKeystorePath(certLocation, MTLS_USER_WITHOUT_ACL);
}
@AfterAll
public static void tearDownMtlsStores() {
TlsUtil.restoreOriginalTrustStore();
}
/**
* Creates SslOptions configured for mTLS with the specified client keystore.
* @param keystorePath path to the client keystore
* @return SslOptions configured for mTLS
*/
protected static SslOptions createMtlsSslOptions(Path keystorePath) {
return SslOptions.builder().truststore(trustStorePath.toFile()).trustStoreType("jceks")
.keystore(keystorePath.toFile(), KEYSTORE_PASSWORD.toCharArray()).keyStoreType("PKCS12")
.sslVerifyMode(SslVerifyMode.FULL).build();
}
/**
* Creates SslOptions for mtls-user1.
*/
protected static SslOptions createMtlsSslOptionsUser1() {
return createMtlsSslOptions(keyStorePath1);
}
/**
* Creates SslOptions for mtls-user2.
*/
protected static SslOptions createMtlsSslOptionsUser2() {
return createMtlsSslOptions(keyStorePath2);
}
/**
* Creates SslOptions for mtls-user-without-acl.
*/
protected static SslOptions createMtlsSslOptionsUserWithoutAcl() {
return createMtlsSslOptions(keyStorePathUserWithoutAcl);
}
/**
* Asserts the expected username based on Redis version.
* <p>
* Redis 8.6+ supports automatic certificate-based authentication via tls-auth-clients-user CN,
* where the username is extracted from the client certificate's Common Name. For Redis versions
* below 8.6, the user will be "default" since cert-based auth is not supported.
* @param jedis the connected Jedis client to check version
* @param actualUsername the actual username from ACL WHOAMI
* @param expectedCertUser the expected username from client certificate CN
*/
protected static void assertExpectedUsername(redis.clients.jedis.Jedis jedis,
String actualUsername, String expectedCertUser) {
RedisVersion version = RedisVersionUtil.getRedisVersion(jedis);
assertUsernameForVersion(version, actualUsername, expectedCertUser);
}
/**
* Asserts the expected username based on Redis version for UnifiedJedis clients (RedisClient,
* RedisClusterClient, etc.).
* @param jedis the connected UnifiedJedis client to check version
* @param actualUsername the actual username from ACL WHOAMI
* @param expectedCertUser the expected username from client certificate CN
*/
protected static void assertExpectedUsername(UnifiedJedis jedis, String actualUsername,
String expectedCertUser) {
RedisVersion version = RedisVersionUtil.getRedisVersion(jedis);
assertUsernameForVersion(version, actualUsername, expectedCertUser);
}
private static void assertUsernameForVersion(RedisVersion version, String actualUsername,
String expectedCertUser) {
if (version.isGreaterThanOrEqualTo(RedisVersion.V8_6_0)) {
assertEquals(expectedCertUser, actualUsername,
"Redis " + version + " supports cert-based auth, expected username from certificate CN");
} else {
List<String> allowedUsers = Arrays.asList("default", expectedCertUser);
assertTrue(allowedUsers.contains(actualUsername),
"Redis " + version + " does not support cert-based auth, expected 'default' or '"
+ expectedCertUser + "' but was '" + actualUsername + "'");
}
}
}