This is an automated email from the ASF dual-hosted git repository.

pvillard31 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 26e9952b2a6 NIFI-16417 Normalized Proxy Entity Headers for Request 
Replication (#11765)
26e9952b2a6 is described below

commit 26e9952b2a6b0f5857c13037c84992f0ec5f2ace
Author: David Handermann <[email protected]>
AuthorDate: Tue Oct 6 16:06:20 2026 -0500

    NIFI-16417 Normalized Proxy Entity Headers for Request Replication (#11765)
    
    - Add validation for single instance of Proxy Entity and Cookie headers 
regardless of casing
---
 .../http/replication/ReplicationHeaderUtils.java   |  38 ++++----
 .../replication/ThreadPoolRequestReplicator.java   |  17 ++--
 .../client/StandardHttpReplicationClient.java      |   5 +
 .../replication/TestReplicationHeaderUtils.java    | 103 ++++++++++++++++++++-
 ...TestStandardUploadRequestReplicatorHeaders.java |  41 +++++++-
 .../client/TestStandardHttpReplicationClient.java  |  27 ++++++
 .../apache/nifi/web/api/ApplicationResource.java   |  10 +-
 .../nifi/web/api/TestApplicationResource.java      |  44 +++++++++
 .../security/x509/X509AuthenticationFilter.java    |  18 ++++
 .../x509/X509AuthenticationFilterTest.java         | 103 +++++++++++++++++++++
 .../authentication/x509/X509IdentityProvider.java  |  16 ++++
 .../x509/X509IdentityProviderTest.java             |  99 ++++++++++++++++++++
 12 files changed, 488 insertions(+), 33 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ReplicationHeaderUtils.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ReplicationHeaderUtils.java
index 60be20e5cb4..84f320efcd1 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ReplicationHeaderUtils.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ReplicationHeaderUtils.java
@@ -25,8 +25,8 @@ import org.apache.nifi.web.security.http.SecurityHeader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -66,9 +66,10 @@ public final class ReplicationHeaderUtils {
     /**
      * Prepares headers for a replicated request. When a non-null user is 
provided, the
      * {@code X-ProxiedEntitiesChain} and {@code X-ProxiedEntityGroups} 
headers are set so the
-     * receiving node knows the request is on behalf of that user. When user 
is {@code null},
-     * these headers are omitted, indicating the request is made directly by 
the cluster node
-     * itself (e.g. for background connector state polling).
+     * receiving node knows the request is on behalf of that user. Existing 
values for those
+     * header names are removed first, regardless of letter case, so the 
framework value is the
+     * only one present. When user is {@code null}, these headers are omitted, 
indicating the
+     * request is made directly by the cluster node itself (e.g. for 
background connector state polling).
      *
      * <p>In all cases the {@code Authorization} header, auth-related cookies, 
and the
      * {@code Host} header are removed because the replicated call uses mTLS 
for transport
@@ -78,6 +79,9 @@ public final class ReplicationHeaderUtils {
      * @param user    the user on whose behalf the request is being made, or 
{@code null} for direct node requests
      */
     public static void applyUserProxyAndStripCredentials(final Map<String, 
String> headers, final NiFiUser user) {
+        removeHeader(headers, ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        removeHeader(headers, ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS);
+
         if (user == null) {
             logger.debug("No user provided: omitting proxied entities header 
from request");
         } else {
@@ -133,14 +137,19 @@ public final class ReplicationHeaderUtils {
     }
 
     static void removeHeader(final Map<String, String> headers, final String 
headerNameSearch) {
-        findHeaderName(headers, headerNameSearch).ifPresent(headers::remove);
+        if (headerNameSearch == null || headerNameSearch.isBlank()) {
+            return;
+        }
+
+        headers.keySet().removeIf(headerNameSearch::equalsIgnoreCase);
     }
 
     static void removeCookie(final Map<String, String> headers, final String 
cookieName) {
-        final Optional<String> cookieHeaderNameFound = findHeaderName(headers, 
COOKIE_HEADER);
+        final List<String> cookieHeaderNames = headers.keySet().stream()
+                .filter(COOKIE_HEADER::equalsIgnoreCase)
+                .toList();
 
-        if (cookieHeaderNameFound.isPresent()) {
-            final String cookieHeaderName = cookieHeaderNameFound.get();
+        for (final String cookieHeaderName : cookieHeaderNames) {
             final String rawCookies = headers.get(cookieHeaderName);
             final String[] rawCookieParts = rawCookies.split(";");
             final Set<String> filteredCookieParts = Stream.of(rawCookieParts)
@@ -155,17 +164,4 @@ public final class ReplicationHeaderUtils {
             }
         }
     }
-
-    /**
-     * Find an HTTP header name in a map regardless of case, since HTTP/1.1 
capitalises headers
-     * but HTTP/2 returns lowercased headers.
-     */
-    static Optional<String> findHeaderName(final Map<String, String> headers, 
final String headerName) {
-        if (headerName == null || headerName.isBlank()) {
-            return Optional.empty();
-        }
-        return headers.keySet().stream()
-                .filter(headerName::equalsIgnoreCase)
-                .findFirst();
-    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
index 9fe079cc26f..c8788383e6e 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
@@ -535,15 +535,20 @@ public class ThreadPoolRequestReplicator implements 
RequestReplicator, Closeable
 
                         try {
                             final Map<String, String> cancelLockHeaders = new 
HashMap<>(headers);
-                            
cancelLockHeaders.put(RequestReplicationHeader.CANCEL_TRANSACTION.getHeader(), 
"true");
+                            
ReplicationHeaderUtils.removeHeader(cancelLockHeaders, 
RequestReplicationHeader.CANCEL_TRANSACTION.getHeader());
+                            
cancelLockHeaders.put(RequestReplicationHeader.CANCEL_TRANSACTION.getHeader(), 
Boolean.TRUE.toString());
                             final Thread cancelLockThread = new Thread(() -> {
-                                logger.debug("Found {} dissenting nodes for {} 
{}; canceling claim request", dissentingCount, method, uri.getPath());
+                                try {
+                                    logger.debug("Found {} dissenting nodes 
for {} {}; canceling claim request", dissentingCount, method, uri.getPath());
 
-                                final PreparedRequest request = 
httpClient.prepareRequest(method, cancelLockHeaders, entity);
-                                final Function<NodeIdentifier, 
NodeHttpRequest> requestFactory =
-                                    nodeId -> new NodeHttpRequest(request, 
nodeId, createURI(uri, nodeId), null, clusterResponse);
+                                    final PreparedRequest request = 
httpClient.prepareRequest(method, cancelLockHeaders, entity);
+                                    final Function<NodeIdentifier, 
NodeHttpRequest> requestFactory =
+                                        nodeId -> new NodeHttpRequest(request, 
nodeId, createURI(uri, nodeId), null, clusterResponse);
 
-                                submitAsyncRequest(nodeIds, requestFactory);
+                                    submitAsyncRequest(nodeIds, 
requestFactory);
+                                } catch (final Exception e) {
+                                    logger.error("Failed to cancel transaction 
for {} {}", method, uri.getPath(), e);
+                                }
                             });
                             cancelLockThread.setName("Cancel Flow Locks");
                             cancelLockThread.start();
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/client/StandardHttpReplicationClient.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/client/StandardHttpReplicationClient.java
index b010176fad6..6aa78bd9f88 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/client/StandardHttpReplicationClient.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/client/StandardHttpReplicationClient.java
@@ -165,6 +165,11 @@ public class StandardHttpReplicationClient implements 
HttpReplicationClient {
             }
 
             final String headerValue = header.getValue();
+            final String preparedHeaderValue = preparedHeaders.get(headerName);
+            if (preparedHeaders.containsKey(headerName) && 
!Objects.equals(preparedHeaderValue, headerValue)) {
+                throw new IllegalArgumentException("Conflicting values for 
HTTP header [%s]".formatted(headerName));
+            }
+
             preparedHeaders.put(headerName, headerValue);
         }
 
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestReplicationHeaderUtils.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestReplicationHeaderUtils.java
index 1f9727de74f..ff647820dde 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestReplicationHeaderUtils.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestReplicationHeaderUtils.java
@@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -36,12 +38,14 @@ class TestReplicationHeaderUtils {
     private static final String TEST_USER_IDENTITY = "alice";
 
     private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String AUTHORIZATION_HEADER_UPPER = "AUTHORIZATION";
     private static final String AUTHORIZATION_VALUE = "Bearer secret";
 
     private static final String CUSTOM_HEADER = "X-Custom-Token";
     private static final String CUSTOM_HEADER_VALUE = "custom-token-123";
 
     private static final String COOKIE_HEADER = "Cookie";
+    private static final String COOKIE_HEADER_LOWER = "cookie";
     private static final String HOST_HEADER = "Host";
     private static final String HOST_VALUE = "original-host:8080";
 
@@ -55,6 +59,17 @@ class TestReplicationHeaderUtils {
     private static final String SPOOFED_VALUE = "spoofed";
     private static final String SHOULD_SURVIVE_VALUE = "should-survive";
 
+    private static final String PROXIED_ENTITIES_CHAIN_UPPER = 
"X-PROXIEDENTITIESCHAIN";
+    private static final String PROXIED_ENTITIES_CHAIN_LOWER = 
"x-proxiedentitieschain";
+    private static final String PROXIED_ENTITY_GROUPS_UPPER = 
"X-PROXIEDENTITYGROUPS";
+    private static final String PROXIED_ENTITY_GROUPS_LOWER = 
"x-proxiedentitygroups";
+
+    private static final String OTHER_IDENTITY = "other-user";
+    private static final String ANOTHER_IDENTITY = "another-user";
+    private static final String IDENTITY_PROVIDER_GROUP = "analysts";
+    private static final String OTHER_GROUP = "other-group";
+    private static final String ANOTHER_GROUP = "another-group";
+
     @Test
     void testApplyUserProxyAndStripCredentialsSetsProxiedEntities() {
         final NiFiUser user = new 
StandardNiFiUser.Builder().identity(TEST_USER_IDENTITY).build();
@@ -83,6 +98,53 @@ class TestReplicationHeaderUtils {
         assertNull(headers.get(AUTHORIZATION_HEADER));
     }
 
+    @Test
+    void testApplyUserProxyReplacesCaseVariantProxiedEntityHeaders() {
+        final NiFiUser user = new StandardNiFiUser.Builder()
+                .identity(TEST_USER_IDENTITY)
+                .identityProviderGroups(Set.of(IDENTITY_PROVIDER_GROUP))
+                .build();
+        final Map<String, String> headers = new HashMap<>();
+        headers.put(PROXIED_ENTITIES_CHAIN_UPPER, 
formatEntities(OTHER_IDENTITY));
+        headers.put(PROXIED_ENTITIES_CHAIN_LOWER, 
formatEntities(ANOTHER_IDENTITY));
+        headers.put(PROXIED_ENTITY_GROUPS_UPPER, formatEntities(OTHER_GROUP));
+        headers.put(PROXIED_ENTITY_GROUPS_LOWER, 
formatEntities(ANOTHER_GROUP));
+
+        ReplicationHeaderUtils.applyUserProxyAndStripCredentials(headers, 
user);
+
+        assertEquals(Set.of(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), 
matchingHeaderNames(headers, ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN));
+        assertEquals(Set.of(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS), 
matchingHeaderNames(headers, ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS));
+        
assertTrue(headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(TEST_USER_IDENTITY));
+        
assertTrue(headers.get(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS).contains(IDENTITY_PROVIDER_GROUP));
+        
assertFalse(headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(OTHER_IDENTITY));
+        
assertFalse(headers.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(ANOTHER_IDENTITY));
+        
assertFalse(headers.get(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS).contains(OTHER_GROUP));
+        
assertFalse(headers.get(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS).contains(ANOTHER_GROUP));
+    }
+
+    @Test
+    void testApplyUserProxyWithNullUserRemovesExistingProxiedEntityHeaders() {
+        final Map<String, String> headers = new HashMap<>();
+        headers.put(PROXIED_ENTITIES_CHAIN_UPPER, 
formatEntities(OTHER_IDENTITY));
+        headers.put(PROXIED_ENTITY_GROUPS_LOWER, formatEntities(OTHER_GROUP));
+
+        ReplicationHeaderUtils.applyUserProxyAndStripCredentials(headers, 
null);
+
+        assertTrue(matchingHeaderNames(headers, 
ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).isEmpty());
+        assertTrue(matchingHeaderNames(headers, 
ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS).isEmpty());
+    }
+
+    @Test
+    void testApplyUserProxyRemovesAllAuthorizationCaseVariants() {
+        final Map<String, String> headers = new HashMap<>();
+        headers.put(AUTHORIZATION_HEADER, AUTHORIZATION_VALUE);
+        headers.put(AUTHORIZATION_HEADER_UPPER, AUTHORIZATION_VALUE);
+
+        ReplicationHeaderUtils.applyUserProxyAndStripCredentials(headers, 
null);
+
+        assertTrue(matchingHeaderNames(headers, 
AUTHORIZATION_HEADER).isEmpty());
+    }
+
     @Test
     void testStripRequestReplicationHeadersRemovesAllProtocolHeaders() {
         final Map<String, String> headers = new HashMap<>();
@@ -130,8 +192,8 @@ class TestReplicationHeaderUtils {
     @Test
     void testStripReplicationMarkerHeadersCaseInsensitive() {
         final Map<String, String> headers = new HashMap<>();
-        headers.put("Request-Replicated", "true");
-        headers.put("Request-Forwarded-To-Coordinator", "true");
+        headers.put("Request-Replicated", Boolean.TRUE.toString());
+        headers.put("Request-Forwarded-To-Coordinator", 
Boolean.TRUE.toString());
         headers.put("Replication-Target-Id", SHOULD_SURVIVE_VALUE);
 
         ReplicationHeaderUtils.stripReplicationMarkerHeaders(headers);
@@ -144,8 +206,8 @@ class TestReplicationHeaderUtils {
     @Test
     void testStripRequestReplicationHeadersCaseInsensitive() {
         final Map<String, String> headers = new HashMap<>();
-        headers.put("Request-Replicated", "true");
-        headers.put("EXECUTION-CONTINUE", "true");
+        headers.put("Request-Replicated", Boolean.TRUE.toString());
+        headers.put("EXECUTION-CONTINUE", Boolean.TRUE.toString());
 
         ReplicationHeaderUtils.stripRequestReplicationHeaders(headers);
 
@@ -153,6 +215,19 @@ class TestReplicationHeaderUtils {
         assertFalse(headers.containsKey("EXECUTION-CONTINUE"));
     }
 
+    @Test
+    void testStripRequestReplicationHeadersRemovesEveryCaseVariant() {
+        final Map<String, String> headers = new HashMap<>();
+        headers.put("Request-Replicated", Boolean.TRUE.toString());
+        headers.put("request-replicated", Boolean.FALSE.toString());
+        headers.put("EXECUTION-CONTINUE", Boolean.TRUE.toString());
+        headers.put("execution-continue", Boolean.FALSE.toString());
+
+        ReplicationHeaderUtils.stripRequestReplicationHeaders(headers);
+
+        assertTrue(headers.isEmpty());
+    }
+
     @Test
     void testStripHopByHopHeaders() {
         final Map<String, String> headers = new HashMap<>();
@@ -181,6 +256,7 @@ class TestReplicationHeaderUtils {
     void testStripAuthCookies() {
         final Map<String, String> headers = new HashMap<>();
         headers.put(COOKIE_HEADER, "__Secure-Authorization-Bearer=token123; 
__Secure-Request-Token=rt456; other=value");
+        headers.put(COOKIE_HEADER_LOWER, 
"__Secure-Authorization-Bearer=other-token; keep=yes");
 
         ReplicationHeaderUtils.applyUserProxyAndStripCredentials(headers, 
null);
 
@@ -189,6 +265,11 @@ class TestReplicationHeaderUtils {
         assertFalse(remaining.contains("__Secure-Authorization-Bearer"));
         assertFalse(remaining.contains("__Secure-Request-Token"));
         assertTrue(remaining.contains("other=value"));
+
+        final String lowerCaseCookies = headers.get(COOKIE_HEADER_LOWER);
+        assertNotNull(lowerCaseCookies);
+        
assertFalse(lowerCaseCookies.contains("__Secure-Authorization-Bearer"));
+        assertTrue(lowerCaseCookies.contains("keep=yes"));
     }
 
     @Test
@@ -202,4 +283,18 @@ class TestReplicationHeaderUtils {
         assertNull(headers.get(HOST_HEADER));
         assertEquals(SHOULD_SURVIVE_VALUE, headers.get(CUSTOM_HEADER));
     }
+
+    private static Set<String> matchingHeaderNames(final Map<String, String> 
headers, final String headerName) {
+        return headers.keySet().stream()
+                .filter(headerName::equalsIgnoreCase)
+                .collect(Collectors.toSet());
+    }
+
+    private static String formatEntities(final String... entities) {
+        final StringBuilder formattedEntities = new StringBuilder();
+        for (final String entity : entities) {
+            formattedEntities.append('<').append(entity).append('>');
+        }
+        return formattedEntities.toString();
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
index 871dd1015c5..4df41d18e30 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/TestStandardUploadRequestReplicatorHeaders.java
@@ -83,6 +83,12 @@ class TestStandardUploadRequestReplicatorHeaders {
     private static final String SPOOFED_VALUE = "spoofed";
     private static final String SPOOFED_TX_VALUE = "spoofed-tx";
 
+    private static final String PROXIED_ENTITIES_CHAIN_UPPER = 
"X-PROXIEDENTITIESCHAIN";
+    private static final String PROXIED_ENTITIES_CHAIN_LOWER = 
"x-proxiedentitieschain";
+    private static final String PROXIED_ENTITY_GROUPS_UPPER = 
"X-PROXIEDENTITYGROUPS";
+    private static final String PROXIED_ENTITY_GROUPS_LOWER = 
"x-proxiedentitygroups";
+    private static final String FORWARDED_GROUP = "admins";
+
     private StandardUploadRequestReplicator replicator;
 
     @BeforeEach
@@ -318,7 +324,7 @@ class TestStandardUploadRequestReplicatorHeaders {
                 .filename(TEST_FILENAME)
                 .identifier(TEST_IDENTIFIER)
                 .contents(new ByteArrayInputStream(new byte[0]))
-                .header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, "<" + 
EVIL_USER_IDENTITY + ">")
+                .header(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
formatEntities(EVIL_USER_IDENTITY))
                 .exampleRequestUri(TEST_REQUEST_URI)
                 .responseClass(String.class)
                 .successfulResponseStatus(SUCCESS_STATUS)
@@ -330,6 +336,31 @@ class TestStandardUploadRequestReplicatorHeaders {
         
assertTrue(result.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(REAL_USER_IDENTITY));
     }
 
+    @Test
+    void testForwardedProxiedEntityCaseVariantsReplacedWithAuthenticatedUser() 
{
+        final Map<String, String> forwarded = new HashMap<>();
+        forwarded.put(PROXIED_ENTITIES_CHAIN_UPPER, 
formatEntities(EVIL_USER_IDENTITY));
+        forwarded.put(PROXIED_ENTITIES_CHAIN_LOWER, 
formatEntities(EVIL_USER_IDENTITY));
+        forwarded.put(PROXIED_ENTITY_GROUPS_UPPER, 
formatEntities(FORWARDED_GROUP));
+        forwarded.put(PROXIED_ENTITY_GROUPS_LOWER, 
formatEntities(FORWARDED_GROUP));
+
+        final UploadRequest<String> request = buildUploadRequest(forwarded);
+        final Map<String, String> result = 
replicator.buildOutboundHeaders(request);
+
+        final List<String> chainKeys = result.keySet().stream()
+                
.filter(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), 
chainKeys);
+        
assertTrue(result.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(TEST_USER_IDENTITY));
+        
assertFalse(result.get(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN).contains(EVIL_USER_IDENTITY));
+
+        final List<String> groupKeys = result.keySet().stream()
+                
.filter(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS::equalsIgnoreCase)
+                .toList();
+        assertEquals(List.of(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS), 
groupKeys);
+        
assertFalse(result.get(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS).contains(FORWARDED_GROUP));
+    }
+
     @Test
     void testAuthCookiesStrippedFromForwardedHeaders() {
         final Map<String, String> forwarded = new HashMap<>();
@@ -392,4 +423,12 @@ class TestStandardUploadRequestReplicatorHeaders {
                 .successfulResponseStatus(SUCCESS_STATUS)
                 .build();
     }
+
+    private static String formatEntities(final String... entities) {
+        final StringBuilder formattedEntities = new StringBuilder();
+        for (final String entity : entities) {
+            formattedEntities.append('<').append(entity).append('>');
+        }
+        return formattedEntities.toString();
+    }
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/client/TestStandardHttpReplicationClient.java
 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/client/TestStandardHttpReplicationClient.java
index fbbd4473df3..5ae1b8fe363 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/client/TestStandardHttpReplicationClient.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/test/java/org/apache/nifi/cluster/coordination/http/replication/client/TestStandardHttpReplicationClient.java
@@ -38,6 +38,7 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -96,6 +97,11 @@ class TestStandardHttpReplicationClient {
 
     private static final String STATUS_PSEUDO_HEADER = ":status";
 
+    private static final String CUSTOM_HEADER = "X-Custom";
+    private static final String CUSTOM_HEADER_LOWER = "x-custom";
+    private static final String CUSTOM_HEADER_VALUE = "same";
+    private static final String CUSTOM_HEADER_OTHER_VALUE = "other";
+
     @Mock
     private WebClientService webClientService;
 
@@ -138,6 +144,27 @@ class TestStandardHttpReplicationClient {
         assertArrayEquals(EMPTY_MAP_SERIALIZED, 
standardPreparedRequest.requestBody());
     }
 
+    @Test
+    void testPrepareRequestRejectsConflictingHeaderValues() {
+        final Map<String, String> headers = new LinkedHashMap<>();
+        headers.put(CUSTOM_HEADER, CUSTOM_HEADER_VALUE);
+        headers.put(CUSTOM_HEADER_LOWER, CUSTOM_HEADER_OTHER_VALUE);
+
+        assertThrows(IllegalArgumentException.class, () -> 
client.prepareRequest(GET_METHOD, headers, Collections.emptyMap()));
+    }
+
+    @Test
+    void testPrepareRequestCollapsesIdenticalHeaderValues() {
+        final Map<String, String> headers = new LinkedHashMap<>();
+        headers.put(CUSTOM_HEADER, CUSTOM_HEADER_VALUE);
+        headers.put(CUSTOM_HEADER_LOWER, CUSTOM_HEADER_VALUE);
+
+        final PreparedRequest preparedRequest = 
client.prepareRequest(GET_METHOD, headers, Collections.emptyMap());
+
+        assertEquals(CUSTOM_HEADER_VALUE, 
preparedRequest.getHeaders().get(CUSTOM_HEADER_LOWER));
+        assertNull(preparedRequest.getHeaders().get(CUSTOM_HEADER));
+    }
+
     @Test
     void testReplicateIllegalArgumentException() {
         assertThrows(IllegalArgumentException.class, () -> 
client.replicate(null, REPLICATE_URI));
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index b43c9614ef6..f0fc58b052d 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -332,7 +332,11 @@ public abstract class ApplicationResource {
         }
 
         final URI requestUri = 
RequestUriBuilder.fromHttpServletRequest(httpServletRequest).build();
-        // Set Proxy Headers based on resolved URI from supported values
+        // Resolved proxy headers replace inbound values. The header map is 
case-sensitive, so a differently
+        // cased name would otherwise remain beside the canonical name.
+        removeHeaderIgnoreCase(result, ProxyHeader.PROXY_SCHEME.getHeader());
+        removeHeaderIgnoreCase(result, ProxyHeader.PROXY_HOST.getHeader());
+        removeHeaderIgnoreCase(result, ProxyHeader.PROXY_PORT.getHeader());
         result.put(ProxyHeader.PROXY_SCHEME.getHeader(), 
requestUri.getScheme());
         result.put(ProxyHeader.PROXY_HOST.getHeader(), requestUri.getHost());
         result.put(ProxyHeader.PROXY_PORT.getHeader(), 
Integer.toString(requestUri.getPort()));
@@ -340,6 +344,10 @@ public abstract class ApplicationResource {
         return result;
     }
 
+    private static void removeHeaderIgnoreCase(final Map<String, String> 
headers, final String headerName) {
+        headers.keySet().removeIf(headerName::equalsIgnoreCase);
+    }
+
     /**
      * Checks whether the request is part of a two-phase commit style request 
(either phase 1 or phase 2)
      *
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
index e1ad5fe3ff3..d0967fcaa98 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestApplicationResource.java
@@ -100,6 +100,50 @@ public class TestApplicationResource {
         SecurityContextHolder.clearContext();
     }
 
+    @Test
+    public void testGetHeadersReplacesCaseVariantProxyHeaders() {
+        final String proxyHostLower = "x-proxyhost";
+        final String proxySchemeLower = "x-proxyscheme";
+        final String proxyPortLower = "x-proxyport";
+        final String acceptHeader = "Accept";
+        final String acceptValue = "application/json";
+        final String inboundScheme = "http";
+        final String inboundPort = "443";
+        final String proxyHostWithPort = HOST + ":" + PORT;
+
+        
when(request.getHeaderNames()).thenReturn(Collections.enumeration(List.of(proxyHostLower,
 proxySchemeLower, proxyPortLower, acceptHeader)));
+        when(request.getHeader(anyString())).thenAnswer(invocation -> {
+            final String headerName = invocation.getArgument(0, String.class);
+            if 
(ProxyHeader.PROXY_HOST.getHeader().equalsIgnoreCase(headerName)) {
+                return proxyHostWithPort;
+            }
+
+            if 
(ProxyHeader.PROXY_SCHEME.getHeader().equalsIgnoreCase(headerName)) {
+                return inboundScheme;
+            }
+
+            if 
(ProxyHeader.PROXY_PORT.getHeader().equalsIgnoreCase(headerName)) {
+                return inboundPort;
+            }
+
+            if (acceptHeader.equalsIgnoreCase(headerName)) {
+                return acceptValue;
+            }
+
+            return null;
+        });
+
+        final Map<String, String> headers = resource.getHeaders();
+
+        assertEquals(HOST, headers.get(ProxyHeader.PROXY_HOST.getHeader()));
+        assertEquals(inboundScheme, 
headers.get(ProxyHeader.PROXY_SCHEME.getHeader()));
+        assertEquals(Integer.toString(PORT), 
headers.get(ProxyHeader.PROXY_PORT.getHeader()));
+        assertEquals(acceptValue, headers.get(acceptHeader));
+        assertEquals(1, 
headers.keySet().stream().filter(ProxyHeader.PROXY_HOST.getHeader()::equalsIgnoreCase).count());
+        assertEquals(1, 
headers.keySet().stream().filter(ProxyHeader.PROXY_SCHEME.getHeader()::equalsIgnoreCase).count());
+        assertEquals(1, 
headers.keySet().stream().filter(ProxyHeader.PROXY_PORT.getHeader()::equalsIgnoreCase).count());
+    }
+
     @Test
     public void 
testGenerateUriShouldBlockProxyContextPathHeaderIfNotInAllowList() {
         when(request.getHeader(anyString())).thenAnswer(new RequestAnswer());
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
index 3cd356afe9f..3388a8ee612 100644
--- 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/x509/X509AuthenticationFilter.java
@@ -17,6 +17,7 @@
 package org.apache.nifi.web.security.x509;
 
 import jakarta.servlet.http.HttpServletRequest;
+import org.apache.nifi.web.security.InvalidAuthenticationException;
 import org.apache.nifi.web.security.NiFiAuthenticationFilter;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.slf4j.Logger;
@@ -25,6 +26,7 @@ import org.springframework.security.core.Authentication;
 import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 
 import java.security.cert.X509Certificate;
+import java.util.Enumeration;
 
 /**
  * Custom X509 filter that will inspect the HTTP headers for a proxied user 
before extracting the user details from the client certificate.
@@ -49,6 +51,9 @@ public class X509AuthenticationFilter extends 
NiFiAuthenticationFilter {
             return null;
         }
 
+        rejectMultipleHeaderValues(request, 
ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        rejectMultipleHeaderValues(request, 
ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS);
+
         final String proxiedEntitiesChain = 
request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
         logger.debug("Raw {} - {}", ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
proxiedEntitiesChain);
 
@@ -73,4 +78,17 @@ public class X509AuthenticationFilter extends 
NiFiAuthenticationFilter {
         this.principalExtractor = principalExtractor;
     }
 
+    private static void rejectMultipleHeaderValues(final HttpServletRequest 
request, final String headerName) {
+        final Enumeration<String> headerValues = 
request.getHeaders(headerName);
+        int valueCount = 0;
+        while (headerValues.hasMoreElements()) {
+            headerValues.nextElement();
+            valueCount++;
+        }
+
+        if (valueCount > 1) {
+            throw new InvalidAuthenticationException("Request provided 
multiple values for header [%s]".formatted(headerName));
+        }
+    }
+
 }
diff --git 
a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterTest.java
 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterTest.java
new file mode 100644
index 00000000000..548f77c1cc5
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/x509/X509AuthenticationFilterTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.nifi.web.security.x509;
+
+import org.apache.nifi.web.security.InvalidAuthenticationException;
+import org.apache.nifi.web.security.NiFiWebAuthenticationDetails;
+import org.apache.nifi.web.security.ProxiedEntitiesUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.core.Authentication;
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import java.security.cert.X509Certificate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.mock;
+
+class X509AuthenticationFilterTest {
+
+    private static final String CLIENT_CERTIFICATE_ATTRIBUTE = 
"jakarta.servlet.request.X509Certificate";
+
+    private static final String ALICE = "alice";
+
+    private static final String BOB = "bob";
+
+    private static final String ANALYSTS = "analysts";
+
+    private static final String OPERATORS = "operators";
+
+    private X509AuthenticationFilter filter;
+
+    @BeforeEach
+    void setup() {
+        filter = new X509AuthenticationFilter();
+        filter.setCertificateExtractor(new X509CertificateExtractor());
+        filter.setPrincipalExtractor(mock(X509PrincipalExtractor.class));
+        
filter.setAuthenticationDetailsSource(NiFiWebAuthenticationDetails::new);
+    }
+
+    @Test
+    void testMultipleProxiedEntitiesChainValuesRejected() {
+        final MockHttpServletRequest request = secureRequestWithCertificate();
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
formatEntities(ALICE));
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
formatEntities(BOB));
+
+        assertThrows(InvalidAuthenticationException.class, () -> 
filter.attemptAuthentication(request));
+    }
+
+    @Test
+    void testMultipleProxiedEntityGroupsValuesRejected() {
+        final MockHttpServletRequest request = secureRequestWithCertificate();
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
formatEntities(ALICE));
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS, 
formatEntities(ANALYSTS));
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS, 
formatEntities(OPERATORS));
+
+        assertThrows(InvalidAuthenticationException.class, () -> 
filter.attemptAuthentication(request));
+    }
+
+    @Test
+    void testSingleProxiedEntityHeaderPassedThrough() {
+        final MockHttpServletRequest request = secureRequestWithCertificate();
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
formatEntities(ALICE));
+        request.addHeader(ProxiedEntitiesUtils.PROXY_ENTITY_GROUPS, 
formatEntities(ANALYSTS));
+
+        final Authentication authentication = 
filter.attemptAuthentication(request);
+
+        final X509AuthenticationRequestToken token = 
assertInstanceOf(X509AuthenticationRequestToken.class, authentication);
+        assertEquals(formatEntities(ALICE), token.getProxiedEntitiesChain());
+        assertEquals(formatEntities(ANALYSTS), token.getProxiedEntityGroups());
+    }
+
+    private static MockHttpServletRequest secureRequestWithCertificate() {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setSecure(true);
+        request.setAttribute(CLIENT_CERTIFICATE_ATTRIBUTE, new 
X509Certificate[] {mock(X509Certificate.class)});
+        return request;
+    }
+
+    private static String formatEntities(final String... entities) {
+        final StringBuilder formattedEntities = new StringBuilder();
+        for (final String entity : entities) {
+            formattedEntities.append('<').append(entity).append('>');
+        }
+        return formattedEntities.toString();
+    }
+}
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
index ba93cad37ad..a9648f38c8a 100644
--- 
a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
@@ -35,6 +35,7 @@ import org.springframework.stereotype.Component;
 import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.X509Certificate;
+import java.util.Enumeration;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -108,6 +109,8 @@ public class X509IdentityProvider implements 
IdentityProvider {
             return null;
         }
 
+        rejectMultipleHeaderValues(servletRequest, 
ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+
         // extract the principal
         final Object certificatePrincipal = 
principalExtractor.extractPrincipal(certificates[0]);
         final String principal = certificatePrincipal.toString();
@@ -171,4 +174,17 @@ public class X509IdentityProvider implements 
IdentityProvider {
         certificate.checkValidity();
     }
 
+    private static void rejectMultipleHeaderValues(final HttpServletRequest 
request, final String headerName) {
+        final Enumeration<String> headerValues = 
request.getHeaders(headerName);
+        int valueCount = 0;
+        while (headerValues.hasMoreElements()) {
+            headerValues.nextElement();
+            valueCount++;
+        }
+
+        if (valueCount > 1) {
+            throw new InvalidCredentialsException("Request provided multiple 
values for header %s".formatted(headerName));
+        }
+    }
+
 }
diff --git 
a/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProviderTest.java
 
b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProviderTest.java
new file mode 100644
index 00000000000..eb7770e226b
--- /dev/null
+++ 
b/nifi-registry/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProviderTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.nifi.registry.web.security.authentication.x509;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import 
org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import 
org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+
+import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class X509IdentityProviderTest {
+
+    private static final String HTTP_METHOD = "GET";
+
+    private static final String CERTIFICATE_PRINCIPAL = "CN=node";
+
+    private static final String ALICE = "alice";
+
+    private static final String BOB = "bob";
+
+    private X509IdentityProvider provider;
+
+    private X509PrincipalExtractor principalExtractor;
+
+    private X509Certificate certificate;
+
+    @BeforeEach
+    void setup() {
+        principalExtractor = mock(X509PrincipalExtractor.class);
+        certificate = mock(X509Certificate.class);
+        final X509CertificateExtractor certificateExtractor = 
mock(X509CertificateExtractor.class);
+        
when(certificateExtractor.extractClientCertificate(any(HttpServletRequest.class))).thenReturn(new
 X509Certificate[] {certificate});
+        provider = new X509IdentityProvider(principalExtractor, 
certificateExtractor);
+    }
+
+    @Test
+    void testMultipleProxiedEntitiesChainValuesRejected() {
+        final HttpServletRequest request = 
secureRequest(formatEntities(ALICE), formatEntities(BOB));
+
+        assertThrows(InvalidCredentialsException.class, () -> 
provider.extractCredentials(request));
+    }
+
+    @Test
+    void testSingleProxiedEntitiesChainPassedThrough() {
+        
when(principalExtractor.extractPrincipal(certificate)).thenReturn(CERTIFICATE_PRINCIPAL);
+        final String chain = formatEntities(ALICE);
+        final HttpServletRequest request = secureRequest(chain);
+
+        final AuthenticationRequest authenticationRequest = 
provider.extractCredentials(request);
+
+        final X509AuthenticationRequestDetails details = 
assertInstanceOf(X509AuthenticationRequestDetails.class, 
authenticationRequest.getDetails());
+        assertEquals(chain, details.getProxiedEntitiesChain());
+        assertEquals(HTTP_METHOD, details.getHttpMethod());
+    }
+
+    private HttpServletRequest secureRequest(final String... chainValues) {
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.isSecure()).thenReturn(true);
+        when(request.getMethod()).thenReturn(HTTP_METHOD);
+        
when(request.getHeaders(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN)).thenReturn(Collections.enumeration(List.of(chainValues)));
+        
when(request.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN)).thenReturn(chainValues[0]);
+        return request;
+    }
+
+    private static String formatEntities(final String... entities) {
+        final StringBuilder formattedEntities = new StringBuilder();
+        for (final String entity : entities) {
+            formattedEntities.append('<').append(entity).append('>');
+        }
+        return formattedEntities.toString();
+    }
+}

Reply via email to