Describe the bug When disabling session management (or using SessionCreationPolicy.STATELESS I believe) the SecurityContext is not persisted between the REQUEST and ASYNC dispatch. This is with Spring Security v5.4.2.

To Reproduce Configure HttpSecurity -

http.sessionManagement().disable();

We are using this stateless configuration for basic/token authentication without sessions.

We have a HandlerInterceptorAdapter that checks the Authentication that looks like this -

@Component
public class RequireAuthenticationInterceptor extends HandlerInterceptorAdapter {

    final AuthenticationTrustResolver trustResolver;

    @Autowired
    public RequireAuthenticationInterceptor(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            if (!method.hasMethodAnnotation(AnonymousAccess.class)) {
                Authentication auth = SecurityContextHolder.getContext().getAuthentication();
                if (auth == null || trustResolver.isAnonymous(auth)) {
                    throw new AccessDeniedException("Anonymous access is not allowed");
                }
            }
        }
        return true;
    }
}

This interceptor works fine when the dispatch type is REQUEST and the user is authenticated, but for subsequent ASYNC dispatches the Authentication is anonymous and the AccessDeniedException is thrown.

I have looked into why this is happening and seen that when using sessions there is a SecurityContextRepository which saves and loads the SecurityContext. I have implemented the following SecurityContextRepository which seems to work, however I am not sure if this is recommended or whether I should just ignore ASYNC dispatches in my HandlerInterceptorAdapter.

class StatelessSecurityContextRepository implements SecurityContextRepository {
    private final String SECURITY_CONTEXT_ATTRIBUTE = "MANGO_SECURITY_CONTEXT";

    @Override
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        HttpServletRequest request = requestResponseHolder.getRequest();
        if (request.getDispatcherType() == DispatcherType.ASYNC) {
            Object securityContext = request.getAttribute(SECURITY_CONTEXT_ATTRIBUTE);
            if (securityContext instanceof SecurityContext) {
                return (SecurityContext) securityContext;
            }
        }
        return SecurityContextHolder.createEmptyContext();
    }

    @Override
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        if (request.getDispatcherType() != DispatcherType.ASYNC) {
            request.setAttribute(SECURITY_CONTEXT_ATTRIBUTE, context);
        }
    }

    @Override
    public boolean containsContext(HttpServletRequest request) {
        return request.getAttribute(SECURITY_CONTEXT_ATTRIBUTE) instanceof SecurityContext;
    }
}

If this is the correct solution, perhaps the above class or a variation of it can be included in Spring Security.

Expected behavior We should be able to get the correct SecurityContext/Authentication during the ASYNC dispatch.

Sample Can provide if needed.

Comment From: ghartz-hz

Any updates on this? We've also run into this problem, and it took quite a while to track down the issue. We didn't have an issue with this until we did an upgrade that pulled in Spring Security 5. Its certainly not ideal, and inefficient, to require a session as our servers do not have sticky routing and are stateless, beyond this singular issue.

In our particular case, this causes the session context to be cleared after the request has been processed by Spring MVC, but before Jackson serializes the output, which breaks code that is filtering the JSON output based on user authorities.

Comment From: jzheaux

Hi, @ghartz-hz, sorry for the delay in responding.

Have you already tried exposing DelegatingSecurityContextAsyncTaskExecutor as a @Bean? This will wrap the existing task executor in one that will propagate the SecurityContext onto the next thread.

@Bean
public DelegatingSecurityContextAsyncTaskExecutor taskExecutor(ThreadPoolTaskExecutor delegate) {
     return new DelegatingSecurityContextAsyncTaskExecutor(delegate);
}

If that does not address your issue, then please do send a sample, and I can take a closer look.

Comment From: ghartz-hz

Yeah, that doesn't make any difference -- we've tried that. I need to find some cycles to create a minimal example, but the place where we see the problem come up is specifically related to Spring MVCs integration of Jackson. With the session disabled (even with a DelegatingSecurityContextAsyncTaskExecutor), when the return values from the MVC controller methods eventually get back up to Jackson to serialize in the response, the authentication has been reset. It wouldn't be a visible problem if you're not looking for the current authentication during serialization.

We use a Jackson PropertyFilter implementation that basically runs SpEL-based security expression against the JSON properties, essentially allowing a property to be dropped based on Spring Security definitions on the bean or projection. That implementation needs to get the current authentication via the SecurityContextHolder and provide it to the SecurityExpressionRoot, and at that point the authentication is no longer the same as during the processing of the request.

When I stepped through Spring Security to figure out why the session being disabled was breaking it, it was the same root cause as this issue -- the authentication is being saved in a construct associated with the session and not the request, and something is happening between the "user level" code and the serialization that is causing it to have to go back to the repository again, and that fails. (Or something along those lines -- I don't remember the precise execution path I found, but it was basically that.)

Looking at the implementation of DelegatingSecurityContextRunnable, my best guess is that it doesn't help because Spring MVC is running the response converter outside of the async context (so its after the finally() has run and reset the context back), and the session repository stuff is "fixing" the issue by essentially restoring it in the context that the serialization is happening in. If so I could see an argument made that any part of the process is not a bug, per se, but the interactions of the different parts is creating one. But that's just a hunch from skimming the code.

Comment From: mvitz

We see the same issue when using a @ControllerAdvice and letting us inject the Authentication-Object as parameter.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: jazdw

@jzheaux Why is this being closed? What more information do you need?

There are pretty clear explanations and code samples in the original message. There's also a few design questions in my original message that have not been answered by anyone from the Spring project.

Comment From: jazdw

This seems to have been addressed by the addition of org.springframework.security.web.context.RequestAttributeSecurityContextRepository in Spring Security 5.7.

With http.sessionManagement().disable() I can use http.securityContext().securityContextRepository(new RequestAttributeSecurityContextRepository()) and everything works as expected.

The Preparing for 6.0 notes also indicate that the use of this RequestAttributeSecurityContextRepository will become the default in Spring Security 6.0