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.
*