You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by th...@apache.org on 2021/10/01 00:39:03 UTC

[nifi] branch main updated: NIFI-9241 Refactored CSRF mitigation using random Request-Token

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

thenatog 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 e16a6c2  NIFI-9241 Refactored CSRF mitigation using random Request-Token
e16a6c2 is described below

commit e16a6c2b89879034be65cca56b33724914b54033
Author: exceptionfactory <ex...@apache.org>
AuthorDate: Tue Sep 28 01:00:47 2021 -0500

    NIFI-9241 Refactored CSRF mitigation using random Request-Token
    
    - Replaced use of Authorization header with custom Request-Token header for CSRF mitigation
    - Added Request-Token cookie for CSRF mitigation
    - Replaced session storage of JWT with expiration in seconds
    - Removed and disabled CORS configuration
    - Disabled HTTP OPTIONS method
    - Refactored HTTP Proxy URI construction using RequestUriBuilder
    
    Signed-off-by: Nathan Gough <th...@gmail.com>
    
    This closes #5417.
---
 .../nifi/web/filter/SanitizeContextPathFilter.java |  13 +-
 .../apache/nifi/web/util/RequestUriBuilder.java    |  88 ++++++++++++
 .../java/org/apache/nifi/web/util/WebUtils.java    |  60 +++-----
 .../nifi/web/util/RequestUriBuilderTest.java       | 100 +++++++++++++
 .../apache/nifi/web/util/WebUtilsGroovyTest.groovy | 156 +-------------------
 .../replication/ThreadPoolRequestReplicator.java   |   1 +
 .../resources/org/apache/nifi/web/webdefault.xml   |   6 +-
 .../apache/nifi/web/NiFiCsrfTokenRepository.java   |  91 ------------
 .../nifi/web/NiFiWebApiSecurityConfiguration.java  |  28 +---
 .../org/apache/nifi/web/api/AccessResource.java    |  26 +---
 .../apache/nifi/web/api/ApplicationResource.java   |  39 +----
 .../nifi/web/api/ApplicationResourceTest.groovy    | 123 +++-------------
 .../nifi/web/api/TestDataTransferResource.java     |  10 +-
 .../security/csrf}/CsrfCookieRequestMatcher.java   |   2 +-
 .../csrf/StandardCookieCsrfTokenRepository.java    | 126 ++++++++++++++++
 .../nifi/web/security/http/SecurityCookieName.java |   2 +
 .../nifi/web/security/http/SecurityHeader.java     |   4 +
 .../StandardCookieCsrfTokenRepositoryTest.java     | 160 +++++++++++++++++++++
 .../src/main/webapp/js/nf/canvas/nf-canvas.js      |  23 +--
 .../src/main/webapp/js/nf/login/nf-login.js        |   5 +-
 .../src/main/webapp/js/nf/nf-ajax-setup.js         |  16 +--
 .../main/webapp/js/nf/nf-authorization-storage.js  |  18 ++-
 .../nifi-web-ui/src/main/webapp/js/nf/nf-common.js |  34 ++++-
 23 files changed, 631 insertions(+), 500 deletions(-)

diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/filter/SanitizeContextPathFilter.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/filter/SanitizeContextPathFilter.java
index 02d8bc3..7079e9d 100644
--- a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/filter/SanitizeContextPathFilter.java
+++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/filter/SanitizeContextPathFilter.java
@@ -17,6 +17,9 @@
 package org.apache.nifi.web.filter;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -26,25 +29,23 @@ import javax.servlet.ServletResponse;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.web.util.WebUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * This filter intercepts a request and populates the {@code contextPath} attribute on the request with a sanitized value (originally) retrieved from {@code nifi.properties}.
  */
 public class SanitizeContextPathFilter implements Filter {
-    private static final Logger logger = LoggerFactory.getLogger(SanitizeContextPathFilter.class);
     private static final String ALLOWED_CONTEXT_PATHS_PARAMETER_NAME = "allowedContextPaths";
 
     private String allowedContextPaths = "";
+    private List<String> parsedAllowedContextPaths = Collections.emptyList();
 
     @Override
     public void init(FilterConfig filterConfig) throws ServletException {
         String providedAllowedList = filterConfig.getServletContext().getInitParameter(ALLOWED_CONTEXT_PATHS_PARAMETER_NAME);
 
-        logger.debug("SanitizeContextPathFilter received provided allowed context paths from NiFi properties: " + providedAllowedList);
         if (StringUtils.isNotBlank(providedAllowedList)) {
             allowedContextPaths = providedAllowedList;
+            parsedAllowedContextPaths = Arrays.asList(StringUtils.split(providedAllowedList, ','));
         }
     }
 
@@ -64,10 +65,8 @@ public class SanitizeContextPathFilter implements Filter {
      */
     protected void injectContextPathAttribute(ServletRequest request) {
         // Capture the provided context path headers and sanitize them before using in the response
-        String contextPath = WebUtils.sanitizeContextPath(request, allowedContextPaths, "");
+        String contextPath = WebUtils.sanitizeContextPath(request, parsedAllowedContextPaths, "");
         request.setAttribute("contextPath", contextPath);
-
-        logger.debug("SanitizeContextPathFilter set contextPath: " + contextPath);
     }
 
     @Override
diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/RequestUriBuilder.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/RequestUriBuilder.java
new file mode 100644
index 0000000..fb77886
--- /dev/null
+++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/RequestUriBuilder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+/**
+ * Request URI Builder encapsulates URI construction handling supported HTTP proxy request headers
+ */
+public class RequestUriBuilder {
+    private final String scheme;
+
+    private final String host;
+
+    private final int port;
+
+    private final String contextPath;
+
+    private String path;
+
+    private RequestUriBuilder(final String scheme, final String host, final int port, final String contextPath) {
+        this.scheme = scheme;
+        this.host = host;
+        this.port = port;
+        this.contextPath = contextPath;
+    }
+
+    /**
+     * Return Builder from HTTP Servlet Request using Scheme, Host, Port, and Context Path reading from headers
+     *
+     * @param httpServletRequest HTTP Servlet Request
+     * @param allowedContextPaths Comma-separated string of allowed context path values for proxy headers
+     * @return Request URI Builder
+     */
+    public static RequestUriBuilder fromHttpServletRequest(final HttpServletRequest httpServletRequest, final List<String> allowedContextPaths) {
+        final String scheme = StringUtils.defaultIfEmpty(WebUtils.determineProxiedScheme(httpServletRequest), httpServletRequest.getScheme());
+        final String host = WebUtils.determineProxiedHost(httpServletRequest);
+        final int port = WebUtils.getServerPort(httpServletRequest);
+        final String contextPath = WebUtils.determineContextPath(httpServletRequest);
+        WebUtils.verifyContextPath(allowedContextPaths, contextPath);
+        return new RequestUriBuilder(scheme, host, port, contextPath);
+    }
+
+    /**
+     * Set Path appended to Context Path on build
+     *
+     * @param path Path may be null
+     * @return Request URI Builder
+     */
+    public RequestUriBuilder path(final String path) {
+        this.path = path;
+        return this;
+    }
+
+    /**
+     * Build URI using configured properties
+     *
+     * @return URI
+     * @throws IllegalArgumentException Thrown on URI syntax exceptions
+     */
+    public URI build() {
+        final String resourcePath = StringUtils.join(contextPath, path);
+        try {
+            return new URI(scheme, null, host, port, resourcePath, null, null);
+        } catch (final URISyntaxException e) {
+            throw new IllegalArgumentException("Build URI Failed", e);
+        }
+    }
+}
diff --git a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
index 32dbb53..fc1fc08 100644
--- a/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
+++ b/nifi-commons/nifi-web-utils/src/main/java/org/apache/nifi/web/util/WebUtils.java
@@ -29,8 +29,6 @@ import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.client.Client;
 import javax.ws.rs.client.ClientBuilder;
 import javax.ws.rs.core.UriBuilderException;
-import java.net.URI;
-import java.util.Arrays;
 import java.util.List;
 import java.util.stream.Stream;
 
@@ -53,6 +51,8 @@ public final class WebUtils {
     public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context";
     public static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix";
 
+    private static final String HOST_HEADER = "Host";
+
     private static final String EMPTY = "";
 
     private WebUtils() {
@@ -114,50 +114,21 @@ public final class WebUtils {
     }
 
     /**
-     * This method will check the provided context path headers against an allow list (provided in nifi.properties) and throw an exception if the requested context path is not registered.
-     *
-     * @param uri                     the request URI
-     * @param request                 the HTTP request
-     * @param allowedContextPaths     comma-separated list of valid context paths
-     * @return the resource path
-     * @throws UriBuilderException if the requested context path is not registered (header poisoning)
-     */
-    public static String getResourcePath(URI uri, HttpServletRequest request, String allowedContextPaths) throws UriBuilderException {
-        String resourcePath = uri.getPath();
-
-        // Determine and normalize the context path
-        String determinedContextPath = determineContextPath(request);
-        determinedContextPath = normalizeContextPath(determinedContextPath);
-
-        // If present, check it and prepend to the resource path
-        if (StringUtils.isNotBlank(determinedContextPath)) {
-            verifyContextPath(allowedContextPaths, determinedContextPath);
-
-            // Determine the complete resource path
-            resourcePath = determinedContextPath + resourcePath;
-        }
-
-        return resourcePath;
-    }
-
-    /**
      * Throws an exception if the provided context path is not in the allowed context paths list.
      *
-     * @param allowedContextPaths a comma-delimited list of valid context paths
+     * @param allowedContextPaths list of valid context paths
      * @param determinedContextPath   the normalized context path from a header
      * @throws UriBuilderException if the context path is not safe
      */
-    public static void verifyContextPath(String allowedContextPaths, String determinedContextPath) throws UriBuilderException {
+    public static void verifyContextPath(final List<String> allowedContextPaths, final String determinedContextPath) throws UriBuilderException {
         // If blank, ignore
         if (StringUtils.isBlank(determinedContextPath)) {
             return;
         }
 
         // Check it against the allowed list
-        List<String> individualContextPaths = Arrays.asList(StringUtils.split(allowedContextPaths, ","));
-        if (!individualContextPaths.contains(determinedContextPath)) {
+        if (!allowedContextPaths.contains(determinedContextPath)) {
             final String msg = "The provided context path [" + determinedContextPath + "] was not registered as allowed [" + allowedContextPaths + "]";
-            logger.error(msg);
             throw new UriBuilderException(msg);
         }
     }
@@ -191,11 +162,11 @@ public final class WebUtils {
      * If no headers are present specifying this value, it is an empty string.
      *
      * @param request the HTTP request
-     * @param allowedContextPaths the comma-separated list of allowed context paths
+     * @param allowedContextPaths list of allowed context paths
      * @param jspDisplayName the display name of the resource for log messages
      * @return the context path safe to be printed to the page
      */
-    public static String sanitizeContextPath(ServletRequest request, String allowedContextPaths, String jspDisplayName) {
+    public static String sanitizeContextPath(final ServletRequest request, final List<String> allowedContextPaths, String jspDisplayName) {
         if (StringUtils.isBlank(jspDisplayName)) {
             jspDisplayName = "JSP page";
         }
@@ -290,7 +261,7 @@ public final class WebUtils {
      * @return the determined host
      */
     public static String determineProxiedHost(final HttpServletRequest httpServletRequest) {
-        final String hostHeaderValue = getFirstHeaderValue(httpServletRequest, PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
+        final String hostHeaderValue = getFirstHeaderValue(httpServletRequest, PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER, HOST_HEADER);
         final String proxiedHost = determineProxiedHost(hostHeaderValue);
         return StringUtils.isBlank(proxiedHost) ? httpServletRequest.getServerName() : proxiedHost;
     }
@@ -320,6 +291,21 @@ public final class WebUtils {
     }
 
     /**
+     * Get Server Port based on Proxy Headers with fallback to HttpServletRequest.getServerPort()
+     *
+     * @param httpServletRequest HTTP Servlet Request
+     * @return Server port number
+     */
+    public static int getServerPort(final HttpServletRequest httpServletRequest) {
+        final String port = determineProxiedPort(httpServletRequest);
+        try {
+            return Integer.parseInt(port);
+        } catch (final NumberFormatException e) {
+            return httpServletRequest.getServerPort();
+        }
+    }
+
+    /**
      * Determines the port based on first considering proxy related headers and falling back to the port of the servlet request.
      *
      * @param httpServletRequest the request
diff --git a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/RequestUriBuilderTest.java b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/RequestUriBuilderTest.java
new file mode 100644
index 0000000..039d1bb
--- /dev/null
+++ b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/RequestUriBuilderTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class RequestUriBuilderTest {
+    private static final String HOST_HEADER = "Host";
+
+    private static final String SCHEME = "https";
+
+    private static final String HOST = "localhost.local";
+
+    private static final int PORT = 443;
+
+    private static final String CONTEXT_PATH = "/context-path";
+
+    @Mock
+    private HttpServletRequest httpServletRequest;
+
+    @Test
+    public void testFromHttpServletRequestBuild() {
+        when(httpServletRequest.getServerPort()).thenReturn(PORT);
+        when(httpServletRequest.getScheme()).thenReturn(SCHEME);
+        lenient().when(httpServletRequest.getHeader(eq(HOST_HEADER))).thenReturn(HOST);
+
+        final RequestUriBuilder builder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest, Collections.emptyList());
+        final URI uri = builder.build();
+
+        assertNotNull(uri);
+        assertEquals(SCHEME, uri.getScheme());
+        assertEquals(HOST, uri.getHost());
+        assertEquals(PORT, uri.getPort());
+        assertEquals(StringUtils.EMPTY, uri.getPath());
+    }
+
+    @Test
+    public void testFromHttpServletRequestPathBuild() {
+        when(httpServletRequest.getServerPort()).thenReturn(PORT);
+        when(httpServletRequest.getScheme()).thenReturn(SCHEME);
+        lenient().when(httpServletRequest.getHeader(eq(HOST_HEADER))).thenReturn(HOST);
+
+        final RequestUriBuilder builder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest, Collections.emptyList());
+        builder.path(CONTEXT_PATH);
+        final URI uri = builder.build();
+
+        assertNotNull(uri);
+        assertEquals(SCHEME, uri.getScheme());
+        assertEquals(HOST, uri.getHost());
+        assertEquals(PORT, uri.getPort());
+        assertEquals(CONTEXT_PATH, uri.getPath());
+    }
+
+    @Test
+    public void testFromHttpServletRequestProxyHeadersBuild() {
+        when(httpServletRequest.getHeader(eq(WebUtils.PROXY_SCHEME_HTTP_HEADER))).thenReturn(SCHEME);
+        when(httpServletRequest.getHeader(eq(WebUtils.PROXY_HOST_HTTP_HEADER))).thenReturn(HOST);
+        when(httpServletRequest.getHeader(eq(WebUtils.PROXY_PORT_HTTP_HEADER))).thenReturn(Integer.toString(PORT));
+        when(httpServletRequest.getHeader(eq(WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER))).thenReturn(CONTEXT_PATH);
+
+        final RequestUriBuilder builder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest, Arrays.asList(CONTEXT_PATH));
+        final URI uri = builder.build();
+
+        assertNotNull(uri);
+        assertEquals(SCHEME, uri.getScheme());
+        assertEquals(HOST, uri.getHost());
+        assertEquals(PORT, uri.getPort());
+        assertEquals(CONTEXT_PATH, uri.getPath());
+    }
+}
diff --git a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsGroovyTest.groovy b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsGroovyTest.groovy
index c8eb7ff..7d7781e 100644
--- a/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsGroovyTest.groovy
+++ b/nifi-commons/nifi-web-utils/src/test/groovy/org/apache/nifi/web/util/WebUtilsGroovyTest.groovy
@@ -18,15 +18,10 @@ package org.apache.nifi.web.util
 
 import org.apache.http.conn.ssl.DefaultHostnameVerifier
 import org.glassfish.jersey.client.ClientConfig
-import org.junit.After
-import org.junit.Before
-import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.mockito.Mockito
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
 import sun.security.tools.keytool.CertAndKeyGen
 import sun.security.x509.X500Name
 
@@ -40,38 +35,18 @@ import java.security.cert.X509Certificate
 
 @RunWith(JUnit4.class)
 class WebUtilsGroovyTest extends GroovyTestCase {
-    private static final Logger logger = LoggerFactory.getLogger(WebUtilsGroovyTest.class)
-
     static final String PCP_HEADER = "X-ProxyContextPath"
     static final String FC_HEADER = "X-Forwarded-Context"
     static final String FP_HEADER = "X-Forwarded-Prefix"
 
     static final String ALLOWED_PATH = "/some/context/path"
-    private static final String OCSP_REQUEST_CONTENT_TYPE = "application/ocsp-request"
-
-    @BeforeClass
-    static void setUpOnce() throws Exception {
-        logger.metaClass.methodMissing = { String name, args ->
-            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
-        }
-    }
-
-    @Before
-    void setUp() throws Exception {
-    }
-
-    @After
-    void tearDown() throws Exception {
-    }
 
     HttpServletRequest mockRequest(Map keys) {
         HttpServletRequest mockRequest = [
                 getContextPath: { ->
-                    logger.mock("Request.getContextPath() -> default/path")
                     "default/path"
                 },
                 getHeader     : { String k ->
-                    logger.mock("Request.getHeader($k) -> ${keys}")
                     switch (k) {
                         case PCP_HEADER:
                             return keys["proxy"]
@@ -108,7 +83,6 @@ class WebUtilsGroovyTest extends GroovyTestCase {
         // Act
         requests.each { HttpServletRequest request ->
             String determinedContextPath = WebUtils.determineContextPath(request)
-            logger.info("Determined context path: ${determinedContextPath}")
 
             // Assert
             assert determinedContextPath == CORRECT_CONTEXT_PATH
@@ -135,7 +109,6 @@ class WebUtilsGroovyTest extends GroovyTestCase {
         // Act
         requests.each { HttpServletRequest request ->
             String determinedContextPath = WebUtils.determineContextPath(request)
-            logger.info("Determined context path: ${determinedContextPath}")
 
             // Assert
             assert determinedContextPath == CORRECT_CONTEXT_PATH
@@ -154,7 +127,6 @@ class WebUtilsGroovyTest extends GroovyTestCase {
         // Act
         contextPaths.each { String contextPath ->
             String normalizedContextPath = WebUtils.normalizeContextPath(contextPath)
-            logger.info("Normalized context path: ${normalizedContextPath} <- ${contextPath}")
 
             // Assert
             assert normalizedContextPath == CORRECT_CONTEXT_PATH
@@ -162,146 +134,31 @@ class WebUtilsGroovyTest extends GroovyTestCase {
     }
 
     @Test
-    void testGetResourcePathShouldBlockContextPathHeaderIfNotInAllowList() throws Exception {
-        // Arrange
-        logger.info("Allowed path(s): ")
-
-        HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "any/context/path"])
-        HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "any/context/path", forward: "any/other/context/path"])
-        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "any/context/path", forward: "any/other/context/path",
-                                                                                    prefix: "any/other/prefix/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithProxyAndForwardHeader, requestWithProxyAndForwardAndPrefixHeader]
-
-        // Act
-        requests.each { HttpServletRequest request ->
-            def msg = shouldFail(UriBuilderException) {
-                String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, "")
-                logger.unexpected("Generated Resource Path: ${generatedResourcePath}")
-            }
-
-            // Assert
-            logger.expected(msg)
-            assert msg =~ "The provided context path \\[.*\\] was not registered as allowed \\[\\]"
-        }
-    }
-
-    @Test
-    void testGetResourcePathShouldAllowContextPathHeaderIfInAllowList() throws Exception {
-        // Arrange
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
-
-        HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
-        HttpServletRequest requestWithForwardHeader = mockRequest([forward: "some/context/path"])
-        HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path"])
-        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy: "some/context/path", forward: "any/other/context/path",
-                                                                                    prefix: "any/other/prefix/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
-                                             requestWithProxyAndForwardAndPrefixHeader]
-
-        // Act
-        requests.each { HttpServletRequest request ->
-            String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, ALLOWED_PATH)
-            logger.info("Generated Resource Path: ${generatedResourcePath}")
-
-            // Assert
-            assert generatedResourcePath == "${ALLOWED_PATH}/actualResource"
-        }
-    }
-
-    @Test
-    void testGetResourcePathShouldAllowContextPathHeaderIfElementInMultipleAllowLists() throws Exception {
-        // Arrange
-        String multipleAllowedPaths = [ALLOWED_PATH, "/another/path", "/a/third/path", "/a/prefix/path"].join(",")
-        logger.info("Allowed path(s): ${multipleAllowedPaths}")
-
-        final List<String> VALID_RESOURCE_PATHS = multipleAllowedPaths.split(",").collect { "$it/actualResource" }
-
-        HttpServletRequest requestWithProxyHeader = mockRequest([proxy: "some/context/path"])
-        HttpServletRequest requestWithForwardHeader = mockRequest([forward: "another/path"])
-        HttpServletRequest requestWithPrefixHeader = mockRequest([prefix: "a/prefix/path"])
-        HttpServletRequest requestWithProxyAndForwardHeader = mockRequest([proxy: "a/third/path", forward: "any/other/context/path"])
-        HttpServletRequest requestWithProxyAndForwardAndPrefixHeader = mockRequest([proxy : "a/third/path", forward: "any/other/context/path",
-                                                                                    prefix: "any/other/prefix/path"])
-        List<HttpServletRequest> requests = [requestWithProxyHeader, requestWithForwardHeader, requestWithProxyAndForwardHeader,
-                                             requestWithPrefixHeader, requestWithProxyAndForwardAndPrefixHeader]
-
-        // Act
-        requests.each { HttpServletRequest request ->
-            String generatedResourcePath = WebUtils.getResourcePath(new URI('https://nifi.apache.org/actualResource'), request, multipleAllowedPaths)
-            logger.info("Generated Resource Path: ${generatedResourcePath}")
-
-            // Assert
-            assert VALID_RESOURCE_PATHS.any { it == generatedResourcePath }
-        }
-    }
-
-    @Test
     void testVerifyContextPathShouldAllowContextPathHeaderIfInAllowList() throws Exception {
-        // Arrange
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
-        String contextPath = ALLOWED_PATH
-
-        // Act
-        logger.info("Testing [${contextPath}] against ${ALLOWED_PATH}")
-        WebUtils.verifyContextPath(ALLOWED_PATH, contextPath)
-        logger.info("Verified [${contextPath}]")
-
-        // Assert
-        // Would throw exception if invalid
+        WebUtils.verifyContextPath(Arrays.asList(ALLOWED_PATH), ALLOWED_PATH)
     }
 
     @Test
     void testVerifyContextPathShouldAllowContextPathHeaderIfInMultipleAllowLists() throws Exception {
-        // Arrange
-        String multipleAllowLists = [ALLOWED_PATH, WebUtils.normalizeContextPath(ALLOWED_PATH.reverse())].join(",")
-        logger.info("Allowed path(s): ${multipleAllowLists}")
-        String contextPath = ALLOWED_PATH
-
-        // Act
-        logger.info("Testing [${contextPath}] against ${multipleAllowLists}")
-        WebUtils.verifyContextPath(multipleAllowLists, contextPath)
-        logger.info("Verified [${contextPath}]")
-
-        // Assert
-        // Would throw exception if invalid
+        WebUtils.verifyContextPath(Arrays.asList(ALLOWED_PATH, ALLOWED_PATH.reverse()), ALLOWED_PATH)
     }
 
     @Test
     void testVerifyContextPathShouldAllowContextPathHeaderIfBlank() throws Exception {
-        // Arrange
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
-
         def emptyContextPaths = ["", "  ", "\t", null]
-
-        // Act
         emptyContextPaths.each { String contextPath ->
-            logger.info("Testing [${contextPath}] against ${ALLOWED_PATH}")
-            WebUtils.verifyContextPath(ALLOWED_PATH, contextPath)
-            logger.info("Verified [${contextPath}]")
-
-            // Assert
-            // Would throw exception if invalid
+            WebUtils.verifyContextPath(Arrays.asList(ALLOWED_PATH), contextPath)
         }
     }
 
     @Test
     void testVerifyContextPathShouldBlockContextPathHeaderIfNotAllowed() throws Exception {
-        // Arrange
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
-
-        def invalidContextPaths = ["/other/path", "somesite.com", "/../trying/to/escape"]
+        def invalidContextPaths = ["/other/path", "localhost", "/../trying/to/escape"]
 
-        // Act
         invalidContextPaths.each { String contextPath ->
-            logger.info("Testing [${contextPath}] against ${ALLOWED_PATH}")
-            def msg = shouldFail(UriBuilderException) {
-                WebUtils.verifyContextPath(ALLOWED_PATH, contextPath)
-                logger.info("Verified [${contextPath}]")
+            shouldFail(UriBuilderException) {
+                WebUtils.verifyContextPath(Arrays.asList(ALLOWED_PATH), contextPath)
             }
-
-            // Assert
-            logger.expected(msg)
-            assert msg =~ " was not registered as allowed "
         }
     }
 
@@ -460,7 +317,6 @@ class WebUtilsGroovyTest extends GroovyTestCase {
         hostnameVerifier.verify(hostname, cert)
     }
 
-
     X509Certificate generateCertificate(String DN) {
          CertAndKeyGen certGenerator = new CertAndKeyGen("RSA", "SHA256WithRSA", null)
          certGenerator.generate(2048)
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
index d5d9242..555ae9e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-cluster/src/main/java/org/apache/nifi/cluster/coordination/http/replication/ThreadPoolRequestReplicator.java
@@ -250,6 +250,7 @@ public class ThreadPoolRequestReplicator implements RequestReplicator {
         // request is replicated
         removeCookie(headers, nifiProperties.getKnoxCookieName());
         removeCookie(headers, SecurityCookieName.AUTHORIZATION_BEARER.getName());
+        removeCookie(headers, SecurityCookieName.REQUEST_TOKEN.getName());
 
         // remove the host header
         headers.remove("Host");
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/resources/org/apache/nifi/web/webdefault.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/resources/org/apache/nifi/web/webdefault.xml
index f686689..74a1059 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/resources/org/apache/nifi/web/webdefault.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/resources/org/apache/nifi/web/webdefault.xml
@@ -538,17 +538,19 @@
 
     <security-constraint>
         <web-resource-collection>
-            <web-resource-name>Disable TRACE</web-resource-name>
+            <web-resource-name>Disable TRACE and OPTIONS</web-resource-name>
             <url-pattern>/</url-pattern>
             <http-method>TRACE</http-method>
+            <http-method>OPTIONS</http-method>
         </web-resource-collection>
         <auth-constraint/>
     </security-constraint>
     <security-constraint>
         <web-resource-collection>
-            <web-resource-name>Enable everything but TRACE</web-resource-name>
+            <web-resource-name>Enable everything but TRACE and OPTIONS</web-resource-name>
             <url-pattern>/</url-pattern>
             <http-method-omission>TRACE</http-method-omission>
+            <http-method-omission>OPTIONS</http-method-omission>
         </web-resource-collection>
     </security-constraint>
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
deleted file mode 100644
index 1c4fc4c..0000000
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiCsrfTokenRepository.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * 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;
-
-
-import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
-import org.springframework.security.web.csrf.CsrfToken;
-import org.springframework.security.web.csrf.CsrfTokenRepository;
-import org.springframework.security.web.csrf.DefaultCsrfToken;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * A {@link CsrfTokenRepository} implementation for NiFi that matches the NiFi Cookie JWT against the
- * Authorization header JWT to protect against CSRF. If the request is an idempotent method type, then only the Cookie
- * is required to be present - this allows authenticating access to static resources using a Cookie. If the request is a non-idempotent
- * method, NiFi requires the Authorization header (eg. for POST requests).
- */
-public final class NiFiCsrfTokenRepository implements CsrfTokenRepository {
-
-    private static String EMPTY = "empty";
-    private CookieCsrfTokenRepository cookieRepository;
-
-    public NiFiCsrfTokenRepository() {
-        cookieRepository = new CookieCsrfTokenRepository();
-    }
-
-    @Override
-    public CsrfToken generateToken(HttpServletRequest request) {
-        // Return an empty value CsrfToken - it will not be saved to the response as our CSRF token is added elsewhere
-        return new DefaultCsrfToken(EMPTY, EMPTY, EMPTY);
-    }
-
-    @Override
-    public void saveToken(CsrfToken token, HttpServletRequest request,
-                          HttpServletResponse response) {
-        // Do nothing - we don't need to add new CSRF tokens to the response
-    }
-
-    @Override
-    public CsrfToken loadToken(HttpServletRequest request) {
-        CsrfToken cookie = cookieRepository.loadToken(request);
-        // We add the Bearer string here in order to match the Authorization header on comparison in CsrfFilter
-        return cookie != null ? new DefaultCsrfToken(cookie.getHeaderName(), cookie.getParameterName(), String.format("Bearer %s", cookie.getToken())) : null;
-    }
-
-    /**
-     * Sets the name of the HTTP request parameter that should be used to provide a token.
-     *
-     * @param parameterName the name of the HTTP request parameter that should be used to
-     * provide a token
-     */
-    public void setParameterName(String parameterName) {
-        cookieRepository.setParameterName(parameterName);
-    }
-
-    /**
-     * Sets the name of the HTTP header that should be used to provide the token.
-     *
-     * @param headerName the name of the HTTP header that should be used to provide the
-     * token
-     */
-    public void setHeaderName(String headerName) {
-        cookieRepository.setHeaderName(headerName);
-    }
-
-    /**
-     * Sets the name of the cookie that the expected CSRF token is saved to and read from.
-     *
-     * @param cookieName the name of the cookie that the expected CSRF token is saved to
-     * and read from
-     */
-    public void setCookieName(String cookieName) {
-        cookieRepository.setCookieName(cookieName);
-    }
-}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
index d639a03..4d16cf3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/NiFiWebApiSecurityConfiguration.java
@@ -19,8 +19,8 @@ package org.apache.nifi.web;
 import org.apache.nifi.util.NiFiProperties;
 import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
 import org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationProvider;
-import org.apache.nifi.web.security.http.SecurityCookieName;
-import org.apache.nifi.web.security.http.SecurityHeader;
+import org.apache.nifi.web.security.csrf.CsrfCookieRequestMatcher;
+import org.apache.nifi.web.security.csrf.StandardCookieCsrfTokenRepository;
 import org.apache.nifi.web.security.jwt.resolver.StandardBearerTokenResolver;
 import org.apache.nifi.web.security.knox.KnoxAuthenticationFilter;
 import org.apache.nifi.web.security.knox.KnoxAuthenticationProvider;
@@ -47,11 +47,6 @@ import org.springframework.security.web.authentication.AnonymousAuthenticationFi
 import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
 import org.springframework.security.web.csrf.CsrfFilter;
 import org.springframework.security.web.util.matcher.AndRequestMatcher;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.CorsConfigurationSource;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-
-import java.util.Arrays;
 
 /**
  * NiFi Web Api Spring security. Applies the various NiFiAuthenticationFilter servlet filters which will extract authentication
@@ -113,16 +108,13 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {
-        NiFiCsrfTokenRepository csrfRepository = new NiFiCsrfTokenRepository();
-        csrfRepository.setHeaderName(SecurityHeader.AUTHORIZATION.getHeader());
-        csrfRepository.setCookieName(SecurityCookieName.AUTHORIZATION_BEARER.getName());
-
         http
-                .cors().and()
                 .rememberMe().disable()
                 .authorizeRequests().anyRequest().fullyAuthenticated().and()
                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
-                .csrf().requireCsrfProtectionMatcher(new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new CsrfCookieRequestMatcher())).csrfTokenRepository(csrfRepository);
+                .csrf().requireCsrfProtectionMatcher(
+                        new AndRequestMatcher(CsrfFilter.DEFAULT_CSRF_MATCHER, new CsrfCookieRequestMatcher()))
+                        .csrfTokenRepository(new StandardCookieCsrfTokenRepository(properties.getAllowedContextPathsAsList()));
 
         // x509
         http.addFilterBefore(x509FilterBean(), AnonymousAuthenticationFilter.class);
@@ -140,16 +132,6 @@ public class NiFiWebApiSecurityConfiguration extends WebSecurityConfigurerAdapte
         http.anonymous().disable();
     }
 
-
-    @Bean
-    CorsConfigurationSource corsConfigurationSource() {
-        CorsConfiguration configuration = new CorsConfiguration();
-        configuration.setAllowedMethods(Arrays.asList("HEAD", "GET"));
-        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
-        source.registerCorsConfiguration("/process-groups/*/templates/upload", configuration);
-        return source;
-    }
-
     @Bean
     @Override
     public AuthenticationManager authenticationManagerBean() throws Exception {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
index f05dbee..b184518 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/AccessResource.java
@@ -73,7 +73,6 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
@@ -233,8 +232,6 @@ public class AccessResource extends ApplicationResource {
             }
     )
     public Response getAccessStatus(@Context HttpServletRequest httpServletRequest, @Context HttpServletResponse httpServletResponse) {
-
-        // only consider user specific access over https
         if (!httpServletRequest.isSecure()) {
             throw new AuthenticationNotSupportedException(AUTHENTICATION_NOT_ENABLED_MSG);
         }
@@ -244,34 +241,21 @@ public class AccessResource extends ApplicationResource {
         try {
             final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest);
 
-            // if there is not certificate, consider a token
             if (certificates == null) {
-                // look for an authorization token in header or cookie
                 final String bearerToken = bearerTokenResolver.resolve(httpServletRequest);
-
-                // if there is no authorization header, we don't know the user
                 if (bearerToken == null) {
                     accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
-                    accessStatus.setMessage("Access Unknown: Token not found.");
-                } else if (httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION) == null) {
-                    accessStatus.setStatus(AccessStatusDTO.Status.UNKNOWN.name());
-                    accessStatus.setMessage("Access Unknown: Authorization Header not found.");
-                    // Remove Session Cookie when Authorization Header not found
-                    applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
+                    accessStatus.setMessage("Access Unknown: Certificate and Token not found.");
                 } else {
                     try {
-                        // authenticate the token
                         final BearerTokenAuthenticationToken authenticationToken = new BearerTokenAuthenticationToken(bearerToken);
                         final Authentication authentication = jwtAuthenticationProvider.authenticate(authenticationToken);
                         final NiFiUserDetails userDetails = (NiFiUserDetails) authentication.getPrincipal();
                         final String identity = userDetails.getUsername();
 
-                        // set the user identity
                         accessStatus.setIdentity(identity);
-
-                        // attempt authorize to /flow
                         accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                        accessStatus.setMessage("You are already logged in.");
+                        accessStatus.setMessage("Access Granted: Token authenticated.");
                     } catch (final AuthenticationException iae) {
                         applicationCookieService.removeCookie(getCookieResourceUri(), httpServletResponse, ApplicationCookieName.AUTHORIZATION_BEARER);
                         throw iae;
@@ -288,12 +272,9 @@ public class AccessResource extends ApplicationResource {
                     final Authentication authenticationResponse = x509AuthenticationProvider.authenticate(x509Request);
                     final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser();
 
-                    // set the user identity
                     accessStatus.setIdentity(nifiUser.getIdentity());
-
-                    // attempt authorize to /flow
                     accessStatus.setStatus(AccessStatusDTO.Status.ACTIVE.name());
-                    accessStatus.setMessage("You are already logged in.");
+                    accessStatus.setMessage("Access Granted: Certificate authenticated.");
                 } catch (final IllegalArgumentException iae) {
                     throw new InvalidAuthenticationException(iae.getMessage(), iae);
                 }
@@ -304,7 +285,6 @@ public class AccessResource extends ApplicationResource {
             throw new AdministrationException(ase.getMessage(), ase);
         }
 
-        // create the entity
         final AccessStatusEntity entity = new AccessStatusEntity();
         entity.setAccessStatus(accessStatus);
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
index ad95819..20c68a9 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ApplicationResource.java
@@ -59,6 +59,7 @@ import org.apache.nifi.web.api.entity.Entity;
 import org.apache.nifi.web.api.entity.TransactionResultEntity;
 import org.apache.nifi.web.security.ProxiedEntitiesUtils;
 import org.apache.nifi.web.security.util.CacheKey;
+import org.apache.nifi.web.util.RequestUriBuilder;
 import org.apache.nifi.web.util.WebUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -74,10 +75,8 @@ import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.UriBuilder;
-import javax.ws.rs.core.UriBuilderException;
 import javax.ws.rs.core.UriInfo;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.Enumeration;
@@ -181,39 +180,9 @@ public abstract class ApplicationResource {
     }
 
     private URI buildResourceUri(final URI uri) {
-        try {
-            final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER);
-            final String hostHeaderValue = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER);
-            final String portHeaderValue = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER);
-
-            final String host = WebUtils.determineProxiedHost(hostHeaderValue);
-            final String port = WebUtils.determineProxiedPort(hostHeaderValue, portHeaderValue);
-
-            // Catch header poisoning
-            final String allowedContextPaths = properties.getAllowedContextPaths();
-            final String resourcePath = WebUtils.getResourcePath(uri, httpServletRequest, allowedContextPaths);
-
-            // determine the port uri
-            int uriPort = uri.getPort();
-            if (StringUtils.isNumeric(port)) {
-                try {
-                    uriPort = Integer.parseInt(port);
-                } catch (final NumberFormatException nfe) {
-                    logger.warn("Parsing Proxy Port [{}] Failed: Using URI Port [{}]", port, uriPort);
-                }
-            }
-
-            return new URI(
-                    (StringUtils.isBlank(scheme)) ? uri.getScheme() : scheme,
-                    uri.getUserInfo(),
-                    (StringUtils.isBlank(host)) ? uri.getHost() : host,
-                    uriPort,
-                    resourcePath,
-                    uri.getQuery(),
-                    uri.getFragment());
-        } catch (final URISyntaxException use) {
-            throw new UriBuilderException(use);
-        }
+        final RequestUriBuilder builder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest, properties.getAllowedContextPathsAsList());
+        builder.path(uri.getPath());
+        return builder.build();
     }
 
     /**
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
index 356b067..9dcb061 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/groovy/org/apache/nifi/web/api/ApplicationResourceTest.groovy
@@ -16,56 +16,31 @@
  */
 package org.apache.nifi.web.api
 
-
 import org.apache.nifi.util.NiFiProperties
 import org.glassfish.jersey.uri.internal.JerseyUriBuilder
-import org.junit.After
-import org.junit.Before
-import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
 
 import javax.servlet.http.HttpServletRequest
 import javax.ws.rs.core.UriBuilderException
 import javax.ws.rs.core.UriInfo
 
+import static org.apache.nifi.web.util.WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.PROXY_HOST_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.PROXY_PORT_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.PROXY_SCHEME_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_CONTEXT_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_HOST_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_PORT_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_PREFIX_HTTP_HEADER
+import static org.apache.nifi.web.util.WebUtils.FORWARDED_PROTO_HTTP_HEADER
+
 @RunWith(JUnit4.class)
 class ApplicationResourceTest extends GroovyTestCase {
-    private static final Logger logger = LoggerFactory.getLogger(ApplicationResourceTest.class)
-
-    public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost"
-    public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Host"
-
-    static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"
-    static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"
-    static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"
-
-    static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"
-    static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"
-    static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"
-    static final String FORWARDED_PREFIX_HTTP_HEADER = "X-Forwarded-Prefix"
-
     static final String PROXY_CONTEXT_PATH_PROP = NiFiProperties.WEB_PROXY_CONTEXT_PATH
     static final String ALLOWED_PATH = "/some/context/path"
 
-    @BeforeClass
-    static void setUpOnce() throws Exception {
-        logger.metaClass.methodMissing = { String name, args ->
-            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
-        }
-    }
-
-    @Before
-    void setUp() throws Exception {
-    }
-
-    @After
-    void tearDown() throws Exception {
-    }
-
     class MockApplicationResource extends ApplicationResource {
         void setHttpServletRequest(HttpServletRequest request) {
             super.httpServletRequest = request
@@ -95,15 +70,16 @@ class ApplicationResourceTest extends GroovyTestCase {
             } else {
                 headerValue = ""
             }
-            logger.mock("Request.getHeader($k) -> \"$headerValue\"")
             headerValue
         }, getContextPath: { ->
-            logger.mock("Request.getContextPath() -> \"$headerValue\"")
             headerValue
+        }, getScheme: { ->
+            "https"
+        }, getServerPort: { ->
+            443
         }] as HttpServletRequest
 
         UriInfo mockUriInfo = [getBaseUriBuilder: { ->
-            logger.mock("Returning mock UriBuilder")
             new JerseyUriBuilder().uri(new URI('https://nifi.apache.org/'))
         }] as UriInfo
 
@@ -116,151 +92,92 @@ class ApplicationResourceTest extends GroovyTestCase {
 
     @Test
     void testGenerateUriShouldBlockProxyContextPathHeaderIfNotInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource()
-        logger.info("Allowed path(s): ")
-
-        // Act
-        def msg = shouldFail(UriBuilderException) {
-            String generatedUri = resource.generateResourceUri('actualResource')
-            logger.unexpected("Generated URI: ${generatedUri}")
+        shouldFail(UriBuilderException) {
+            resource.generateResourceUri('actualResource')
         }
-
-        // Assert
-        logger.expected(msg)
-        assert msg =~ "The provided context path \\[.*\\] was not registered as allowed \\[\\]"
     }
 
     @Test
     void testGenerateUriShouldAllowProxyContextPathHeaderIfInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource()
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
 
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldAllowProxyContextPathHeaderIfElementInMultipleAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource()
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
-        logger.info("Allowed path(s): ${multipleAllowedPaths}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
 
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldBlockForwardedContextHeaderIfNotInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
-        logger.info("Allowed path(s): ")
 
-        // Act
-        def msg = shouldFail(UriBuilderException) {
-            String generatedUri = resource.generateResourceUri('actualResource')
-            logger.unexpected("Generated URI: ${generatedUri}")
+        shouldFail(UriBuilderException) {
+            resource.generateResourceUri('actualResource')
         }
-
-        // Assert
-        logger.expected(msg)
-        assert msg =~ "The provided context path \\[.*\\] was not registered as allowed \\[\\]"
     }
 
     @Test
     void testGenerateUriShouldBlockForwardedPrefixHeaderIfNotInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
-        logger.info("Allowed path(s): ")
 
-        // Act
-        def msg = shouldFail(UriBuilderException) {
-            String generatedUri = resource.generateResourceUri('actualResource')
-            logger.unexpected("Generated URI: ${generatedUri}")
+        shouldFail(UriBuilderException) {
+            resource.generateResourceUri('actualResource')
         }
-
-        // Assert
-        logger.expected(msg)
-        assert msg =~ "The provided context path \\[.*\\] was not registered as allowed \\[\\]"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedContextHeaderIfInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
-
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedPrefixHeaderIfInAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
-        logger.info("Allowed path(s): ${ALLOWED_PATH}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): ALLOWED_PATH] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
-
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedContextHeaderIfElementInMultipleAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_CONTEXT_HTTP_HEADER])
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
-        logger.info("Allowed path(s): ${multipleAllowedPaths}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
-
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 
     @Test
     void testGenerateUriShouldAllowForwardedPrefixHeaderIfElementInMultipleAllowList() throws Exception {
-        // Arrange
         ApplicationResource resource = buildApplicationResource([FORWARDED_PREFIX_HTTP_HEADER])
         String multipleAllowedPaths = [ALLOWED_PATH, "another/path", "a/third/path"].join(",")
-        logger.info("Allowed path(s): ${multipleAllowedPaths}")
         NiFiProperties niFiProperties = new NiFiProperties([(PROXY_CONTEXT_PATH_PROP): multipleAllowedPaths] as Properties)
         resource.properties = niFiProperties
 
-        // Act
         String generatedUri = resource.generateResourceUri('actualResource')
-        logger.info("Generated URI: ${generatedUri}")
-
-        // Assert
         assert generatedUri == "https://nifi.apache.org:8081${ALLOWED_PATH}/actualResource"
     }
 }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
index f503395..cba3ea6 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/api/TestDataTransferResource.java
@@ -168,6 +168,10 @@ public class TestDataTransferResource {
         uriInfoField.set(resource, uriInfo);
 
         final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getServerPort()).thenReturn(8080);
+        when(request.getScheme()).thenReturn("http");
+        when(request.getHeader(eq("Host"))).thenReturn("localhost");
+
         final Field httpServletRequestField = resource.getClass().getSuperclass().getSuperclass()
                 .getDeclaredField("httpServletRequest");
         httpServletRequestField.setAccessible(true);
@@ -301,7 +305,7 @@ public class TestDataTransferResource {
     }
 
     @Test
-    public void testCommitInputPortTransaction() throws Exception {
+    public void testCommitInputPortTransaction() {
         final HttpServletRequest req = createCommonHttpServletRequest();
 
         final DataTransferResource resource = getDataTransferResource();
@@ -323,7 +327,7 @@ public class TestDataTransferResource {
     }
 
     @Test
-    public void testTransferFlowFiles() throws Exception {
+    public void testTransferFlowFiles() {
         final HttpServletRequest req = createCommonHttpServletRequest();
 
         final DataTransferResource resource = getDataTransferResource();
@@ -346,7 +350,7 @@ public class TestDataTransferResource {
     }
 
     @Test
-    public void testCommitOutputPortTransaction() throws Exception {
+    public void testCommitOutputPortTransaction() {
         final HttpServletRequest req = createCommonHttpServletRequest();
 
         final DataTransferResource resource = getDataTransferResource();
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/CsrfCookieRequestMatcher.java
similarity index 97%
rename from nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java
rename to nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/CsrfCookieRequestMatcher.java
index 474236e..69a836c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/CsrfCookieRequestMatcher.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/CsrfCookieRequestMatcher.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.nifi.web;
+package org.apache.nifi.web.security.csrf;
 
 import org.apache.nifi.web.security.http.SecurityCookieName;
 import org.springframework.security.web.util.matcher.RequestMatcher;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepository.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepository.java
new file mode 100644
index 0000000..eef4e67
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepository.java
@@ -0,0 +1,126 @@
+/*
+ * 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.csrf;
+
+import org.apache.nifi.web.security.http.SecurityCookieName;
+import org.apache.nifi.web.security.http.SecurityHeader;
+import org.apache.nifi.web.util.RequestUriBuilder;
+import org.springframework.security.web.csrf.CsrfToken;
+import org.springframework.security.web.csrf.CsrfTokenRepository;
+import org.springframework.security.web.csrf.DefaultCsrfToken;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.WebUtils;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.net.URI;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Standard implementation of CSRF Token Repository using stateless Spring Security double-submit cookie strategy
+ */
+public class StandardCookieCsrfTokenRepository implements CsrfTokenRepository {
+    private static final String REQUEST_PARAMETER = "requestToken";
+
+    private static final String ROOT_PATH = "/";
+
+    private static final String EMPTY = "";
+
+    private static final boolean SECURE_ENABLED = true;
+
+    private static final int MAX_AGE_EXPIRED = 0;
+
+    private static final int MAX_AGE_SESSION = -1;
+
+    private final List<String> allowedContextPaths;
+
+    /**
+     * Standard Cookie CSRF Token Repository with list of allowed context paths from proxy headers
+     *
+     * @param allowedContextPaths Allowed context paths from proxy headers
+     */
+    public StandardCookieCsrfTokenRepository(final List<String> allowedContextPaths) {
+        this.allowedContextPaths = Objects.requireNonNull(allowedContextPaths, "Allowed Context Paths required");
+    }
+
+    /**
+     * Generate CSRF Token or return current Token when present in HTTP Servlet Request Cookie header
+     *
+     * @param httpServletRequest HTTP Servlet Request
+     * @return CSRF Token
+     */
+    @Override
+    public CsrfToken generateToken(final HttpServletRequest httpServletRequest) {
+        CsrfToken csrfToken = loadToken(httpServletRequest);
+        if (csrfToken == null) {
+            csrfToken = getCsrfToken(generateRandomToken());
+        }
+        return csrfToken;
+    }
+
+    /**
+     * Save CSRF Token in HTTP Servlet Response using defaults that allow JavaScript read for session cookies
+     *
+     * @param csrfToken CSRF Token to be saved or null indicated the token should be removed
+     * @param httpServletRequest HTTP Servlet Request
+     * @param httpServletResponse HTTP Servlet Response
+     */
+    @Override
+    public void saveToken(final CsrfToken csrfToken, final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) {
+        final String token = csrfToken == null ? EMPTY : csrfToken.getToken();
+        final int maxAge = csrfToken == null ? MAX_AGE_EXPIRED : MAX_AGE_SESSION;
+
+        final Cookie cookie = new Cookie(SecurityCookieName.REQUEST_TOKEN.getName(), token);
+        cookie.setSecure(SECURE_ENABLED);
+        cookie.setMaxAge(maxAge);
+
+        final String cookiePath = getCookiePath(httpServletRequest);
+        cookie.setPath(cookiePath);
+        httpServletResponse.addCookie(cookie);
+    }
+
+    /**
+     * Load CSRF Token from HTTP Servlet Request Cookie header
+     *
+     * @param httpServletRequest HTTP Servlet Request
+     * @return CSRF Token or null when Cookie header not found
+     */
+    @Override
+    public CsrfToken loadToken(final HttpServletRequest httpServletRequest) {
+        final Cookie cookie = WebUtils.getCookie(httpServletRequest, SecurityCookieName.REQUEST_TOKEN.getName());
+        final String token = cookie == null ? null : cookie.getValue();
+        return StringUtils.hasLength(token) ? getCsrfToken(token) : null;
+    }
+
+    private CsrfToken getCsrfToken(final String token) {
+        return new DefaultCsrfToken(SecurityHeader.REQUEST_TOKEN.getHeader(), REQUEST_PARAMETER, token);
+    }
+
+    private String generateRandomToken() {
+        return UUID.randomUUID().toString();
+    }
+
+    private String getCookiePath(final HttpServletRequest httpServletRequest) {
+        final RequestUriBuilder requestUriBuilder = RequestUriBuilder.fromHttpServletRequest(httpServletRequest, allowedContextPaths);
+        requestUriBuilder.path(ROOT_PATH);
+        final URI uri = requestUriBuilder.build();
+        return uri.getPath();
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java
index 9e54c29..49b8ae3 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityCookieName.java
@@ -21,6 +21,8 @@ package org.apache.nifi.web.security.http;
  */
 public enum SecurityCookieName {
     /** See IETF Cookie Prefixes Draft Section 3.1 related to Secure prefix handling */
+    REQUEST_TOKEN("__Secure-Request-Token"),
+
     AUTHORIZATION_BEARER("__Secure-Authorization-Bearer");
 
     private String name;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java
index b1d36a6..d6ed19c 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/http/SecurityHeader.java
@@ -20,6 +20,10 @@ package org.apache.nifi.web.security.http;
  * Enumeration of HTTP Headers for Security
  */
 public enum SecurityHeader {
+    /** Custom Request Token Header for CSRF mitigation */
+    REQUEST_TOKEN("Request-Token"),
+
+    /** Authorization Header described in RFC 7235 Section 4.2 */
     AUTHORIZATION("Authorization");
 
     private String header;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepositoryTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepositoryTest.java
new file mode 100644
index 0000000..062417e
--- /dev/null
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/csrf/StandardCookieCsrfTokenRepositoryTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.csrf;
+
+import org.apache.nifi.web.security.http.SecurityCookieName;
+import org.apache.nifi.web.util.WebUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.security.web.csrf.CsrfToken;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.Collections;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class StandardCookieCsrfTokenRepositoryTest {
+    private static final int MAX_AGE_SESSION = -1;
+
+    private static final int MAX_AGE_EXPIRED = 0;
+
+    private static final String ROOT_PATH = "/";
+
+    private static final String CONTEXT_PATH = "/context-path";
+
+    private static final String COOKIE_CONTEXT_PATH = CONTEXT_PATH + ROOT_PATH;
+
+    private static final String HTTPS = "https";
+
+    private static final String HOST = "localhost";
+
+    private static final String PORT = "443";
+
+    private static final String EMPTY = "";
+
+    @Mock
+    private HttpServletRequest request;
+
+    @Mock
+    private HttpServletResponse response;
+
+    @Captor
+    private ArgumentCaptor<Cookie> cookieArgumentCaptor;
+
+    private StandardCookieCsrfTokenRepository repository;
+
+    @BeforeEach
+    public void setRepository() {
+        this.repository = new StandardCookieCsrfTokenRepository(Collections.emptyList());
+    }
+
+    @Test
+    public void testGenerateToken() {
+        final CsrfToken csrfToken = repository.generateToken(request);
+        assertNotNull(csrfToken);
+        assertNotNull(csrfToken.getToken());
+    }
+
+    @Test
+    public void testGenerateTokenCookieFound() {
+        final String token = UUID.randomUUID().toString();
+        final Cookie cookie = new Cookie(SecurityCookieName.REQUEST_TOKEN.getName(), token);
+        when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+        final CsrfToken csrfToken = repository.generateToken(request);
+        assertNotNull(csrfToken);
+        assertEquals(token, csrfToken.getToken());
+    }
+
+    @Test
+    public void testLoadToken() {
+        final String token = UUID.randomUUID().toString();
+        final Cookie cookie = new Cookie(SecurityCookieName.REQUEST_TOKEN.getName(), token);
+        when(request.getCookies()).thenReturn(new Cookie[]{cookie});
+
+        final CsrfToken csrfToken = repository.loadToken(request);
+        assertNotNull(csrfToken);
+        assertEquals(token, csrfToken.getToken());
+    }
+
+    @Test
+    public void testSaveToken() {
+        final CsrfToken csrfToken = repository.generateToken(request);
+        repository.saveToken(csrfToken, request, response);
+
+        verify(response).addCookie(cookieArgumentCaptor.capture());
+        final Cookie cookie = cookieArgumentCaptor.getValue();
+        assertCookieEquals(csrfToken, cookie);
+        assertEquals(ROOT_PATH, cookie.getPath());
+    }
+
+    @Test
+    public void testSaveTokenNullCsrfToken() {
+        repository.saveToken(null, request, response);
+
+        verify(response).addCookie(cookieArgumentCaptor.capture());
+        final Cookie cookie = cookieArgumentCaptor.getValue();
+        assertEquals(ROOT_PATH, cookie.getPath());
+        assertEquals(EMPTY, cookie.getValue());
+        assertEquals(MAX_AGE_EXPIRED, cookie.getMaxAge());
+        assertTrue(cookie.getSecure());
+        assertFalse(cookie.isHttpOnly());
+        assertNull(cookie.getDomain());
+    }
+
+    @Test
+    public void testSaveTokenProxyContextPath() {
+        this.repository = new StandardCookieCsrfTokenRepository(Collections.singletonList(CONTEXT_PATH));
+
+        final CsrfToken csrfToken = repository.generateToken(request);
+        when(request.getHeader(eq(WebUtils.PROXY_SCHEME_HTTP_HEADER))).thenReturn(HTTPS);
+        when(request.getHeader(eq(WebUtils.PROXY_HOST_HTTP_HEADER))).thenReturn(HOST);
+        when(request.getHeader(eq(WebUtils.PROXY_PORT_HTTP_HEADER))).thenReturn(PORT);
+        when(request.getHeader(eq(WebUtils.PROXY_CONTEXT_PATH_HTTP_HEADER))).thenReturn(CONTEXT_PATH);
+        repository.saveToken(csrfToken, request, response);
+
+        verify(response).addCookie(cookieArgumentCaptor.capture());
+        final Cookie cookie = cookieArgumentCaptor.getValue();
+        assertCookieEquals(csrfToken, cookie);
+        assertEquals(COOKIE_CONTEXT_PATH, cookie.getPath());
+    }
+
+    private void assertCookieEquals(final CsrfToken csrfToken, final Cookie cookie) {
+        assertEquals(csrfToken.getToken(), cookie.getValue());
+        assertEquals(MAX_AGE_SESSION, cookie.getMaxAge());
+        assertTrue(cookie.getSecure());
+        assertFalse(cookie.isHttpOnly());
+        assertNull(cookie.getDomain());
+    }
+}
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
index 4b280fa..11c8d96 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas.js
@@ -108,7 +108,7 @@
     var config = {
         urls: {
             api: '../nifi-api',
-            accessStatus: '../nifi-api/access',
+            accessConfig: '../nifi-api/access/config',
             currentUser: '../nifi-api/flow/current-user',
             controllerBulletins: '../nifi-api/flow/controller/bulletins',
             kerberos: '../nifi-api/access/kerberos',
@@ -865,8 +865,12 @@
         init: function () {
             // attempt kerberos/oidc/saml authentication
             var ticketExchange = $.Deferred(function (deferred) {
-                var successfulAuthentication = function (token) {
-                    nfAuthorizationStorage.setToken(token)
+                var successfulAuthentication = function (jwt) {
+                    // Use Expiration from JWT for tracking authentication status
+                    var sessionExpiration = nfCommon.getSessionExpiration(jwt);
+                    if (sessionExpiration) {
+                        nfAuthorizationStorage.setToken(sessionExpiration);
+                    }
                     deferred.resolve();
                 };
 
@@ -914,15 +918,18 @@
                             if (nfAuthorizationStorage.hasToken()) {
                                 $('#logout-link-container').show();
                             } else {
-                                // Check Access Status when Token not found to remove Session Cookie if needed
+                                // Check Access Configuration when Token not found for new browser tabs
                                 $.ajax({
                                     type: 'GET',
-                                    url: config.urls.accessStatus,
+                                    url: config.urls.accessConfig,
                                     dataType: 'json'
                                 }).done(function (response) {
-                                    var accessStatus = response.accessStatus;
-                                    if (accessStatus.status === 'UNKNOWN') {
-                                        window.location = '../nifi/login';
+                                    if (response.config.supportsLogin) {
+                                        // Show logout button when login supported
+                                        $('#logout-link-container').show();
+                                        // Set default expiration when authenticated to enable logout status
+                                        var expiration = nfCommon.getDefaultExpiration();
+                                        nfAuthorizationStorage.setToken(expiration);
                                     }
                                 }).fail(function () {
                                     window.location = '../nifi/login';
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
index d1cc08c..8cfaf94 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/login/nf-login.js
@@ -102,7 +102,10 @@
                 'password': $('#password').val()
             }
         }).done(function (jwt) {
-            nfAuthorizationStorage.setToken(jwt);
+            var sessionExpiration = nfCommon.getSessionExpiration(jwt);
+            if (sessionExpiration) {
+                nfAuthorizationStorage.setToken(sessionExpiration);
+            }
 
             // check to see if they actually have access now
             $.ajax({
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js
index ab022ba..193de91 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-ajax-setup.js
@@ -36,20 +36,12 @@
      * Performs ajax setup for use within NiFi.
      */
     $(document).ready(function ($) {
-        // include jwt when possible
         $.ajaxSetup({
             'beforeSend': function (xhr) {
-                var hadToken = nfAuthorizationStorage.hasToken();
-
-                // get the token to include in all requests
-                var token = nfAuthorizationStorage.getToken();
-                if (token !== null) {
-                    xhr.setRequestHeader('Authorization', 'Bearer ' + token);
-                } else {
-                    // if the current user was logged in with a token and the token just expired, cancel the request
-                    if (hadToken === true) {
-                        return false;
-                    }
+                // Get the Request Token for CSRF mitigation on and send on all requests
+                var requestToken = nfAuthorizationStorage.getRequestToken();
+                if (requestToken !== null) {
+                    xhr.setRequestHeader('Request-Token', requestToken);
                 }
             }
         });
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-authorization-storage.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-authorization-storage.js
index 2b78c89..1847c09 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-authorization-storage.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-authorization-storage.js
@@ -28,10 +28,26 @@
         nf.AuthorizationStorage = factory();
     }
 }(this, function () {
-    var TOKEN_ITEM_KEY = 'nifi-authorization-token';
+    var TOKEN_ITEM_KEY = 'Access-Token-Expiration';
+
+    var REQUEST_TOKEN_PATTERN = new RegExp('Request-Token=([^;]+)');
 
     return {
         /**
+         * Get Request Token from document cookies
+         *
+         * @return Request Token string or null when not found
+         */
+        getRequestToken: function () {
+            var requestToken = null;
+            var requestTokenMatcher = REQUEST_TOKEN_PATTERN.exec(document.cookie);
+            if (requestTokenMatcher) {
+                requestToken = requestTokenMatcher[1];
+            }
+            return requestToken;
+        },
+
+        /**
          * Get Token from Session Storage
          *
          * @return Bearer Token string
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
index a884f55..652cbaf 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js
@@ -507,10 +507,9 @@
             var checkExpiration = function () {
                 var token = nfAuthorizationStorage.getToken();
 
-                // ensure there is an expiration and token present
+                // Parse token as expiration in number of seconds
                 if (token !== null) {
-                    var jsonWebToken = nfCommon.getJwtPayload(token);
-                    var expiration = parseInt(jsonWebToken['exp'], 10) * nfCommon.MILLIS_PER_SECOND;
+                    var expiration = parseInt(token, 10) * nfCommon.MILLIS_PER_SECOND;
 
                     var expirationDate = new Date(expiration);
                     var now = new Date();
@@ -562,6 +561,35 @@
         },
 
         /**
+         * Get Session Expiration from JSON Web Token Payload exp claim
+         *
+         * @param {string} jwt
+         * @return {string}
+         */
+        getSessionExpiration: function(jwt) {
+            var sessionExpiration = null;
+
+            var jwtPayload = nfCommon.getJwtPayload(jwt);
+            if (jwtPayload) {
+                sessionExpiration = jwtPayload['exp'];
+            }
+
+            return sessionExpiration;
+        },
+
+        /**
+         * Get Default Session Expiration based on current time plus 12 hours as seconds
+         *
+         * @return {string}
+         */
+        getDefaultExpiration: function() {
+            var now = new Date();
+            var expiration = now.getTime() + 43200000;
+            var expirationSeconds = Math.round(expiration / 1000);
+            return expirationSeconds.toString();
+        },
+
+        /**
          * Extracts the subject from the specified jwt. If the jwt is not as expected
          * an empty string is returned.
          *