mirror of https://github.com/apache/nifi.git
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:
parent
d7ed66032e
commit
a661b035e8
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue