This is an automated email from the ASF dual-hosted git repository.
jinwoo pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 4e93d2c482 Testcases for Server-Only TLS with Application-Layer
Authentication (#7987)
4e93d2c482 is described below
commit 4e93d2c48296c01cc45e15ca328fd78ead8d9d41
Author: Jinwoo Hwang <[email protected]>
AuthorDate: Wed Mar 4 07:29:24 2026 -0500
Testcases for Server-Only TLS with Application-Layer Authentication (#7987)
---
.../ssl/P2PServerOnlyTLSWithAuthDUnitTest.java | 382 +++++++++++++++
.../cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java | 513 +++++++++++++++++++++
.../ssl/ServerOnlyTLSWithAuthNegativeTest.java | 481 +++++++++++++++++++
.../geode/security/templates/TokenAuthInit.java | 68 +++
.../test/junit/rules/ServerOnlyTLSTestFixture.java | 253 ++++++++++
5 files changed, 1697 insertions(+)
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java
new file mode 100644
index 0000000000..0424c6bd79
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/P2PServerOnlyTLSWithAuthDUnitTest.java
@@ -0,0 +1,382 @@
+/*
+ * 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.geode.cache.ssl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.security.GemFireSecurityException;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.SecurityTest;
+import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture;
+
+/**
+ * Distributed tests for Peer-to-Peer (P2P) Cache Topology using Server-only
TLS with
+ * Application-Layer Authentication (Approach 3).
+ *
+ * <p>
+ * This test demonstrates that in a P2P cache configuration (where all members
are peers, no
+ * client/server distinction), Approach 3 works correctly:
+ * <ul>
+ * <li><strong>TLS Encryption:</strong> All peer-to-peer connections use TLS
for transport
+ * encryption</li>
+ * <li><strong>No Certificate Authentication:</strong> Peers do NOT exchange
certificates during
+ * TLS handshake (ssl-require-authentication=false)</li>
+ * <li><strong>Application-Layer Authentication:</strong> Peers authenticate
using
+ * username/password via SecurityManager</li>
+ * <li><strong>Authorization:</strong> SecurityManager enforces CLUSTER:MANAGE
permission for peer
+ * join</li>
+ * </ul>
+ *
+ * <p>
+ * <strong>Key Difference from Client/Server:</strong> In P2P topology, all
members are equal peers
+ * that communicate directly. Each peer presents a server certificate for TLS
encryption, but
+ * authentication happens at the application layer using credentials validated
by SecurityManager.
+ *
+ * <p>
+ * This approach solves the public CA clientAuth EKU sunset problem for P2P
topologies by:
+ * <ol>
+ * <li>Eliminating the need for client certificates entirely</li>
+ * <li>Maintaining full TLS encryption for all transport</li>
+ * <li>Using existing authentication infrastructure (LDAP, database,
tokens)</li>
+ * </ol>
+ *
+ * @see ServerOnlyTLSWithAuthDUnitTest for client/server topology tests
+ */
+@Category({SecurityTest.class})
+public class P2PServerOnlyTLSWithAuthDUnitTest {
+
+ private static final String REGION_NAME = "testRegion";
+
+ @Rule
+ public ClusterStartupRule cluster = new ClusterStartupRule();
+
+ private ServerOnlyTLSTestFixture fixture;
+
+ @Before
+ public void setUp() throws Exception {
+ fixture = new ServerOnlyTLSTestFixture();
+
+ // Add ignored exceptions for SSL-related cleanup warnings
+ IgnoredException.addIgnoredException("javax.net.ssl.SSLException");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ IgnoredException.addIgnoredException("Authentication failed");
+ IgnoredException.addIgnoredException("Security check failed");
+ }
+
+ /**
+ * Test basic P2P cluster formation with server-only TLS and
application-layer authentication.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Locator and servers present TLS certificates (server cert from public
or private CA)</li>
+ * <li>Peers do NOT present client certificates during TLS handshake</li>
+ * <li>All peers authenticate using username/password</li>
+ * <li>SecurityManager validates credentials and requires CLUSTER:MANAGE
permission</li>
+ * <li>Cluster forms successfully with encrypted peer-to-peer
communication</li>
+ * </ul>
+ */
+ @Test
+ public void testP2PClusterFormationWithServerOnlyTLSAndAppAuth() throws
Exception {
+ // Create certificate stores using fixture
+ // All peers use the same certificate for TLS (server cert)
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Configure locator with:
+ // - Server-only TLS (ssl-require-authentication=false)
+ // - Security manager for application-layer authentication
+ // - Peer authentication credentials
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+
+ // Start locator - it will authenticate itself when joining
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Configure first server with same setup
+ Properties server1Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server1Props);
+ fixture.addPeerAuthProperties(server1Props, "cluster", "cluster");
+
+ // Start first server - it joins via application-layer auth, not
certificate auth
+ MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
+
+ // Verify server1 successfully joined the cluster
+ server1.invoke(() -> {
+ assertThat(ClusterStartupRule.getCache()).isNotNull();
+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
+ .hasSize(1); // locator
+ });
+
+ // Configure second server with same setup
+ Properties server2Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server2Props);
+ fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
+
+ // Start second server
+ MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
+
+ // Verify server2 successfully joined and sees all peers
+ server2.invoke(() -> {
+ assertThat(ClusterStartupRule.getCache()).isNotNull();
+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
+ .hasSize(2); // locator + server1
+ });
+
+ // Verify all peers see each other (peer-to-peer mesh formed)
+ server1.invoke(() -> {
+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
+ .hasSize(2); // locator + server2
+ });
+ }
+
+ /**
+ * Test P2P data replication across encrypted peer connections without
certificate authentication.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Data replicates across peers using TLS-encrypted connections</li>
+ * <li>No certificate authentication is used (application credentials
only)</li>
+ * <li>All operations succeed over server-only TLS</li>
+ * </ul>
+ */
+ @Test
+ public void testP2PDataReplicationOverServerOnlyTLS() throws Exception {
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Configure and start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server1 with replicated region
+ Properties server1Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server1Props);
+ fixture.addPeerAuthProperties(server1Props, "cluster", "cluster");
+ MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
+
+ server1.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Start server2 with same replicated region
+ Properties server2Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server2Props);
+ fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
+ MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
+
+ server2.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Put data on server1
+ server1.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .getRegion(REGION_NAME)
+ .put("key1", "value1");
+ });
+
+ // Verify data replicated to server2 over TLS-encrypted peer connection
+ server2.invoke(() -> {
+ Object value = ClusterStartupRule.getCache()
+ .getRegion(REGION_NAME)
+ .get("key1");
+ assertThat(value).isEqualTo("value1");
+ });
+
+ // Put data on server2
+ server2.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .getRegion(REGION_NAME)
+ .put("key2", "value2");
+ });
+
+ // Verify data replicated to server1
+ server1.invoke(() -> {
+ Object value = ClusterStartupRule.getCache()
+ .getRegion(REGION_NAME)
+ .get("key2");
+ assertThat(value).isEqualTo("value2");
+ });
+ }
+
+ /**
+ * Test that peer with invalid credentials cannot join P2P cluster.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>TLS handshake succeeds (server cert validation)</li>
+ * <li>Application-layer authentication fails with wrong password</li>
+ * <li>Peer is rejected and cannot join cluster</li>
+ * </ul>
+ */
+ @Test
+ public void testP2PPeerRejectedWithInvalidCredentials() throws Exception {
+ // Add ignored exception for authentication failure messages
+ IgnoredException.addIgnoredException("Authentication FAILED");
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Configure and start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Try to start server with INVALID credentials
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "wrongPassword"); //
INVALID
+
+ // Server should fail to join due to authentication failure
+ // Note: Root cause is SecurityException, not GemFireSecurityException
+ assertThatThrownBy(() -> cluster.startServerVM(1, serverProps,
locatorPort))
+ .hasRootCauseInstanceOf(SecurityException.class)
+ .hasStackTraceContaining("invalid username/password");
+ }
+
+ /**
+ * Test that peer without CLUSTER:MANAGE permission cannot join P2P cluster.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>TLS handshake succeeds</li>
+ * <li>Application-layer authentication succeeds (valid
username/password)</li>
+ * <li>Authorization fails (lacks CLUSTER:MANAGE permission)</li>
+ * <li>Peer is rejected and cannot join cluster</li>
+ * </ul>
+ */
+ @Test
+ public void testP2PPeerRejectedWithoutClusterManagePermission() throws
Exception {
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Configure and start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Try to start server with user that has valid credentials but no
CLUSTER:MANAGE
+ // SimpleSecurityManager allows authentication when username == password
+ // but "data" user does NOT have CLUSTER:MANAGE permission
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "data", "data"); // Valid
creds, insufficient perms
+
+ // Server should fail to join due to authorization failure
+ // Note: Root cause is SecurityException, not GemFireSecurityException
+ assertThatThrownBy(() -> cluster.startServerVM(1, serverProps,
locatorPort))
+ .hasRootCauseInstanceOf(SecurityException.class)
+ .hasStackTraceContaining("not authorized for CLUSTER:MANAGE");
+ }
+
+ /**
+ * Test that peer with no credentials cannot join P2P cluster.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>TLS handshake succeeds (encryption established)</li>
+ * <li>Application-layer authentication fails (no credentials provided)</li>
+ * <li>Peer is rejected</li>
+ * </ul>
+ */
+ @Test
+ public void testP2PPeerRejectedWithNoCredentials() throws Exception {
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Configure and start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Try to start server WITHOUT any authentication credentials
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ // NO peer auth properties added - missing credentials
+
+ // Server should fail to join due to missing credentials
+ assertThatThrownBy(() -> cluster.startServerVM(1, serverProps,
locatorPort))
+ .hasRootCauseInstanceOf(GemFireSecurityException.class);
+ }
+
+ /**
+ * Test multiple peers joining with different valid credentials.
+ *
+ * <p>
+ * Verifies that the cluster supports heterogeneous peer credentials as long
as all have
+ * CLUSTER:MANAGE permission. This demonstrates flexibility in credential
management where
+ * different services/teams can use different credentials.
+ */
+ @Test
+ public void testMultiplePeersWithDifferentCredentials() throws Exception {
+ CertStores clusterStores = fixture.createClusterStores();
+
+ // Start locator with "cluster" credentials
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server1 with "clusterManage" credentials
+ // SimpleSecurityManager grants CLUSTER:MANAGE to "cluster" and
"clusterManage" users
+ Properties server1Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server1Props);
+ fixture.addPeerAuthProperties(server1Props, "clusterManage",
"clusterManage");
+ MemberVM server1 = cluster.startServerVM(1, server1Props, locatorPort);
+
+ // Start server2 with "cluster" credentials (same as locator)
+ Properties server2Props = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(server2Props);
+ fixture.addPeerAuthProperties(server2Props, "cluster", "cluster");
+ MemberVM server2 = cluster.startServerVM(2, server2Props, locatorPort);
+
+ // Verify all peers joined successfully
+ server1.invoke(() -> {
+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
+ .hasSize(2); // locator + server2
+ });
+
+ server2.invoke(() -> {
+
assertThat(ClusterStartupRule.getCache().getDistributedSystem().getAllOtherMembers())
+ .hasSize(2); // locator + server1
+ });
+ }
+}
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java
new file mode 100644
index 0000000000..04305b0a0f
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthDUnitTest.java
@@ -0,0 +1,513 @@
+/*
+ * 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.geode.cache.ssl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.cache.client.ClientCache;
+import org.apache.geode.cache.client.ClientCacheFactory;
+import org.apache.geode.cache.client.ClientRegionShortcut;
+import org.apache.geode.examples.SimpleSecurityManager;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.ClientVM;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.SecurityTest;
+import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture;
+
+/**
+ * Distributed tests for Server-only TLS with Alternative Client
Authentication.
+ *
+ * <p>
+ * These tests verify that:
+ * <ul>
+ * <li>Servers present certificates and clients verify them</li>
+ * <li>Clients do NOT present certificates
(ssl-require-authentication=false)</li>
+ * <li>All transport is TLS-encrypted in both directions</li>
+ * <li>Clients authenticate using application-layer credentials
(username/password or tokens)</li>
+ * <li>Authorization is enforced through SecurityManager</li>
+ * </ul>
+ */
+@Category({SecurityTest.class})
+public class ServerOnlyTLSWithAuthDUnitTest {
+
+ private static final String REGION_NAME = "testRegion";
+
+ @Rule
+ public ClusterStartupRule cluster = new ClusterStartupRule();
+
+ private ServerOnlyTLSTestFixture fixture;
+
+ @Before
+ public void setUp() throws Exception {
+ fixture = new ServerOnlyTLSTestFixture();
+
+ // Add ignored exceptions for SSL-related cleanup warnings
+ IgnoredException.addIgnoredException("javax.net.ssl.SSLException");
+ IgnoredException.addIgnoredException("java.io.IOException");
+ }
+
+ /**
+ * Test basic client connection with TLS transport encryption and
username/password
+ * authentication.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Server presents certificate, client verifies it</li>
+ * <li>Client does NOT present certificate</li>
+ * <li>Client authenticates with valid username/password</li>
+ * <li>Transport is encrypted</li>
+ * </ul>
+ */
+ @Test
+ public void testBasicConnectionWithUsernamePassword() throws Exception {
+ // Create certificates and stores using fixture
+ // Note: Use createClusterStores() for both locator and server peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Configure locator with server-only TLS (require-authentication=false)
and security manager
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+
+ // Start locator
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Configure server with server-only TLS and security manager
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+
+ // Start server
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region on server
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Configure client with server-only TLS (truststore only, no keystore)
+ Properties clientSSLProps = clientStores.propertiesWith("all", false,
false);
+
+ // Add authentication properties (username/password)
+ // SimpleSecurityManager accepts username when username == password
+ Properties clientAuthProps = fixture.createClientAuthProperties("data",
"data");
+ clientSSLProps.putAll(clientAuthProps);
+
+ // Connect client
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientSSLProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify client can perform operations
+ client.invoke(() -> {
+ ClientCache clientCache = ClusterStartupRule.getClientCache();
+ assertThat(clientCache).isNotNull();
+
+ Region<Object, Object> region = clientCache
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // Perform basic operations
+ region.put("key1", "value1");
+ Object value = region.get("key1");
+ assertThat(value).isEqualTo("value1");
+ });
+ }
+
+ /**
+ * Test multiple clients connecting with different credentials.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Multiple clients can connect simultaneously</li>
+ * <li>Each client has its own identity (username)</li>
+ * <li>All clients use TLS transport encryption</li>
+ * <li>None of the clients present certificates</li>
+ * </ul>
+ */
+ @Test
+ public void testMultipleClientsWithDifferentCredentials() throws Exception {
+ // Create certificates and stores
+ CertStores locatorStores = fixture.createLocatorStores();
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores client1Stores = fixture.createClientStores();
+ CertStores client2Stores = fixture.createClientStores();
+ CertStores client3Stores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = locatorStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Connect client 1 with user "dataRead"
+ Properties client1Props = client1Stores.propertiesWith("all", false,
false);
+ client1Props.putAll(fixture.createClientAuthProperties("data", "data"));
+ ClientVM client1 = cluster.startClientVM(2, c -> c
+ .withProperties(client1Props)
+ .withLocatorConnection(locatorPort));
+
+ // Connect client 2 with user "dataWrite"
+ Properties client2Props = client2Stores.propertiesWith("all", false,
false);
+ client2Props.putAll(fixture.createClientAuthProperties("dataWrite",
"dataWrite"));
+ ClientVM client2 = cluster.startClientVM(3, c -> c
+ .withProperties(client2Props)
+ .withLocatorConnection(locatorPort));
+
+ // Connect client 3 with user "dataManage"
+ Properties client3Props = client3Stores.propertiesWith("all", false,
false);
+ client3Props.putAll(fixture.createClientAuthProperties("dataManage",
"dataManage"));
+ ClientVM client3 = cluster.startClientVM(4, c -> c
+ .withProperties(client3Props)
+ .withLocatorConnection(locatorPort));
+
+ // Verify all clients can access region
+ for (ClientVM client : new ClientVM[] {client1, client2, client3}) {
+ client.invoke(() -> {
+ Region<Object, Object> region = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+ assertThat(region).isNotNull();
+ });
+ }
+ }
+
+ /**
+ * Test token-based authentication instead of username/password.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Clients can authenticate using bearer tokens</li>
+ * <li>Token authentication works with TLS transport</li>
+ * <li>No client certificates are required</li>
+ * </ul>
+ */
+ @Test
+ public void testTokenBasedAuthentication() throws Exception {
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Configure client with token authentication
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ Properties tokenAuthProps = fixture.createClientTokenAuthProperties(
+ SimpleSecurityManager.VALID_TOKEN);
+ clientProps.putAll(tokenAuthProps);
+
+ // Connect client
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify client can perform operations
+ client.invoke(() -> {
+ Region<Object, Object> region = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ region.put("tokenKey", "tokenValue");
+ assertThat(region.get("tokenKey")).isEqualTo("tokenValue");
+ });
+ }
+
+ /**
+ * Test server restart with client reconnection.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Client can reconnect after server restart</li>
+ * <li>TLS session is re-established</li>
+ * <li>Client re-authenticates successfully</li>
+ * </ul>
+ */
+ @Test
+ public void testServerRestartWithClientReconnection() throws Exception {
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Connect client
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("data", "data"));
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify initial connection
+ client.invoke(() -> {
+ Region<Object, Object> region = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+ region.put("beforeRestart", "value1");
+ });
+
+ // Stop and restart server
+ cluster.stop(1);
+ server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Recreate region on restarted server
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Verify client can reconnect and operate
+ client.invoke(() -> {
+ Region<Object, Object> region =
ClusterStartupRule.getClientCache().getRegion(REGION_NAME);
+ region.put("afterRestart", "value2");
+ assertThat(region.get("afterRestart")).isEqualTo("value2");
+ });
+ }
+
+ /**
+ * Test concurrent client operations over TLS.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Multiple threads can perform concurrent operations over TLS</li>
+ * <li>TLS connection handles concurrent load</li>
+ * <li>Authentication works with concurrent operations</li>
+ * </ul>
+ */
+ @Test
+ public void testConcurrentClientConnections() throws Exception {
+ // Create certificates and stores
+ // Note: Use createClusterStores() for both locator and server peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Create single client cache with TLS
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("data", "data"));
+
+ ClientCacheFactory factory = new ClientCacheFactory(clientProps)
+ .addPoolLocator("localhost", locatorPort)
+ .setPoolSubscriptionEnabled(true);
+
+ try (ClientCache clientCache = factory.create()) {
+ Region<Object, Object> region = clientCache
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // Perform concurrent operations from multiple threads
+ int numThreads = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(numThreads);
+ CountDownLatch startLatch = new CountDownLatch(1);
+ CountDownLatch completionLatch = new CountDownLatch(numThreads);
+ List<Future<?>> futures = new ArrayList<>();
+
+ for (int i = 0; i < numThreads; i++) {
+ final int operationId = i;
+ Future<?> future = executor.submit(() -> {
+ try {
+ // Wait for all threads to be ready
+ startLatch.await();
+
+ // Perform operation
+ region.put("key" + operationId, "value" + operationId);
+ assertThat(region.get("key" + operationId)).isEqualTo("value" +
operationId);
+
+ completionLatch.countDown();
+ } catch (Exception e) {
+ throw new RuntimeException("Operation " + operationId + " failed",
e);
+ }
+ });
+ futures.add(future);
+ }
+
+ // Start all operations simultaneously
+ startLatch.countDown();
+
+ // Wait for all operations to complete
+ boolean completed = completionLatch.await(2, TimeUnit.MINUTES);
+
+ // Check for exceptions first to see what actually failed
+ for (Future<?> future : futures) {
+ future.get();
+ }
+
+ assertThat(completed).isTrue();
+
+ executor.shutdown();
+ }
+ }
+
+ /**
+ * Test region operations with authorization checks.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>SecurityManager enforces authorization based on principal</li>
+ * <li>Different users have different permissions</li>
+ * <li>Authorization works with TLS transport encryption</li>
+ * </ul>
+ */
+ @Test
+ public void testRegionOperationsWithAuthorization() throws Exception {
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Connect client with "data" credentials
+ // SimpleSecurityManager authorizes based on username prefix matching
permission
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("data", "data"));
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify authorized operations succeed
+ client.invoke(() -> {
+ Region<Object, Object> region = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // User "data" should be authorized for DATA:READ and DATA:WRITE
+ region.put("authKey", "authValue");
+ assertThat(region.get("authKey")).isEqualTo("authValue");
+ });
+ }
+}
diff --git
a/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java
new file mode 100644
index 0000000000..6c762d214d
--- /dev/null
+++
b/geode-core/src/distributedTest/java/org/apache/geode/cache/ssl/ServerOnlyTLSWithAuthNegativeTest.java
@@ -0,0 +1,481 @@
+/*
+ * 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.geode.cache.ssl;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.Properties;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+
+import org.apache.geode.cache.Region;
+import org.apache.geode.cache.RegionShortcut;
+import org.apache.geode.cache.client.ClientCache;
+import org.apache.geode.cache.client.ClientRegionShortcut;
+import org.apache.geode.cache.client.ServerOperationException;
+import org.apache.geode.security.AuthenticationFailedException;
+import org.apache.geode.security.AuthenticationRequiredException;
+import org.apache.geode.security.NotAuthorizedException;
+import org.apache.geode.test.dunit.IgnoredException;
+import org.apache.geode.test.dunit.rules.ClientVM;
+import org.apache.geode.test.dunit.rules.ClusterStartupRule;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.junit.categories.SecurityTest;
+import org.apache.geode.test.junit.rules.ServerOnlyTLSTestFixture;
+
+/**
+ * Negative tests for Server-only TLS with Alternative Client Authentication.
+ *
+ * <p>
+ * These tests verify that security violations are properly detected and
rejected:
+ * <ul>
+ * <li>Invalid credentials are rejected</li>
+ * <li>Missing credentials are rejected</li>
+ * <li>Invalid tokens are rejected</li>
+ * <li>Unauthorized operations are blocked</li>
+ * <li>Missing or invalid server certificates are detected</li>
+ * </ul>
+ */
+@Category({SecurityTest.class})
+public class ServerOnlyTLSWithAuthNegativeTest {
+
+ private static final String REGION_NAME = "testRegion";
+
+ @Rule
+ public ClusterStartupRule cluster = new ClusterStartupRule();
+
+ private ServerOnlyTLSTestFixture fixture;
+
+ @Before
+ public void setUp() throws Exception {
+ fixture = new ServerOnlyTLSTestFixture();
+ }
+
+ @org.junit.After
+ public void tearDown() throws Exception {
+ // Remove ignored exceptions
+ IgnoredException.removeAllExpectedExceptions();
+ // Give VMs time to fully shut down
+ Thread.sleep(500);
+ }
+
+ /**
+ * Test that clients with invalid credentials are rejected.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Wrong password causes authentication failure</li>
+ * <li>TLS connection is established but authentication fails</li>
+ * </ul>
+ */
+ @Test
+ public void testClientWithInvalidCredentialsRejected() throws Exception {
+
IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName());
+ IgnoredException.addIgnoredException("Authentication FAILED");
+ IgnoredException.addIgnoredException("ServerOperationException");
+
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Configure client with WRONG password
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("testUser",
"wrongPassword"));
+
+ // Client connection succeeds (TLS is established), but authentication
fails on first operation
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify authentication fails when attempting an operation
+ client.invoke(() -> {
+ ClientCache clientCache = ClusterStartupRule.getClientCache();
+ Region<Object, Object> region = clientCache
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // Authentication should fail on first operation
+ assertThatThrownBy(() -> region.put("key", "value"))
+ .isInstanceOf(ServerOperationException.class)
+ .hasCauseInstanceOf(AuthenticationFailedException.class);
+ });
+ }
+
+ /**
+ * Test that clients without credentials are rejected.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Missing authentication credentials cause connection failure</li>
+ * <li>TLS connection might establish but authentication is required</li>
+ * </ul>
+ */
+ @Test
+ public void testClientWithMissingCredentialsRejected() throws Exception {
+
IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName());
+ IgnoredException.addIgnoredException("Authentication FAILED");
+ IgnoredException.addIgnoredException("AuthenticationRequiredException");
+ IgnoredException.addIgnoredException("No security credentials are
provided");
+ IgnoredException.addIgnoredException("ServerOperationException");
+
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Configure client with TLS but NO authentication credentials
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ // Do NOT add authentication properties
+
+ // Client connection succeeds (TLS is established), but authentication
fails on first operation
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify authentication fails when attempting an operation
+ client.invoke(() -> {
+ ClientCache clientCache = ClusterStartupRule.getClientCache();
+ Region<Object, Object> region = clientCache
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // Authentication should fail on first operation due to missing
credentials
+ assertThatThrownBy(() -> region.put("key", "value"))
+ .isInstanceOf(ServerOperationException.class)
+ .hasCauseInstanceOf(AuthenticationRequiredException.class);
+ });
+ }
+
+ /**
+ * Test that clients with invalid tokens are rejected.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Invalid bearer tokens cause authentication failure</li>
+ * <li>Token validation is enforced</li>
+ * </ul>
+ */
+ @Test
+ public void testClientWithInvalidTokenRejected() throws Exception {
+
IgnoredException.addIgnoredException(AuthenticationFailedException.class.getName());
+ IgnoredException.addIgnoredException("Authentication FAILED");
+ IgnoredException.addIgnoredException("Token authentication FAILED");
+ IgnoredException.addIgnoredException("ServerOperationException");
+
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Configure client with INVALID token
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+
clientProps.putAll(fixture.createClientTokenAuthProperties("INVALID_TOKEN"));
+
+ // Client connection succeeds (TLS is established), but authentication
fails on first operation
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify authentication fails when attempting an operation
+ client.invoke(() -> {
+ ClientCache clientCache = ClusterStartupRule.getClientCache();
+ Region<Object, Object> region = clientCache
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // Authentication should fail on first operation due to invalid token
+ assertThatThrownBy(() -> region.put("key", "value"))
+ .isInstanceOf(ServerOperationException.class)
+ .hasCauseInstanceOf(AuthenticationFailedException.class);
+ });
+ }
+
+ /**
+ * Test that authenticated clients without authorization are blocked from
operations.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Authentication succeeds</li>
+ * <li>Unauthorized operations are blocked by SecurityManager</li>
+ * </ul>
+ */
+ @Test
+ public void testClientUnauthorizedForOperation() throws Exception {
+
IgnoredException.addIgnoredException(NotAuthorizedException.class.getName());
+
+ // Create certificates and stores
+ // Note: Locator and server must use same CertStores for peer SSL
communication
+ CertStores clusterStores = fixture.createClusterStores();
+ CertStores clientStores = fixture.createClientStores();
+
+ // Start locator
+ Properties locatorProps = clusterStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server
+ Properties serverProps = clusterStores.propertiesWith("all", false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ // Create region
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Connect client with credentials that don't match required permissions
+ // SimpleSecurityManager authorizes based on principal matching permission
string prefix
+ // User "readonly" will NOT be authorized for "DATA:WRITE" operations
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("readonly",
"readonly"));
+
+ ClientVM client = cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+
+ // Verify client can connect but unauthorized operations fail
+ client.invoke(() -> {
+ Region<Object, Object> region = ClusterStartupRule.getClientCache()
+ .createClientRegionFactory(ClientRegionShortcut.PROXY)
+ .create(REGION_NAME);
+
+ // This operation should fail due to lack of authorization
+ assertThatThrownBy(() -> region.put("unauthorizedKey", "value"))
+ .hasCauseInstanceOf(NotAuthorizedException.class);
+ });
+ }
+
+ /**
+ * Test that clients reject connections to servers without certificates.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>TLS handshake fails when server doesn't present certificate</li>
+ * <li>Client-side verification is enforced</li>
+ * </ul>
+ */
+ @Test
+ public void testClientCannotConnectWithoutServerCert() throws Exception {
+ IgnoredException.addIgnoredException("SSLHandshakeException");
+ IgnoredException.addIgnoredException("Server expecting SSL handshake");
+
+ // This test would require starting a server WITHOUT SSL configuration
+ // which is complex in a proper test environment. The test verifies the
concept
+ // that mixing SSL and non-SSL components fails.
+
+ // Create locator WITH SSL
+ CertStores locatorStores = fixture.createLocatorStores();
+
+ Properties locatorProps = locatorStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(2, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Start server WITHOUT SSL (simulating misconfiguration)
+ Properties serverProps = new Properties();
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ // No SSL properties
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+
+ // Server should fail to join the cluster due to SSL mismatch
+ assertThatThrownBy(() -> {
+ cluster.startServerVM(3, serverProps, locatorPort);
+ }).getCause().getCause().getCause()
+ .isInstanceOf(javax.net.ssl.SSLHandshakeException.class);
+ }
+
+ /**
+ * Test that clients reject servers with invalid or untrusted certificates.
+ *
+ * <p>
+ * Verifies:
+ * <ul>
+ * <li>Client verifies server certificate against truststore</li>
+ * <li>Invalid server certificates are rejected</li>
+ * <li>PKIX path validation is enforced</li>
+ * </ul>
+ */
+ @Test
+ public void testClientRejectsServerWithInvalidCert() throws Exception {
+ IgnoredException.addIgnoredException("SSLHandshakeException");
+ IgnoredException.addIgnoredException("path");
+ IgnoredException.addIgnoredException("certificate");
+
+ // Create two separate CAs
+ CertificateMaterial validCA = fixture.getCA();
+
+ // Create a separate untrusted CA
+ CertificateMaterial untrustedCA = new CertificateBuilder()
+ .commonName("Untrusted-CA")
+ .isCA()
+ .generate();
+
+ // Create locator with valid CA
+ CertStores locatorStores = fixture.createLocatorStores();
+
+ Properties locatorProps = locatorStores.propertiesWith("all", false,
false);
+ fixture.addSecurityManagerConfig(locatorProps);
+ fixture.addPeerAuthProperties(locatorProps, "cluster", "cluster");
+ MemberVM locator = cluster.startLocatorVM(0, locatorProps);
+ int locatorPort = locator.getPort();
+
+ // Create server with certificate from UNTRUSTED CA
+ CertificateMaterial untrustedServerCert = new CertificateBuilder()
+ .commonName("untrusted-server")
+ .issuedBy(untrustedCA)
+ .sanDnsName("localhost")
+ .sanIpAddress("127.0.0.1")
+ .generate();
+
+ CertStores untrustedServerStores = CertStores.serverStore();
+ untrustedServerStores.withCertificate("server", untrustedServerCert);
+ untrustedServerStores.trust("untrustedCA", untrustedCA); // Server ONLY
trusts untrusted CA, not
+ // valid CA
+
+ Properties serverProps = untrustedServerStores.propertiesWith("all",
false, false);
+ fixture.addSecurityManagerConfig(serverProps);
+ fixture.addPeerAuthProperties(serverProps, "cluster", "cluster");
+ serverProps.setProperty("locators", "localhost[" + locatorPort + "]");
+ serverProps.setProperty("member-timeout", "5000"); // Fail fast if can't
join cluster
+
+ // Server might start but cluster communication could fail due to cert
mismatch
+ // OR server might fail to join cluster
+ // Either way, this demonstrates certificate validation
+
+ // Create client truststore with valid CA only (doesn't trust untrusted CA)
+ CertStores clientStores = CertStores.clientStore();
+ clientStores.trust("ca", validCA); // Client ONLY trusts valid CA
+
+ Properties clientProps = clientStores.propertiesWith("all", false, false);
+ clientProps.putAll(fixture.createClientAuthProperties("testUser",
"testUser"));
+
+ // If server manages to start, client connection should fail due to
untrusted certificate
+ try {
+ MemberVM server = cluster.startServerVM(1, serverProps, locatorPort);
+
+ server.invoke(() -> {
+ ClusterStartupRule.getCache()
+ .createRegionFactory(RegionShortcut.REPLICATE)
+ .create(REGION_NAME);
+ });
+
+ // Client should reject server's untrusted certificate
+ assertThatThrownBy(() -> {
+ cluster.startClientVM(2, c -> c
+ .withProperties(clientProps)
+ .withLocatorConnection(locatorPort));
+ }).satisfiesAnyOf(
+ e -> assertThat(e).hasMessageContaining("PKIX"),
+ e -> assertThat(e).hasMessageContaining("certificate"),
+ e -> assertThat(e).hasMessageContaining("trust"));
+ } catch (Exception e) {
+ // Server startup failure due to certificate mismatch is expected
+ // The cause chain should contain SSL/certificate/handshake errors
+ Throwable cause = e;
+ boolean foundSSLError = false;
+ while (cause != null && !foundSSLError) {
+ String message = cause.getClass().getName() + ": " +
cause.getMessage();
+ if (message.contains("SSL") || message.contains("certificate")
+ || message.contains("handshake")) {
+ foundSSLError = true;
+ }
+ cause = cause.getCause();
+ }
+ assertThat(foundSSLError).as("Should find SSL/certificate/handshake
error in cause chain")
+ .isTrue();
+ }
+ }
+}
diff --git
a/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java
b/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java
new file mode 100644
index 0000000000..a7aa7d3255
--- /dev/null
+++
b/geode-junit/src/main/java/org/apache/geode/security/templates/TokenAuthInit.java
@@ -0,0 +1,68 @@
+/*
+ * 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.geode.security.templates;
+
+import java.util.Properties;
+
+import org.apache.geode.LogWriter;
+import org.apache.geode.distributed.DistributedMember;
+import org.apache.geode.security.AuthInitialize;
+import org.apache.geode.security.AuthenticationFailedException;
+import org.apache.geode.security.SecurityManager;
+
+/**
+ * An {@link AuthInitialize} implementation that obtains a bearer token as
credentials from the
+ * given set of properties.
+ *
+ * To use this class the {@code security-client-auth-init} property should be
set to the fully
+ * qualified name of the static {@code create} method viz.
+ * {@code org.apache.geode.security.templates.TokenAuthInit.create}
+ */
+public class TokenAuthInit implements AuthInitialize {
+
+ public static final String BEARER_TOKEN = "security-bearer-token";
+
+ protected LogWriter systemLogWriter;
+ protected LogWriter securityLogWriter;
+
+ public static AuthInitialize create() {
+ return new TokenAuthInit();
+ }
+
+ @Override
+ public void init(final LogWriter systemLogWriter, final LogWriter
securityLogWriter)
+ throws AuthenticationFailedException {
+ this.systemLogWriter = systemLogWriter;
+ this.securityLogWriter = securityLogWriter;
+ }
+
+ @Override
+ public Properties getCredentials(final Properties securityProperties,
+ final DistributedMember server, final boolean isPeer) throws
AuthenticationFailedException {
+ String token = securityProperties.getProperty(BEARER_TOKEN);
+ if (token == null) {
+ throw new AuthenticationFailedException(
+ "TokenAuthInit: bearer token property [" + BEARER_TOKEN + "] not
set.");
+ }
+
+ Properties securityPropertiesCopy = new Properties();
+ // SecurityManager expects TOKEN property
+ securityPropertiesCopy.setProperty(SecurityManager.TOKEN, token);
+ return securityPropertiesCopy;
+ }
+
+ @Override
+ public void close() {}
+}
diff --git
a/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java
b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java
new file mode 100644
index 0000000000..20ffc0924d
--- /dev/null
+++
b/geode-junit/src/main/java/org/apache/geode/test/junit/rules/ServerOnlyTLSTestFixture.java
@@ -0,0 +1,253 @@
+/*
+ * 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.geode.test.junit.rules;
+
+import java.util.Properties;
+
+import org.apache.geode.cache.ssl.CertStores;
+import org.apache.geode.cache.ssl.CertificateBuilder;
+import org.apache.geode.cache.ssl.CertificateMaterial;
+import org.apache.geode.examples.SimpleSecurityManager;
+import org.apache.geode.security.templates.UserPasswordAuthInit;
+
+/**
+ * Test fixture for Server-only TLS with Alternative Client Authentication
scenarios.
+ *
+ * <p>
+ * This fixture creates:
+ * <ul>
+ * <li>A Certificate Authority (CA)</li>
+ * <li>Server certificates signed by the CA (for locators and servers)</li>
+ * <li>NO client certificates - clients authenticate via username/password or
tokens</li>
+ * <li>Server/locator keystores with server certificates</li>
+ * <li>Client truststores with CA certificate (to verify servers)</li>
+ * <li>Security configuration using SimpleSecurityManager</li>
+ * </ul>
+ *
+ * <p>
+ * Key characteristics:
+ * <ul>
+ * <li>ssl-require-authentication=false - servers don't require client
certificates</li>
+ * <li>All transport is TLS-encrypted in both directions</li>
+ * <li>Clients authenticate using application-layer credentials
(username/password or tokens)</li>
+ * </ul>
+ */
+public class ServerOnlyTLSTestFixture {
+
+ private static final String CA_CN = "Server-Only-TLS-CA";
+ private static final String SERVER_CN = "geode-server";
+ private static final String LOCATOR_CN = "geode-locator";
+
+ private final CertificateMaterial ca;
+ private final CertificateMaterial serverCertificate;
+ private final CertificateMaterial locatorCertificate;
+
+ public ServerOnlyTLSTestFixture() throws Exception {
+ // Create CA
+ ca = createCA();
+
+ // Create server and locator certificates (no client certificates needed)
+ serverCertificate = createServerCertificate(ca);
+ locatorCertificate = createLocatorCertificate(ca);
+ }
+
+ /**
+ * Creates a Certificate Authority for signing server certificates.
+ */
+ private CertificateMaterial createCA() throws Exception {
+ CertificateBuilder caBuilder = new CertificateBuilder()
+ .commonName(CA_CN)
+ .isCA();
+
+ return caBuilder.generate();
+ }
+
+ /**
+ * Creates a server certificate signed by the CA.
+ * Includes comprehensive Subject Alternative Names for server verification.
+ */
+ private CertificateMaterial createServerCertificate(CertificateMaterial ca)
throws Exception {
+ CertificateBuilder serverBuilder = new CertificateBuilder()
+ .commonName(SERVER_CN)
+ .issuedBy(ca)
+ .sanDnsName("localhost")
+ .sanDnsName("server.localdomain")
+ .sanIpAddress("127.0.0.1")
+ .sanIpAddress("0.0.0.0");
+
+ return serverBuilder.generate();
+ }
+
+ /**
+ * Creates a locator certificate signed by the CA.
+ * Includes comprehensive Subject Alternative Names for locator verification.
+ */
+ private CertificateMaterial createLocatorCertificate(CertificateMaterial ca)
throws Exception {
+ CertificateBuilder locatorBuilder = new CertificateBuilder()
+ .commonName(LOCATOR_CN)
+ .issuedBy(ca)
+ .sanDnsName("localhost")
+ .sanDnsName("locator.localdomain")
+ .sanIpAddress("127.0.0.1")
+ .sanIpAddress("0.0.0.0");
+
+ return locatorBuilder.generate();
+ }
+
+ /**
+ * Creates and returns server CertStores for server-only TLS.
+ *
+ * <p>
+ * Server truststore contains the CA certificate to verify peer connections.
+ * <p>
+ * Server keystore contains the server's certificate for presentation to
clients.
+ *
+ * @return CertStores configured for server with server certificate
+ */
+ public CertStores createServerStores() {
+ CertStores certStores = CertStores.serverStore();
+ certStores.withCertificate("server", serverCertificate);
+ certStores.trust("ca", ca);
+ return certStores;
+ }
+
+ /**
+ * Creates and returns locator CertStores for server-only TLS.
+ *
+ * <p>
+ * Locator truststore contains the CA certificate to verify peer connections.
+ * <p>
+ * Locator keystore contains the locator's certificate for presentation to
clients and peers.
+ *
+ * @return CertStores configured for locator with locator certificate
+ */
+ public CertStores createLocatorStores() {
+ CertStores certStores = CertStores.locatorStore();
+ certStores.withCertificate("locator", locatorCertificate);
+ certStores.trust("ca", ca);
+ return certStores;
+ }
+
+ /**
+ * Creates and returns cluster CertStores for both locator and server.
+ *
+ * <p>
+ * For peer SSL communication, both locator and server need compatible
certificates.
+ * This method creates CertStores with the server certificate that works for
both.
+ * The server certificate includes SANs for localhost/127.0.0.1 which work
for both roles.
+ *
+ * @return CertStores configured with server certificate and CA trust
+ */
+ public CertStores createClusterStores() {
+ CertStores certStores = CertStores.serverStore();
+ // Use server certificate for both locator and server (SANs cover both
roles)
+ certStores.withCertificate("server", serverCertificate);
+ certStores.trust("ca", ca);
+ return certStores;
+ }
+
+ /**
+ * Creates and returns client CertStores for server-only TLS.
+ *
+ * <p>
+ * Client truststore contains ONLY the CA certificate to verify server
certificates.
+ * <p>
+ * NO keystore is created - clients do not present certificates.
+ *
+ * @return CertStores configured for client with only truststore (no client
certificate)
+ */
+ public CertStores createClientStores() {
+ CertStores certStores = CertStores.clientStore();
+ // Client only needs truststore with CA to verify servers
+ certStores.trust("ca", ca);
+ // NO client keystore - clients don't present certificates
+ return certStores;
+ }
+
+ /**
+ * Adds security manager configuration to properties.
+ * Uses SimpleSecurityManager for authentication and authorization.
+ */
+ public Properties addSecurityManagerConfig(Properties props) {
+ props.setProperty("security-manager",
SimpleSecurityManager.class.getName());
+ return props;
+ }
+
+ /**
+ * Adds peer authentication credentials for server/locator-to-locator
connections.
+ * Required when security-manager is enabled.
+ *
+ * @param props the properties to add authentication to
+ * @param username the username for peer authentication
+ * @param password the password for peer authentication
+ * @return the properties with peer authentication configured
+ */
+ public Properties addPeerAuthProperties(Properties props, String username,
String password) {
+ props.setProperty("security-username", username);
+ props.setProperty("security-password", password);
+ return props;
+ }
+
+ /**
+ * Creates client authentication properties with username and password.
+ *
+ * @param username the username
+ * @param password the password
+ * @return Properties with authentication configuration
+ */
+ public Properties createClientAuthProperties(String username, String
password) {
+ Properties props = new Properties();
+ props.setProperty("security-client-auth-init",
+ UserPasswordAuthInit.class.getName() + ".create");
+ props.setProperty(UserPasswordAuthInit.USER_NAME, username);
+ props.setProperty(UserPasswordAuthInit.PASSWORD, password);
+ return props;
+ }
+
+ /**
+ * Creates client authentication properties with a bearer token.
+ *
+ * @param token the bearer token
+ * @return Properties with token authentication configuration
+ */
+ public Properties createClientTokenAuthProperties(String token) {
+ Properties props = new Properties();
+ props.setProperty("security-client-auth-init",
+ "org.apache.geode.security.templates.TokenAuthInit.create");
+ props.setProperty("security-bearer-token", token);
+ return props;
+ }
+
+ /**
+ * Gets the CA certificate material for testing purposes.
+ */
+ public CertificateMaterial getCA() {
+ return ca;
+ }
+
+ /**
+ * Gets the server certificate material for testing purposes.
+ */
+ public CertificateMaterial getServerCertificate() {
+ return serverCertificate;
+ }
+
+ /**
+ * Gets the locator certificate material for testing purposes.
+ */
+ public CertificateMaterial getLocatorCertificate() {
+ return locatorCertificate;
+ }
+}