spring-security icon indicating copy to clipboard operation
spring-security copied to clipboard

Endpoint returns a 500, instead of 403 status code when the user does not have required permission

Open dyleph opened this issue 1 year ago • 6 comments

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

dyleph avatar Jun 14 '24 07:06 dyleph

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.

marcusdacoregio avatar Jun 14 '24 14:06 marcusdacoregio

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?

benjaminlefevre avatar Jun 21 '24 09:06 benjaminlefevre

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.

marcusdacoregio avatar Jun 24 '24 11:06 marcusdacoregio

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

databen95 avatar Jun 25 '24 08:06 databen95

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.

dilip-dhankecha avatar Jul 08 '24 05:07 dilip-dhankecha

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

dreamstar-enterprises avatar Sep 16 '24 23:09 dreamstar-enterprises

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.

jzheaux avatar Nov 21 '24 23:11 jzheaux

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

Izolius avatar Nov 27 '24 05:11 Izolius

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

72wildcard avatar Dec 02 '24 15:12 72wildcard

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.

jzheaux avatar Dec 06 '24 00:12 jzheaux