skip to content

Reactive Security Context Propagation

ReactiveSecurityContextHolder reads the principal from the Reactor Context, because the ThreadLocal-based holder cannot survive a thread hop on the event loop. A precise way to test whether you understand why reactive code needs its own security plumbing.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

In a Spring WebFlux controller, how do you obtain the currently authenticated user, and why can't you just call SecurityContextHolder.getContext()?

level: juniorimportance: must knowfreq 70%

answer

  1. ReactiveSecurityContextHolder.getContext() -> Mono<SecurityContext>
  2. principal lives in Reactor Context, not ThreadLocal
  3. empty Mono = anonymous
  4. @AuthenticationPrincipal / Mono<Principal> param
  5. must return the Mono to stay in the chain

basics

~10 s

Use ReactiveSecurityContextHolder.getContext(), which returns a Mono<SecurityContext> you map to the Authentication/principal. The blocking SecurityContextHolder reads a ThreadLocal, which isn't reliably set on WebFlux's shared event-loop threads.

solid answer

~40 s

In WebFlux you never read the principal from a ThreadLocal. Instead call ReactiveSecurityContextHolder.getContext(), which returns Mono<SecurityContext>; map it to getAuthentication() and then to the principal or name, and return that Mono/Flux so it stays part of the reactive chain. Alternatively inject the principal with @AuthenticationPrincipal or accept a Mono<Principal>/Authentication parameter — Spring's argument resolvers pull it from the reactive context for you. SecurityContextHolder.getContext() uses a ThreadLocal that Spring Security populates per request on the blocking Servlet model; on WebFlux a handful of event-loop threads serve many interleaved requests and processing hops threads between operators, so that ThreadLocal is empty or belongs to another request. The authenticated principal instead lives in the immutable Reactor Context attached to the subscription.

code

java · 18 lines
java
@RestController
class ProfileController {

    // Option 1: read from the reactive holder
    @GetMapping("/username")
    Mono<String> username() {
        return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .map(Authentication::getName)
                .switchIfEmpty(Mono.just("anonymous"));
    }

    // Option 2: let the argument resolver inject it
    @GetMapping("/me")
    Mono<String> me(@AuthenticationPrincipal UserDetails user) {
        return Mono.just(user.getUsername());
    }
}

go deeper

for a junior

Know the API name ReactiveSecurityContextHolder.getContext() and that it returns a Mono, plus @AuthenticationPrincipal as the easy path.

for a middle

Explain that empty Mono means anonymous and that you must return the Mono so it stays wired to the request chain.

for a senior

Contrast ThreadLocal (Servlet) vs Reactor Context (WebFlux) and know the argument resolvers behind @AuthenticationPrincipal / Mono<Principal>.

for a principal

Discuss failure modes under load (cross-request ThreadLocal leakage) and why the framework chose subscription-scoped context over thread-scoped.

