Search code examples
spring-bootkotlinspring-mvcspring-securitykotlin-coroutines

403 Forbidden for suspend controller methods with basic authentication after migration to Spring Boot 3


I recently migrated my Spring Boot MVC Kotlin project to Spring Boot 3. A basic auth is used for a REST controller (configured via SecurityFilterChain - also migrated from WebSecurityConfigurerAdapter based security). Since then, all calls to endpoints that are handled by suspend controller methods return 403. For the other endpoints it works fine.

All endpoints worked fine before the migration. When we make a controller method not suspend (using runBlocking), it works fine.

Interesting is, that the body of the controller method is executed. I noticed, that the request is passed through the security filter chain more than once, and it is in the default AnonymousAuthenticationFilter where it gets marked as unauthorized. The request param there does not contain the required auth headers, even if it does during the first pass through.

Some of the default HttpSecurity logic is disabled:

http
    .csrf().disable()
    .headers().disable()
    .sessionManagement().disable()
    .securityContext().disable()
    .requestCache().disable()
    .servletApi().disable()
    .apply(DefaultLoginPageConfigurer()).disable()
    .logout().disable()

It seems that when coroutines are involved, the authorization logic is triggered twice. Once it works correctly. For the second time it fails because of missing auth headers.

I tried enabling the disabled defaults, but that did not help.


Solution

  • So after some more debugging and consultation with some Spring experts, I found out what was it about. It was a combination of three factors:

    1. In the new Spring version, not only the initial request processing (dispatcher type "REQUEST") goes through the security chain, but in case of async calls like those I have with Kotlin suspend handler methods, even the asynchronous processing started in a new thread have to go through it (dispatcher type "ASYNC"). That means the authorization is done twice. 2.BasicAuthenticationFilter is a OncePerRequestFilter that runs only in the first filtering round - security context is not updated this time.
    2. As we have storing of security context in between requests disabled http.securityContext().disable(), it is missing in the second filtering round and so the request comes out as unauthorized.

    There are several possible solutions to this. Either disabling of the security chain processing for requests with the "ASYNC" dispatcher type, enabling to remember security context in between requests, or some other.