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);
+    }
+}

Reply via email to