From 0f62aa09f64a9cf69e46171542808e3d4c2a7a09 Mon Sep 17 00:00:00 2001 From: Francisco Bento Date: Fri, 1 Oct 2021 03:19:24 -0300 Subject: [PATCH] Introduce support for AuthenticationFailureHandler (#682) To be used in conjunction with AbstractAuthenticationProcessingFilter for rendering problem+json --- .../security/SecurityProblemSupport.java | 15 ++++++-- .../security/SecurityAdviceTraitTest.java | 37 ++++++++++++++++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/problem-spring-web/src/main/java/org/zalando/problem/spring/web/advice/security/SecurityProblemSupport.java b/problem-spring-web/src/main/java/org/zalando/problem/spring/web/advice/security/SecurityProblemSupport.java index 62dd9e84..4ade7145 100644 --- a/problem-spring-web/src/main/java/org/zalando/problem/spring/web/advice/security/SecurityProblemSupport.java +++ b/problem-spring-web/src/main/java/org/zalando/problem/spring/web/advice/security/SecurityProblemSupport.java @@ -7,25 +7,28 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; /** - * A compound {@link AuthenticationEntryPoint} and {@link AccessDeniedHandler} that delegates exceptions to - * Spring WebMVC's {@link HandlerExceptionResolver} as defined in {@link WebMvcConfigurationSupport}. + * A compound {@link AuthenticationEntryPoint}, {@link AuthenticationFailureHandler} and {@link AccessDeniedHandler} + * that delegates exceptions to Spring WebMVC's {@link HandlerExceptionResolver} as defined in {@link WebMvcConfigurationSupport}. * * Compatible with spring-webmvc 4.3.3. */ @API(status = STABLE) @Component -public class SecurityProblemSupport implements AuthenticationEntryPoint, AccessDeniedHandler { +public class SecurityProblemSupport implements AuthenticationEntryPoint, AuthenticationFailureHandler, AccessDeniedHandler { private final HandlerExceptionResolver resolver; @@ -42,6 +45,12 @@ public void commence(final HttpServletRequest request, final HttpServletResponse resolver.resolveException(request, response, null, exception); } + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, + AuthenticationException exception) throws IOException, ServletException { + resolver.resolveException(request, response, null, exception); + } + @Override public void handle(final HttpServletRequest request, final HttpServletResponse response, final AccessDeniedException exception) { diff --git a/problem-spring-web/src/test/java/org/zalando/problem/spring/web/advice/security/SecurityAdviceTraitTest.java b/problem-spring-web/src/test/java/org/zalando/problem/spring/web/advice/security/SecurityAdviceTraitTest.java index cc52a6b2..911f2d64 100644 --- a/problem-spring-web/src/test/java/org/zalando/problem/spring/web/advice/security/SecurityAdviceTraitTest.java +++ b/problem-spring-web/src/test/java/org/zalando/problem/spring/web/advice/security/SecurityAdviceTraitTest.java @@ -12,9 +12,15 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.web.WebAppConfiguration; @@ -31,6 +37,10 @@ import org.zalando.problem.spring.common.MediaTypes; import org.zalando.problem.spring.web.advice.ProblemHandling; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.util.List; import java.util.Locale; @@ -104,9 +114,25 @@ public void configure(final HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport); + http.addFilterBefore(new AuthenticationFilter(problemSupport), LogoutFilter.class); } } + + public static class AuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + protected AuthenticationFilter(SecurityProblemSupport problemSupport) { + super(new AntPathRequestMatcher("/authFilter")); + setAuthenticationFailureHandler(problemSupport); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException, IOException, ServletException { + throw new BadCredentialsException("invalid pass"); + } + + } @ControllerAdvice public static class ExceptionHandling implements ProblemHandling, SecurityAdviceTrait { @@ -137,7 +163,7 @@ void notAuthenticated() throws Exception { } @Test - void notAuthorized() throws Exception { + void notAuthorizedByRole() throws Exception { mvc.perform(get("/greet").param("name", "Alice").with(user("user").roles("USER"))) .andExpect(status().isForbidden()) .andExpect(content().contentType(MediaTypes.PROBLEM)) @@ -146,4 +172,13 @@ void notAuthorized() throws Exception { .andExpect(jsonPath("$.detail", is("Access is denied"))); } + @Test + void notAuthorizedByFilter() throws Exception { + mvc.perform(get("/authFilter").param("name", "Alice").with(user("user").roles("ADMIN"))) + .andExpect(status().isUnauthorized()) + .andExpect(content().contentType(MediaTypes.PROBLEM)) + .andExpect(jsonPath("$.title", is("Unauthorized"))) + .andExpect(jsonPath("$.status", is(401))) + .andExpect(jsonPath("$.detail", is("invalid pass"))); + } }