NIFI-10259 Improved HTTP error handling for authentication failures

- Added Standard AuthenticationEntryPoint
- Configured AuthenticationEntryPoint for SecurityFilterChain and BearerTokenAuthenticationFilter

Signed-off-by: Nathan Gough <thenatog@gmail.com>

This closes #6233.
This commit is contained in:
exceptionfactory 2022-07-21 17:06:41 -05:00 committed by Nathan Gough
parent d7ed66032e
commit a661b035e8
4 changed files with 222 additions and 3 deletions

View File

@ -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)

View File

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

View File

@ -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());

View File

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