## The problem In classic Spring MVC (Servlet stack), each HTTP request is bound to one thread for its whole lifetime. Spring Security stores the authenticated user in a **`ThreadLocal`** via `SecurityContextHolder` (default strategy `MODE_THREADLOCAL`). Anywhere in that call stack you can call `SecurityContextHolder.getContext().getAuthentication()` and get the right user, because you're guaranteed to be on the same thread the filter set it on. Spring WebFlux is different. It runs on a small pool of **event-loop threads** (Reactor Netty, typically one per CPU core). A single request is processed as a chain of asynchronous operators, and work can **hop between threads** (e.g. after a `flatMap` that switches schedulers). One event-loop thread also serves **many requests interleaved**. A `ThreadLocal` therefore cannot reliably carry per-request state: it may be empty, or worse, hold *another* request's data. So `SecurityContextHolder.getContext()` on WebFlux returns an empty/incorrect context. ## The reactive solution Spring Security stores the authenticated user in the **Reactor Context** — an immutable key/value map attached to the *subscription*, not to a thread — and exposes it through **`ReactiveSecurityContextHolder`**. ```java ReactiveSecurityContextHolder.getContext() // Mono<SecurityContext> .map(SecurityContext::getAuthentication) // Authentication .map(Authentication::getName) // e.g. the username ``` Key facts: - `getContext()` returns a **`Mono<SecurityContext>`**, never a plain value — reading the principal is itself an async step in the chain. - If there is no authenticated user, the Mono is **empty** (it does not emit), so `.map(...)` is simply skipped. Use `.switchIfEmpty(...)` or `.defaultIfEmpty(...)` to handle anonymous access. - You **must return** the resulting Mono/Flux from your controller so it stays connected to the request's reactive chain — that's how it can read the context. ## Convenience alternatives You usually don't call the holder directly. Spring's argument resolvers read the same reactive context: - **`@AuthenticationPrincipal`** on a method parameter (resolved by `AuthenticationPrincipalArgumentResolver`). - Declaring a parameter of type `Mono<Principal>`, `Authentication`, or `Principal` — WebFlux injects it. ```java @GetMapping("/me") Mono<String> me(@AuthenticationPrincipal UserDetails user) { return Mono.just(user.getUsername()); } ``` ## When to use what - Simple 'who am I' in a controller: `@AuthenticationPrincipal` or a `Mono<Principal>` param. - Needing the full `Authentication`/authorities deep inside a service chain: `ReactiveSecurityContextHolder.getContext()`. - Method security (`@PreAuthorize`) works the same way reactively; it reads the reactive context, not the ThreadLocal. ## Gotcha Calling `SecurityContextHolder.getContext()` (the blocking one) inside WebFlux is a classic bug: it compiles, sometimes even 'works' in a naive test, then returns null/wrong data under load. Always use the `Reactive*` variant on the reactive stack.

  • What does ReactiveSecurityContextHolder.getContext() emit when the request is unauthenticated?
    An empty Mono — it completes without emitting a SecurityContext. Chain a switchIfEmpty/defaultIfEmpty to handle the anonymous case; otherwise downstream maps are silently skipped.
  • If you call SecurityContextHolder.getContext() (the blocking one) in a WebFlux handler, what happens?
    It reads a ThreadLocal that Spring Security never populated on the event-loop thread, so you get an empty context (or, under interleaving, another request's context). It compiles and may pass a trivial test, which makes it a dangerous latent bug.

context

open as a page

Explain precisely why the ThreadLocal-based SecurityContextHolder breaks on WebFlux's event loop, and what replaces it.

level: middleimportance: must knowfreq 65%

basics

~20 s

WebFlux uses a few event-loop threads that serve many requests and hop threads between operators. A ThreadLocal is bound to a thread, not a request, so it's empty or holds the wrong user. The Reactor Context (subscription-scoped) replaces it.

open as a page

A developer reads the principal fine at the top of a WebFlux handler but gets an empty Mono deeper in the pipeline. What common mistakes break Reactor Context propagation of the security context?

level: seniorimportance: should knowfreq 40%

basics

~20 s

The context is lost whenever you leave the connected chain: subscribing to a separate publisher, using an independent subscribe()/block, spawning work whose context you don't propagate, or mixing in blocking code that reads SecurityContextHolder. Keep everything in one returned chain.

open as a page

How does the authenticated principal actually get into the Reactor Context so ReactiveSecurityContextHolder can read it, and how would you seed it manually in a test?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Spring Security's reactive filter chain authenticates the request and writes the SecurityContext into the Reactor Context with contextWrite. In tests you seed it via ReactiveSecurityContextHolder.withAuthentication(...) / .withSecurityContext(...) using contextWrite, or the @WithMockUser annotation.

open as a page

You must call a blocking library that reads SecurityContextHolder (ThreadLocal) from within a WebFlux request. How do you bridge the reactive SecurityContext to that ThreadLocal correctly, and what are the risks?

level: principalimportance: nice to knowfreq 20%

basics

~10 s

Use the Micrometer context-propagation library to copy the reactive SecurityContext into a ThreadLocal for the blocking call. Enable Reactor's automatic context propagation (Hooks.enableAutomaticContextPropagation) or contextCapture(), and run the blocking code on Schedulers.boundedElastic().

open as a page