Endpoint returns a 500, instead of 403 status code when the user does not have required permission
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
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.
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?
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.
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");
}
}
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.
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
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.
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
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();
}
}
}
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.