Add context propagation support via Micrometer Context Propagation for the SecurityContext, between SecurityContextHolder and ReactiveSecurityContextHolder.

This would allow applications to easily cross between the reactive <-> imperative border in either direction, and have the SecurityContext available on both sides.

Examples: * a WebFlux application could use the handle or tap operators (which propagate from Context to ThreadLocals) to call into imperative code that expects the SecurityContext to be accessible from SecurityContextHolder. * a Spring WebMVC application could use the captureContext operator (which propagates ThreadLocals to Context) to call into reactive code that expects the SecurityContext to be accessible from ReactiveSecurityContextHolder.

Comment From: jzheaux

Hi, @philsttr, thanks for the suggestion.

Reading the links you provided, I gather that this will be done automatically when an application uses handle/tap and captureContext. I think this would be valuable to add to the documentation, but I'm not yet clear on what if any support Spring Security would need to add. Can you elaborate?

Comment From: philsttr

Hi @jzheaux,

Thanks for considering this feature.

In order to support context propagation of the Spring Security Context, Spring Security would need to: 1. Provide an implementation of ThreadLocalAccessor that operates on the Spring Security Context.
2. Register this implementation with micrometer context propagation. Micrometer context propagation can discover these implementations via Java's ServiceLoader (see ContextRegistry.loadThreadLocalAccessors()), or Spring Security could provide some other mechanism for registering it.

When micrometer context propagation needs to propagate context in either direction (e.g. handle/tap or captureContext), it will invoke all of the registered ThreadLocalAccessors to do so (see DefaultContextSnapshot).

Comment From: osi

spring-graphql provides an implementation of this, https://github.com/spring-projects/spring-graphql/blob/06e485be7a1936d32b7aef470eda218b4f3c17fd/spring-graphql/src/main/java/org/springframework/graphql/execution/SecurityContextThreadLocalAccessor.java

Comment From: philsttr

@osi Spring Security stores a Mono<SecurityContext> in the subscriber context with key SecurityContext.class, but that accessor puts the SecurityContext into the subscriber context with key SecurityContext.class.getName(). So the one from spring-graphql can't just be copied into Spring Security, since it operates on a different key in the subscriber context.

Comment From: osi

ah, i could be wrong! i was looking at the type signature of what is being @. - http://fotap.org/~osiOn Sep 28, 2023, at 1:01 AM, Phil Clay @.> wrote: @osi I'm curious if that one is correct, because Spring Security stores a Mono in the subscriber context with key SecurityContext.class, but that accessor puts the SecurityContext directly into the subscriber context with key SecurityContext.class.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

Comment From: Saljack

I tried to implement it in my project but it fails on the SecurityContext is stored as Mono<SecurityContext> in reactor Context. Unfortunately, I have Mono<SecurityContext> and I cannot get the stored SecurityContext value from it. I will put it here just as an example of how to automatically propagate Mono<SecurityContext> to a ThreadLocal:

class ReactiveSecurityContextThreadLocalHelper {

  public static final ThreadLocal<Mono<SecurityContext>> SECURITY_CONTEXT = new ThreadLocal<>();

  public static void init() {
    ContextRegistry.getInstance()
        .registerThreadLocalAccessor(new SpringSecurityThreadLocalAccessor<SecurityContext>(SecurityContext.class, SECURITY_CONTEXT));
    Hooks.enableAutomaticContextPropagation();
  }

  private static class SpringSecurityThreadLocalAccessor<V> implements ThreadLocalAccessor<V> {

    private final Object key;
    private final ThreadLocal<V> threadLocal;

    public SpringSecurityThreadLocalAccessor(Object key, ThreadLocal<V> threadLocal) {
      this.key = key;
      this.threadLocal = threadLocal;
    }

    @Override
    public Object key() {
      return key;
    }

    @Override
    public V getValue() {
      return threadLocal.get();
    }

    @Override
    public void setValue(V value) {
      threadLocal.set(value);
    }

    @Override
    public void setValue() {
      threadLocal.remove();
    }

  }

}

Then just run in your main method ReactiveSecurityContextThreadLocalHelper.init() (I would like to know if there is a better place where it can be initialized because the main method is not executed in tests).

So I will skip my attempts and I hope that some expert on reactive stack and Spring Security help with this.

Maybe someone can try to extract SecurityContext from the Mono in some WebFilter or somewhere (I do not have any idea where to hook this extracting code) and store is in the reactor Context just as the plain SecurityContext (or just Principal). For example something like this:

ReactiveSecurityContextHolder.getContext() // Mono<SecurityContext>
    .flatMap(securityContext ->
                Mono.deferContextual(ctx -> {
                    return Mono.just(securityContext);
                }).contextWrite(ctx -> ctx.put("securityContextRaw", securityContext)))
 // ...

It is really pain to work with reactive stack without this because you are not able to log user information easily.

Comment From: fdlk

This is now provided in version 6.5, implemented in #16665. I have tried it out and it sort of works but it really needs better documentation.

If your reactive context contains a Mono under the key SecurityContext.class (this is how the ReactiveSecurityContextHolder.withSecurityContext method fills it), that context will be synchronized back to other reactive contexts, but invisible in synchronous code.

If your reactive context contains an actual SecurityContext under the key SecurityContext.class.getName() (I think this is how graphql fills it, unsure), that context will be synchronised with the threadlocal synchronous context in the SecurityContextHolder.

Comment From: krezovic

I have used the following WebFilter to provide non-reactive SecurityContext in WebFlux servers.

import io.micrometer.context.ContextRegistry;

import org.springframework.context.i18n.LocaleContextThreadLocalAccessor;
import org.springframework.core.Ordered;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;

import reactor.core.publisher.Mono;
import reactor.util.context.Context;

import java.util.function.Function;

@Component
public class ThreadLocalsFilter implements WebFilter, Ordered {
    static {
        var registry = ContextRegistry.getInstance();

        registry.registerThreadLocalAccessor(new LocaleContextThreadLocalAccessor());
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return ReactiveSecurityContextHolder.getContext()
                .defaultIfEmpty(SecurityContextHolder.createEmptyContext())
                .flatMap(ctx -> chain.filter(exchange).contextWrite(writeCtx(exchange, ctx)));
    }

    private Function<Context, Context> writeCtx(ServerWebExchange exchange, SecurityContext ctx) {
        return context -> {
            context =
                    context.put(LocaleContextThreadLocalAccessor.KEY, exchange.getLocaleContext());

            if (ctx.getAuthentication() != null) {
                return context.put(SecurityContext.class.getName(), ctx);
            }

            return context.delete(SecurityContext.class.getName());
        };
    }
}