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 a661b035e8 NIFI-10259 Improved HTTP error handling for authentication
failures
a661b035e8 is described below
commit a661b035e8903c69d63f8427d4f2f7bdaf309d89
Author: exceptionfactory <[email protected]>
AuthorDate: Thu Jul 21 17:06:41 2022 -0500
NIFI-10259 Improved HTTP error handling for authentication failures
- Added Standard AuthenticationEntryPoint
- Configured AuthenticationEntryPoint for SecurityFilterChain and
BearerTokenAuthenticationFilter
Signed-off-by: Nathan Gough <[email protected]>
This closes #6233.
---
.../nifi/web/NiFiWebApiSecurityConfiguration.java | 6 +-
.../security/StandardAuthenticationEntryPoint.java | 98 ++++++++++++++++++
.../JwtAuthenticationSecurityConfiguration.java | 9 ++
.../StandardAuthenticationEntryPointTest.java | 112 +++++++++++++++++++++
4 files changed, 222 insertions(+), 3 deletions(-)
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 a1b366c091..d43824d122 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
@@ -17,6 +17,7 @@
package org.apache.nifi.web;
import org.apache.nifi.util.NiFiProperties;
+import org.apache.nifi.web.security.StandardAuthenticationEntryPoint;
import
org.apache.nifi.web.security.anonymous.NiFiAnonymousAuthenticationFilter;
import org.apache.nifi.web.security.csrf.CsrfCookieRequestMatcher;
import org.apache.nifi.web.security.csrf.StandardCookieCsrfTokenRepository;
@@ -28,7 +29,6 @@ import
org.apache.nifi.web.security.saml2.web.authentication.logout.Saml2SingleL
import org.apache.nifi.web.security.x509.X509AuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
@@ -44,7 +44,6 @@ import
org.springframework.security.saml2.provider.service.web.authentication.lo
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.ExceptionTranslationFilter;
import
org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
-import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
@@ -72,6 +71,7 @@ public class NiFiWebApiSecurityConfiguration {
public SecurityFilterChain securityFilterChain(
final HttpSecurity http,
final NiFiProperties properties,
+ final StandardAuthenticationEntryPoint authenticationEntryPoint,
final X509AuthenticationFilter x509AuthenticationFilter,
final BearerTokenAuthenticationFilter
bearerTokenAuthenticationFilter,
final KnoxAuthenticationFilter knoxAuthenticationFilter,
@@ -118,7 +118,7 @@ public class NiFiWebApiSecurityConfiguration {
)
)
.exceptionHandling(exceptionHandling -> exceptionHandling
- .authenticationEntryPoint(new
HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
+ .authenticationEntryPoint(authenticationEntryPoint)
)
.addFilterBefore(x509AuthenticationFilter,
AnonymousAuthenticationFilter.class)
.addFilterBefore(bearerTokenAuthenticationFilter,
AnonymousAuthenticationFilter.class)
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/StandardAuthenticationEntryPoint.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/StandardAuthenticationEntryPoint.java
new file mode 100644
index 0000000000..dbe7eea195
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/StandardAuthenticationEntryPoint.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import org.apache.nifi.web.security.cookie.ApplicationCookieName;
+import org.apache.nifi.web.security.cookie.ApplicationCookieService;
+import org.apache.nifi.web.security.cookie.StandardApplicationCookieService;
+import org.apache.nifi.web.util.RequestUriBuilder;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
+import org.springframework.security.web.AuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.util.Objects;
+import java.util.Optional;
+
+/**
+ * Standard Authentication Entry Point delegates to Bearer Authentication
Entry Point and performs additional processing
+ */
+public class StandardAuthenticationEntryPoint implements
AuthenticationEntryPoint {
+ protected static final String AUTHENTICATE_HEADER = "WWW-Authenticate";
+
+ protected static final String BEARER_HEADER = "Bearer";
+
+ protected static final String UNAUTHORIZED = "Unauthorized";
+
+ private static final ApplicationCookieService applicationCookieService =
new StandardApplicationCookieService();
+
+ private final BearerTokenAuthenticationEntryPoint
bearerTokenAuthenticationEntryPoint;
+
+ public StandardAuthenticationEntryPoint(final
BearerTokenAuthenticationEntryPoint bearerTokenAuthenticationEntryPoint) {
+ this.bearerTokenAuthenticationEntryPoint =
Objects.requireNonNull(bearerTokenAuthenticationEntryPoint);
+ }
+
+ /**
+ * Commence exception handling with handling for OAuth2 Authentication
Exceptions using Bearer Token implementation
+ *
+ * @param request HTTP Servlet Request
+ * @param response HTTP Servlet Response
+ * @param exception Authentication Exception
+ * @throws IOException Thrown on response processing failures
+ * @throws ServletException Thrown on response processing failures
+ */
+ @Override
+ public void commence(final HttpServletRequest request, final
HttpServletResponse response, final AuthenticationException exception) throws
IOException, ServletException {
+ if (exception instanceof OAuth2AuthenticationException) {
+ bearerTokenAuthenticationEntryPoint.commence(request, response,
exception);
+ } else {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ }
+ removeAuthorizationBearerCookie(request, response);
+ sendErrorMessage(response);
+ }
+
+ private void sendErrorMessage(final HttpServletResponse response) throws
IOException {
+ response.setContentType(MediaType.TEXT_PLAIN_VALUE);
+ final String message = getErrorMessage(response);
+ try (final PrintWriter writer = response.getWriter()) {
+ writer.print(message);
+ }
+ }
+
+ private String getErrorMessage(final HttpServletResponse response) {
+ // Use WWW-Authenticate Header from
BearerTokenAuthenticationEntryPoint when found
+ final String authenticateHeader =
response.getHeader(AUTHENTICATE_HEADER);
+ final String errorMessage = authenticateHeader == null ? UNAUTHORIZED
: authenticateHeader;
+ return errorMessage.replaceFirst(BEARER_HEADER, UNAUTHORIZED);
+ }
+
+ private void removeAuthorizationBearerCookie(final HttpServletRequest
request, final HttpServletResponse response) {
+ final Optional<String> authorizationBearer =
applicationCookieService.getCookieValue(request,
ApplicationCookieName.AUTHORIZATION_BEARER);
+ if (authorizationBearer.isPresent()) {
+ final URI uri =
RequestUriBuilder.fromHttpServletRequest(request).build();
+ applicationCookieService.removeCookie(uri, response,
ApplicationCookieName.AUTHORIZATION_BEARER);
+ }
+ }
+}
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java
index c89c437ba2..bcdf6afe52 100644
---
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/main/java/org/apache/nifi/web/security/configuration/JwtAuthenticationSecurityConfiguration.java
@@ -28,6 +28,7 @@ import org.apache.nifi.components.state.StateManager;
import org.apache.nifi.components.state.StateManagerProvider;
import org.apache.nifi.util.NiFiProperties;
import
org.apache.nifi.web.security.jwt.converter.StandardJwtAuthenticationConverter;
+import org.apache.nifi.web.security.StandardAuthenticationEntryPoint;
import org.apache.nifi.web.security.jwt.jws.StandardJWSKeySelector;
import org.apache.nifi.web.security.jwt.jws.StandardJwsSignerProvider;
import org.apache.nifi.web.security.jwt.key.command.KeyExpirationCommand;
@@ -57,6 +58,7 @@ import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
+import
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter;
import
org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
@@ -109,6 +111,7 @@ public class JwtAuthenticationSecurityConfiguration {
public BearerTokenAuthenticationFilter
bearerTokenAuthenticationFilter(final AuthenticationManager
authenticationManager) {
final BearerTokenAuthenticationFilter bearerTokenAuthenticationFilter
= new BearerTokenAuthenticationFilter(authenticationManager);
bearerTokenAuthenticationFilter.setBearerTokenResolver(bearerTokenResolver());
+
bearerTokenAuthenticationFilter.setAuthenticationEntryPoint(authenticationEntryPoint());
return bearerTokenAuthenticationFilter;
}
@@ -117,6 +120,12 @@ public class JwtAuthenticationSecurityConfiguration {
return new StandardBearerTokenResolver();
}
+ @Bean
+ public StandardAuthenticationEntryPoint authenticationEntryPoint() {
+ final BearerTokenAuthenticationEntryPoint
bearerTokenAuthenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
+ return new
StandardAuthenticationEntryPoint(bearerTokenAuthenticationEntryPoint);
+ }
+
@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider() {
final JwtAuthenticationProvider jwtAuthenticationProvider = new
JwtAuthenticationProvider(jwtDecoder());
diff --git
a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/StandardAuthenticationEntryPointTest.java
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/StandardAuthenticationEntryPointTest.java
new file mode 100644
index 0000000000..2047a748dc
--- /dev/null
+++
b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-security/src/test/java/org/apache/nifi/web/security/StandardAuthenticationEntryPointTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+import org.apache.nifi.web.security.cookie.ApplicationCookieName;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import
org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import
org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StandardAuthenticationEntryPointTest {
+ static final String FAILED = "Authentication Failed";
+
+ static final String BEARER_TOKEN = "Bearer Token";
+
+ MockHttpServletRequest request;
+
+ MockHttpServletResponse response;
+
+ StandardAuthenticationEntryPoint authenticationEntryPoint;
+
+ @BeforeEach
+ void setAuthenticationEntryPoint() {
+ final BearerTokenAuthenticationEntryPoint
bearerTokenAuthenticationEntryPoint = new BearerTokenAuthenticationEntryPoint();
+ authenticationEntryPoint = new
StandardAuthenticationEntryPoint(bearerTokenAuthenticationEntryPoint);
+
+ request = new MockHttpServletRequest();
+ response = new MockHttpServletResponse();
+ }
+
+ @Test
+ void testCommenceAuthenticationServiceException() throws ServletException,
IOException {
+ final AuthenticationException exception = new
AuthenticationServiceException(FAILED);
+
+ authenticationEntryPoint.commence(request, response, exception);
+
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED,
response.getStatus());
+ final String authenticateHeader =
response.getHeader(StandardAuthenticationEntryPoint.AUTHENTICATE_HEADER);
+ assertNull(authenticateHeader);
+
+ final Cookie cookie =
response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
+ assertNull(cookie);
+
+ final String content = response.getContentAsString();
+ assertEquals(StandardAuthenticationEntryPoint.UNAUTHORIZED, content);
+ }
+
+ @Test
+ void testCommenceOAuth2AuthenticationException() throws ServletException,
IOException {
+ final OAuth2AuthenticationException exception = new
OAuth2AuthenticationException(FAILED);
+
+ authenticationEntryPoint.commence(request, response, exception);
+
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED,
response.getStatus());
+ final String authenticateHeader =
response.getHeader(StandardAuthenticationEntryPoint.AUTHENTICATE_HEADER);
+ assertNotNull(authenticateHeader);
+
assertTrue(authenticateHeader.startsWith(StandardAuthenticationEntryPoint.BEARER_HEADER),
"Bearer header not found");
+ assertTrue(authenticateHeader.contains(FAILED), "Header error message
not found");
+
+ final Cookie cookie =
response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
+ assertNull(cookie);
+
+ final String content = response.getContentAsString();
+
assertTrue(content.startsWith(StandardAuthenticationEntryPoint.UNAUTHORIZED),
"Unauthorized message not found");
+ assertTrue(content.contains(FAILED), "Response error message not
found");
+ }
+
+ @Test
+ void testCommenceRemoveCookie() throws ServletException, IOException {
+ final AuthenticationException exception = new
AuthenticationServiceException(FAILED);
+
+ final Cookie cookie = new
Cookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName(),
BEARER_TOKEN);
+ request.setCookies(cookie);
+ authenticationEntryPoint.commence(request, response, exception);
+
+ assertEquals(HttpServletResponse.SC_UNAUTHORIZED,
response.getStatus());
+
+ final Cookie responseCookie =
response.getCookie(ApplicationCookieName.AUTHORIZATION_BEARER.getCookieName());
+ assertNotNull(responseCookie);
+
+ final String content = response.getContentAsString();
+ assertEquals(StandardAuthenticationEntryPoint.UNAUTHORIZED, content);
+ }
+}