Describe the bug After upgrading an app to Spring Boot 3, a working filter chain that works in the latest Spring Boot 2 version fails with Cannot create a session after the response has been committed

To Reproduce Create a filter chain with a securityContextConfigurer.requireExplicitSave(true). Attach a filter in this chain that does the saveContext job.

Expected behavior Since we now need to explicitly save the Security Context, we have successfully refactored our Spring Security configuration for months.

We invoke the saveContext in a filter that we put in our main filterChain, and it does the job. (See https://github.com/mpalourdio/springsecrepro/blob/main/src/main/java/com/example/demo/config/SecurityContextExplicitSaveFilter.java)

But this now fails from Spring 6.x with Cannot create a session after the response has been committed. What is weird, it's that if we force the sessionManagement with SessionCreationPolicy.ALWAYS or SessionCreationPolicy.IF_REQUIRED, it works.

Expected : The sessionManagement should remained untouched, like in Spring Sec 5.7+

Sample

https://github.com/mpalourdio/springsecrepro , README added

Comment From: marcusdacoregio

Hi, @mpalourdio. This is probably happening because there is no session related to that request, therefore when your filter finally block is invoked, the HttpSessionSecurityContextRepository tries to create a session to save the SecurityContext, but the response has already been committed (written).

You can disallow the HttpSessionSecurityContextRepository of creating HTTP sessions to avoid seeing that error, like:

private final SecurityContextRepository repository = getHttpSessionSecurityContextRepository();

private static HttpSessionSecurityContextRepository getHttpSessionSecurityContextRepository() {
    var repository = new HttpSessionSecurityContextRepository();
    repository.setAllowSessionCreation(false);
    return repository;
}

I'll close this since it is related to a misconfiguration. If you still believe there is a bug in Spring Security, please reply with the reasons and we can reopen this.

Comment From: mpalourdio

@marcusdacoregio Thanks for feedback. How to you explain that this works flawlessly in Spring Boot 2 ? Have you been able to reproduce this behavior with the sample I have provided ?

Also in this sample, switching to SB 2 makes the things work back. I can hear there is a misconfiguration, but could you point me to what has changed between Spring Security 5.x and 6x that causes this side effect ?

Thanks again !

Comment From: marcusdacoregio

Sorry for not giving enough details @mpalourdio.

The SecurityContextPersistenceFilter uses HttpSessionSecurityContextRepository#loadContext(HttpRequestResponseHolder), which wraps the response with a SaveToSessionResponseWrapper, that wrapper has the responsibility of saving the context just before the response is committed (see SaveContextOnUpdateOrErrorResponseWrapper), and the HTTP session repository forces the session creation.

The new SecurityContextRepository#loadDeferredContext used by SecurityContextHolderFilter does not do that, therefore, there is no session creation before the response is committed.

Comment From: mpalourdio

@marcusdacoregio Many thanks, very much clearer for me now ! Keep up the good work, you all do an AWESOME job for years! Much appreciated.

Comment From: mpalourdio

@marcusdacoregio After many tries, your solution does not seem to be satisfying because we need to persist this context in the session, so it cannot be null.

I think that the cause was that the context was saved before the chain.doFilter. By watching the code of other built-in filters, like AuthenticationFilter in Spring Security, I have come to this

var context = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
SecurityContextHolder.setContext(context);
repository.saveContext(context, req, res);
logger.debug("saved context to complete filter");
chain.doFilter(req, res); // filter AFTER the save

vs the code that errors (notice here how the doFilter happens before the saveContext

try {
            chain.doFilter(req, res);
        } finally {
            var context = SecurityContextHolder.getContext();
            SecurityContextHolder.clearContext();
            repository.saveContext(context, req, res);
            logger.debug("saved context to complete filter");
        }

What do you think ?

Comment From: marcusdacoregio

I recommend saving the context in the authentication filters instead of having a custom filter to do that for you. But, in that case, you have to save it before the response is committed, therefore you have to do it before the filter chain returns most of the time. The order of the filter matters too, if you place it in the wrong place it can lead to unexpected behaviors.

Comment From: mpalourdio

@marcusdacoregio Thanks for reaching me. Yep, we place this filter in the filter chain just after (ie, addFilterAfter) a custom authentication filter (that is maintained by a third party lib, so hard to do without breaking things all over the place), and before SecurityContextHolderFilter.

So you agree that in this specific case, the error comes from the fact that the doFilter is done prior to saveContext ? I mean, that was wrong from the beginning ?