Describe the bug When upgrading Spring boot from 3.2.5 to Spring boot 3.3.0, which contains a new version of Spring security 6.3, I got some failing test cases that should return a 403 FORBIDDEN status code instead returns a 500 INTERNAL SERVER ERROR.
Spring Boot: 3.3.0 Spring Security: 6.3.0
To Reproduce
Here is a controller class
@GetMapping(value = "/books")
@PreAuthorize("hasAuthority('developer')")
public ResponseEntity<List<BookProjection>> getAllByBookCategory(@RequestParam BookCategory bookCategory) {
return ResponseEntity.ok(bookQueryService.getAllByBookCategory(bookCategory));
}
and SecurityConfiguration.class
@Configuration
@ConditionalOnWebApplication
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfiguration {
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/v3/api-docs", "/v3/api-docs.yaml")
.requestMatchers(HttpMethod.GET, "/api/v1/developers")
.requestMatchers(HttpMethod.GET, "/ready")
.requestMatchers("/error");
}
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.addFilterAfter(new AuthTokenCompleteFilter(), BearerTokenAuthenticationFilter.class)
.authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(EndpointRequest.to("info", "health")).permitAll()
.anyRequest().authenticated())
.cors(withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.oauth2ResourceServer(oauth2ResourceServer -> oauth2ResourceServer
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())));
return http.build();
}
}
and here is an Integration test class:
@ParameterizedTest
@CsvSource({"200,developer", "403,no_developer_permission", AuthAssertion.UNAUTHORIZED})
void getAllByBookCategory_whenValidRequest_shouldReturnOk(@AggregateWith(AuthAssertionParameterResolver.class) AuthAssertion authAssertion) throws JSONException {
given(bookQueryService.getAllByBookCategory(BookCategory.NOVEL))
.willReturn(BookOpsControllerContractDataFixture.standardGetAllByBookCategoryServiceReturnValue());
final ResponseEntity<String> responseEntity = testRestTemplate.exchange("/api/v1/books?bookCategory={bookCategory}",
HttpMethod.GET,
new HttpEntity<>(prepareHeaders(authAssertion)),
String.class,
BookCategory.NOVEL);
logger.debug("responseEntity status code: " + responseEntity.getStatusCode());
logger.debug("authAssertion status code: " + authAssertion.expectedStatus());
logger.debug("responseEntity body: " + responseEntity.getBody());
assertThat(responseEntity.getStatusCode()).isEqualTo(authAssertion.expectedStatus());
if (authAssertion.expectedStatus().is2xxSuccessful()) {
JSONAssert.assertEquals(MakeOpsControllerContractDataFixture.standardGetAllByBookCategoryShouldReturnOkResponse(),
responseEntity.getBody(),
JSONCompareMode.NON_EXTENSIBLE);
}
}
The test is failing when trying to assert status code when the user does not have the required permissions (403)
Actual behavior
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : responseEntity status code: 500 INTERNAL_SERVER_ERROR
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : authAssertion status code: 403 FORBIDDEN
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : responseEntity body: {"code":"AUTHORIZATION_DENIED","message":"Access Denied"}
Expected :403 FORBIDDEN
Actual :500 INTERNAL_SERVER_ERROR
<Click to see difference>
024-06-14T14:02:01.137+07:00 ERROR 63510 --- [sample-service] [omcat-handler-1] i.g.w.e.LoggingService : Access Denied
org.springframework.security.authorization.AuthorizationDeniedException: Access Denied
at org.springframework.security.authorization.method.ThrowingMethodAuthorizationDeniedHandler.handleDeniedInvocation(ThrowingMethodAuthorizationDeniedHandler.java:38) ~[spring-security-core-6.3.0.jar:6.3.0]
at org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager.handleDeniedInvocation(PreAuthorizeAuthorizationManager.java:92) ~[spring-security-core-6.3.0.jar:6.3.0]
at org.springframework.security.config.annotation.method.configuration.DeferringObservationAuthorizationManager.handleDeniedInvocation(DeferringObservationAuthorizationManager.java:63) ~[spring-security-config-6.3.0.jar:6.3.0]
at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.handle(AuthorizationManagerBeforeMethodInterceptor.java:288) ~[spring-security-core-6.3.0.jar:6.3.0]
at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.attemptAuthorization(AuthorizationManagerBeforeMethodInterceptor.java:261) ~[spring-security-core-6.3.0.jar:6.3.0]
at org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor.invoke(AuthorizationManagerBeforeMethodInterceptor.java:197) ~[spring-security-core-6.3.0.jar:6.3.0]
at org.springframework.security.config.annotation.method.configuration.PrePostMethodSecurityConfiguration$DeferringMethodInterceptor.invoke(PrePostMethodSecurityConfiguration.java:200) ~[spring-security-config-6.3.0.jar:6.3.0]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184) ~[spring-aop-6.1.8.jar:6.1.8]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:768) ~[spring-aop-6.1.8.jar:6.1.8]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:720) ~[spring-aop-6.1.8.jar:6.1.8]
at ch.autoscout24.shared.infra.web.rest.MakeOpsController$$SpringCGLIB$$0.getAllByBookCategory(<generated>) ~[main/:na]
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:255) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:926) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:831) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.1.8.jar:6.1.8]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:527) ~[jakarta.servlet-api-6.0.0.jar:6.0.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.1.8.jar:6.1.8]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:614) ~[jakarta.servlet-api-6.0.0.jar:6.0.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:108) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.FilterChainProxy.lambda$doFilterInternal$3(FilterChainProxy.java:231) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$FilterObservation$SimpleFilterObservation.lambda$wrap$1(ObservationFilterChainDecorator.java:479) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$1(ObservationFilterChainDecorator.java:340) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator.lambda$wrapSecured$0(ObservationFilterChainDecorator.java:82) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:128) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:100) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at ch.autoscout24.commons.security.AuthTokenCompleteFilter.doFilterInternal(AuthTokenCompleteFilter.java:37) ~[main/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter.doFilterInternal(BearerTokenAuthenticationFilter.java:145) ~[spring-security-oauth2-resource-server-6.3.0.jar:6.3.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$AroundFilterObservation$SimpleAroundFilterObservation.lambda$wrap$0(ObservationFilterChainDecorator.java:323) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:224) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.ObservationFilterChainDecorator$VirtualFilterChain.doFilter(ObservationFilterChainDecorator.java:137) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.3.0.jar:6.3.0]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195) ~[spring-webmvc-6.1.8.jar:6.1.8]
at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230) ~[spring-security-config-6.3.0.jar:6.3.0]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268) ~[spring-web-6.1.8.jar:6.1.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:109) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.1.8.jar:6.1.8]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.1.8.jar:6.1.8]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:389) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.24.jar:10.1.24]
at java.base/java.lang.VirtualThread.run(VirtualThread.java:309) ~[na:na]
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : responseEntity status code: 500 INTERNAL_SERVER_ERROR
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : authAssertion status code: 403 FORBIDDEN
2024-06-14T14:02:01.174+07:00 INFO 63510 --- [sample-service] [ Test worker] s.i.w.r.MakeOpsControllerIntegrationTest : responseEntity body: {"code":"AUTHORIZATION_DENIED","message":"Access Denied"}
Expected behavior The endpoint should return a 403 FORBIDDEN status code when the user does not have the required permissions.
Regarding there is a related change in new Sping Security 6.3 (https://github.com/spring-projects/spring-security/pull/14712/files), I guess there is something that we could handle.
Best Regards, Dy
Comment From: marcusdacoregio
Hi @dyleph, thanks for the report. Are you able to provide a minimal, reproducible sample that we can run on our side? Looks like your application has some customizations that will make it harder for us to reproduce the same behavior.
Comment From: benjaminlefevre
Hi, same problem for us
Do you know when it will be fixed and if it will be included in Spring Boot 3.3.1?
Comment From: marcusdacoregio
Hi folks, based on my tests I got a proper 403 returned. It would be great if you could provide a minimal sample that we can use to reproduce the problem on our side.
Comment From: databen95
Hello I have the same problem when using a Custom ApplicationExceptionHandler extending ResponseEntityExceptionHandler. It seams that AccessDeniedException are automatically catched when using this type of handler but not AuthorizationDeniedException. It is possible to add a custom exception handler for AuthorizationDeniedException to bypass this problem but it should not be necessary.
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
class ApplicationExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(AuthorizationDeniedException.class)
ResponseEntity<String> handleAuthorizationDeniedException(AuthorizationDeniedException exception) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access denied");
}
}
Comment From: dilipdhankecha2530
Hey Folks,
We're using Spring Boot version 3.3.1 and managing authorization with the @PreAuthorize annotation. Here's our security filter chain bean definition, where we permit all on the /error path. Despite this, exceptions thrown by Spring Security aren't handled correctly.
@Bean
@ConditionalOnProperty(value = "security.enabled", havingValue = "true", matchIfMissing = true)
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authRequest -> authRequest
.requestMatchers("/", "/docs/api-docs/**", "/ui/swagger-ui/**", "/actuator/health/**").permitAll()
.requestMatchers("/error/**").permitAll()
.requestMatchers("/api/**").authenticated()
)
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(jwt -> jwt.jwtAuthenticationConverter(new CustomAuthenticationConverter(configProperties.clientId()))))
.build();
}
When Spring Security returns a 403 status, we receive a 500 status code instead. We can handle this by catching the exception in our codebase, but I think Spring Security should handle it automatically.
Comment From: dreamstar-enterprises
I get exactly the same result, where it throws a 500 error, for some reason...
https://stackoverflow.com/questions/78992112/spring-security-access-denied-from-preauthorize-keeps-returning-500-full-cod
Comment From: jzheaux
Thanks for the reports. Unfortunately, our samples all appear to be returning 403s as expected. This doesn't mean that there isn't a bug, but it does mean that a minimal sample would be helpful.
@dyleph, I can't run the code you provided as it refers to methods like prepareHeaders and classes like AuthAssertionParameterResolver and AuthAssertion that I don't have. If you aren't able to create a sample from scratch, perhaps you can alter one of the Spring Security Samples in a branch and post that here.
I'm not yet seeing a common enough thread in the reports to suppose that they are all the same problem. If someone can provide a minimal GitHub sample, then I'll be happy to dig into the issue.
Comment From: Izolius
I faced the same issue. After spending some time on debugging and trying to reproduce it in Samples project, I found out that the reason is response object is already committed and it makes org.springframework.security.web.access.ExceptionTranslationFilter#doFilter(jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse, jakarta.servlet.FilterChain) method executing
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
instead of calling
handleSpringSecurityException(request, response, chain, securityException);
I'm not really sure why response is commited already in my case, probably it's related to using Jersey for processing requests
Comment From: 72wildcard
Hi guys
I observed that using spring-boot-starter-web 3.2.6/ spring-security 6.2.4, failing the condition of @PreAuthorize(...) causes an an exception of type AccessDeniedException.class to be thrown.
After upgrading to spring-boot-starter-web 3.3.0/ spring-security 6.3.0, failing the condition of @PreAuthorize(...) causes an an exception of type AuthorizationDeniedException.class to be thrown.
I suspect this ticket was opened because of this change and some exception handler configurations like such
@ExceptionHandler
public ResponseEntity<GenericUiErrorResponse<ErrorType>> handleAccessDeniedException(AccessDeniedException ex) {
...
}
I found no mentioning in any release notes or elsewhere searching for this change. Could anyone verify that the behaviour of @PreAuthorize was altered?
I need to verify on our side that this behaviour change is not breaking, as my application has an exception handler for AccessDeniedExceptions as well.
Here is the code of a test class to verify
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {
ExampleTest.TestConfiguration.class,
ExampleTest.AccessServiceWrapper.class
})
class ExampleTest {
@Autowired
ProtectedService protectedService;
@MockBean
AccessService accessService;
@BeforeEach
void beforeEach() {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication auth = mock(Authentication.class);
when(auth.isAuthenticated()).thenReturn(true);
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
}
@Test
void hasAccessToAccountById() {
// arrange
when(this.accessService.hasAccess("1")).thenReturn(true);
// act + assert
assertThatCode(() -> this.protectedService.someProtectedMethod("1")).doesNotThrowAnyException();
assertThatCode(() -> this.protectedService.someProtectedMethod("2")).isExactlyInstanceOf(AccessDeniedException.class);
}
static class ProtectedService {
@SuppressWarnings({"EmptyMethod"}) // justification: dummy endpoint used for testing
@PreAuthorize("@accessServiceWrapper.hasAccess(#id)")
public void someProtectedMethod(String id) {
}
}
@Service
static class AccessService {
boolean hasAccess(String id) {
return false;
}
}
static class AccessServiceWrapper {
private final AccessService accessService;
public AccessServiceWrapper(AccessService accessService) {
this.accessService = accessService;
}
public boolean hasAccess(String id) {
return this.accessService.hasAccess(id);
}
}
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
static class TestConfiguration {
@Bean
public AccessServiceWrapper accessServiceWrapper(AccessService accessService) {
return new AccessServiceWrapper(accessService);
}
@Bean
public ProtectedService protectedService() {
return new ProtectedService();
}
}
}
Comment From: jzheaux
AuthorizationDeniedException is a subclass of AccessDeniedException, so AccessDeniedException exception handlers should still catch them. If that isn't happening, I'd need a reproducer to dig in further.
I'm going to close this issue for now as I am not seeing a common thread in the reports. I will still check this ticket on occasion for a sample application that can demonstrate the issue.