javaspringspring-bootspring-securityvirtual-threads

Spring Security Virtual Threads and ThreadLocal


As I was reading up about virtual threads and their pitfalls I found this mention :

Don't Cache Expensive Reusable Objects in Thread-Local Variables

Virtual threads support thread-local variables just as platform threads do. See Thread-Local Variables for more information. Usually, thread-local variables are used to associate some context-specific information with the currently running code, such as the current transaction and user ID. This use of thread-local variables is perfectly reasonable with virtual threads. However, consider using the safer and more efficient scoped values. See Scoped Values for more information.

Here : https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-68216B85-7B43-423E-91BA-11489B1ACA61

But i also remembered that Spring Security uses ThreadLocal to save the SecurityContext of a given request:

By default, SecurityContextHolder uses a ThreadLocal to store these details, which means that the SecurityContext is always available to methods in the same thread, even if the SecurityContext is not explicitly passed around as an argument to those methods. Using a ThreadLocal in this way is quite safe if you take care to clear the thread after the present principal’s request is processed. Spring Security’s FilterChainProxy ensures that the SecurityContext is always cleared.

Docs : https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

So the question is : is it safe to use virtual threads in a Spring Boot REST Application with endpoints that do require authentication and authorization and therefor have a SecurityContext ? Is this considered a pitfall ?

Thanks !


Solution

  • While it is possible 1) to implement a custom SecurityContextHolderStrategy which retrieves SecurityContext from a ScopedValue and saves it there:

    public class ScopedSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
        private static final ScopedValue<SecurityContextScopedValueHolder> SECURITY_CONTEXT = ScopedValue.newInstance();
    
        private static class SecurityContextScopedValueHolder {
            
            private SecurityContext securityContext;
    
            public SecurityContext getSecurityContext() {
                return securityContext;
            }
    
            public void setSecurityContext(SecurityContext securityContext) {
                this.securityContext = securityContext;
            }
    
        }
        
        @Override
        public void clearContext() {
            retrieveSecurityContextScopedValueHolder().setSecurityContext(null);
        }
    
        @Override
        public SecurityContext getContext() {
            return retrieveSecurityContextScopedValueHolder().getSecurityContext();
        }
    
        @Override
        public void setContext(SecurityContext context) {
            retrieveSecurityContextScopedValueHolder().setSecurityContext(context);
        }
    
        @Override
        public SecurityContext createEmptyContext() {
            return new SecurityContextImpl();
        }
        
        private SecurityContextScopedValueHolder retrieveSecurityContextScopedValueHolder() {
            if (SECURITY_CONTEXT.isBound()) {
                return SECURITY_CONTEXT.get();
            } else {
                throw new IllegalStateException("Security Context Scoped Value not bound");
            }
        }
        
        public static ScopedValue.Carrier getSecuriyContextCarrier() {
            return ScopedValue.where(SECURITY_CONTEXT, new SecurityContextScopedValueHolder());
        }
    
    }  
    

    and 2) configure Tomcat to start a virtual thread with the ScopedValue, bound to it:

    @Component
    public class TomcatVirtualThreadExecutorCustomizer 
            implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    
        private static class ScopedVirtualThreadExecutor extends VirtualThreadExecutor {
    
            public ScopedVirtualThreadExecutor(String namePrefix) {
                super(namePrefix);
            }
    
            @Override
            public void execute(Runnable command) {
                super.execute(() -> ScopedSecurityContextHolderStrategy.getSecuriyContextCarrier().run(command));
            }
    
        }
    
        @Override
        public void customize(TomcatServletWebServerFactory factory) {
            factory.addProtocolHandlerCustomizers((protocolHandler) -> protocolHandler
                    .setExecutor(new ScopedVirtualThreadExecutor("tomcat-handler-")));
        }
    
    }
    

    it is easy to see, however, a substantial awkwardness in such approach.

    First, the approach is tightly bound to type of web server/servlet container, Tomcat in our case. The solution for other servers, like Undertow or Jetty, might be different if at all possible.

    Second, Spring Security is a ubiquitous thing that SecurityContext is meant to be used everywhere, not only on server's worker threads. For example, there might be a need to setup a SecurityContext on a cron/scheduler thread or just on a thread, managed by a standalone Executor. ScopeValue-based approach will require similar binding of it to such thread, while with a standard ThreadLocal-bound SecurityContextHolderStrategy the context can be set without any thread tweaking.

    All in all, this technique introduces some not-very-welcome coupling between the code which creates a thread and the code which sets/retrieves SecurityContext.

    From conceptual standpoint, I'd daresay that the concepts of Structured Programming and Spring Security don't get along with each other very well - at least in current versions of both.

    The small POC Spring Boot project is available here.

    Note that the example works for Spring Boot 3.2.2, its applicability to earlier and later versions is not guaranteed as things with Loom are rather volatile at the moment.


    Yet another approach is a replacement of Spring Security's stock SecurityContextHolderFilter with a custom one which uses ScopedSecurityContextHolderStrategy, discussed above:

    public class ScopedSecurityContextHolderFilter extends SecurityContextHolderFilter {
        
        ...
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
        }
    
        private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            if (request.getAttribute(FILTER_APPLIED) != null) {
                chain.doFilter(request, response);
                return;
            }
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            DeferredSecurityContext deferredContext = securityContextRepository.loadDeferredContext(request);
            try {
                ScopedSecurityContextHolderStrategy.runWhere(deferredContext, () -> {
                    securityContextHolderStrategy.setDeferredContext(deferredContext);
                    try {
                        chain.doFilter(request, response);
                    } catch (IOException | ServletException e) {
                        throw new RuntimeException(e);
                    }
                });
            } catch (RuntimeException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof ServletException)
                    throw (ServletException)cause;
                if (cause instanceof IOException)
                    throw (IOException)cause;
                throw e;
            } finally {
                request.removeAttribute(FILTER_APPLIED);
            }
        }   
        
        ... 
        
    }
    

    In this scenario, a ScopedValue is bound to a thread not at the point of its initiation in Web Server/Servlet Container (Tomcat), but at arbitrary point up-stack of such initiation. This allows to avoid the dependency of Web Server/Servlet Container (Tomcat), but brings another issues.

    First, String Security Filter Chain implementation, FilterChainProxy, invokes Security Context clearing, SecurityContextHolderStrategy.clearContext() method, at the end of Security Filter Chain executing, thus employing a free, unrestricted ThreadLocal design. Evidently, more restrictive ScopedValue design comes into conflict with the Spring Security design and Security Filters, that execute after ScopedValue gets unbound from the thread, appear top be SecurityContext-less, and whether this might be an issue is difficult to say in general.

    Second issue is associated with the replacement of SecurityContextHolderFilter itself. Spring Security Filters configuration uses the correspondent SecurityContextConfigurer instance directly, as a source of SecurityContextRepository. Therefore, certain tricks are necessary to implement this replacement. One of such solutions is brought and discussed in the POC example, mentioned above, it is probably as "hacky" as its equivalents.

    Finally, the implementation of ScopedSecurityContextHolderFilter is bound to be a shameless copy-paste job from SecurityContextHolderFilter, which also compromises upgradability and maintainability of the solution.

    All in all, the approach of custom Spring Security Filter turns out to be even more questionable than the one that involves Tomcat Executor service customization.