Mechanically, how does the SecurityContext reach downstream operators, and why does Reactor Context propagation direction matter?
answer
- AuthenticationWebFilter does contextWrite
- Reactor Context flows bottom-up (toward source)
- write must be downstream of the read
- deferContextual reads SecurityContext key
- ThreadLocal bridging needs context-propagation
basics
~20 sSpring Security's AuthenticationWebFilter authenticates the request and calls .contextWrite(...) to place the SecurityContext into the Reactor Context. Because Reactor Context flows bottom-up (from subscriber toward source), that write is visible to all operators above it, i.e. your handler.
solid answer
~40 sThe reactive filter chain's `AuthenticationWebFilter` authenticates the exchange and then continues the downstream filter chain wrapped in `.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(...))`. Reactor's `Context` is **immutable and propagates from the subscriber upward toward the source** — `contextWrite` at a given point makes the value visible to every operator *above* it in the assembly chain, which includes your controller/service that later calls `ReactiveSecurityContextHolder.getContext()`. This upstream-flowing model is why the security filter must sit closer to the subscriber than your business code, and why manually adding `contextWrite` *below* the code that reads the context has no effect. It also means the context is per-subscription and immune to thread switches (`publishOn`/`subscribeOn`), unlike a ThreadLocal. When bridging to imperative/ThreadLocal code you need Reactor's context-propagation library or explicit copying, because the value won't auto-populate a ThreadLocal.
code
java · 11 lines// Manually establishing a context (e.g. a custom filter or a test):
Mono<String> pipeline =
ReactiveSecurityContextHolder.getContext() // read (upstream)
.map(ctx -> ctx.getAuthentication().getName())
.publishOn(Schedulers.parallel()) // thread hop: context survives
// contextWrite is placed AFTER the read in the fluent chain,
// so it propagates UPSTREAM into getContext():
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(
new UsernamePasswordAuthenticationToken("alice", "n/a", List.of())));
// pipeline emits "alice" — the write below the read is visible to it.go deeper
Not expected to know propagation direction in detail.
Should know the filter writes the context and reads survive thread hops.
Should explain bottom-up propagation, the write-below-read rule, and deferContextual reads.
Should reason about ThreadLocal bridging, context-propagation library, and designing custom filters that don't break the chain.
## Reactor Context is not a ThreadLocal Reactor's **`Context`** is an **immutable** key/value store bound to a **Subscription** (one execution of a reactive pipeline). Two facts drive everything: 1. **It is tied to the stream, not the thread**, so it survives `publishOn`/`subscribeOn` thread hops — the exact property ThreadLocal lacks. 2. **It propagates from the bottom (the subscriber) upward toward the source (the publisher).** When you call `.contextWrite(...)`, Reactor evaluates the write and exposes the resulting context to operators *assembled above* that point (upstream). Operators *below* it (downstream, closer to the subscriber) do not see it. ## The write side: who and where During request handling, Spring Security's **`AuthenticationWebFilter`** (part of the `SecurityWebFilterChain`) authenticates the exchange, producing an `Authentication`. It then invokes the rest of the WebFilter chain wrapped roughly as: ```java chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext))); ``` Because your controller/service runs *inside* (upstream of) that continued chain, when it calls `ReactiveSecurityContextHolder.getContext()` the deferred read resolves the value the filter wrote. ## The read side `ReactiveSecurityContextHolder.getContext()` is implemented with `Mono.deferContextual(...)` — it reads the key `SecurityContext.class` (a documented internal key) from the contextual view at subscription time. If the key is absent, the Mono is empty (anonymous). ## Why direction matters — the classic trap Because propagation is **bottom-up**, this is a no-op: ```java service.currentUser() // reads context .contextWrite(...) // WRONG: below the read, invisible to it ``` and this works: ```java service.currentUser() // ... more operators ... ; // the contextWrite must be assembled AFTER (downstream of) currentUser() Mono<String> pipeline = service.currentUser().contextWrite(...); // still below! ``` The correct mental model: put `contextWrite` **downstream** (later in the fluent chain) of the operators that must observe it, because writes flow upstream. In practice the framework handles this; the trap appears when developers add manual `contextWrite` in the wrong spot in tests or custom filters. ## Thread-hop safety Since the value rides the subscription, inserting `publishOn(Schedulers.parallel())` between the write and the read does not lose it — the whole reason WebFlux abandoned ThreadLocal. ## Bridging to imperative code If you call blocking/imperative libraries that expect the ThreadLocal `SecurityContextHolder`, the reactive context will **not** auto-populate it. Use Reactor's **context-propagation** (Micrometer `context-propagation` + `ThreadLocalAccessor`, wired via `Hooks.enableAutomaticContextPropagation()` in recent Reactor) or copy the value explicitly on the boundary. Naively reading `SecurityContextHolder` on a scheduler thread returns nothing. ## Gotchas summary - `contextWrite` affects **upstream** operators only. - Context is **immutable** — each write returns a new context. - Absent key ⇒ empty Mono ⇒ handle anonymous. - ThreadLocal bridging needs explicit propagation. ## When this knowledge is needed Writing custom `WebFilter`s, debugging 'user is null in my service' issues, integrating blocking security-aware libraries, or authoring tests that set the context manually.
- You add .contextWrite(...) but getContext() still returns empty. What's the most likely cause?The contextWrite was assembled upstream (before) the getContext() read. Reactor Context flows bottom-up, so the write must appear downstream of — later in the fluent chain than — the operator that reads it.
- How do you make the reactive SecurityContext visible to blocking code that reads the ThreadLocal SecurityContextHolder?Use Reactor/Micrometer context-propagation (a ThreadLocalAccessor for the security context, plus Hooks.enableAutomaticContextPropagation), or copy the Authentication onto the ThreadLocal explicitly at the boundary. It is not automatic.
saying these in an interview costs you the question
- Claiming Reactor Context propagates top-down like normal method calls
- Believing contextWrite affects operators declared after it in the chain
- Assuming the reactive context auto-populates the ThreadLocal SecurityContextHolder