# 5. ReactiveSecurityContextHolder and Reactor Context [← Prev: Executor/ExecutorService wrapping](04-executor-wrapping.md) | [Next: Scheduled tasks →](06-scheduled-tasks.md) Every other demo in this repository asks "does the *thread* on the other side of a hand-off see the `SecurityContext`?" `Demo5ReactiveContext.java` asks a different question, because WebFlux doesn't have a thread on the other side of anything in the sense the rest of this repo cares about -- a reactive chain hops between scheduler threads as operators execute, and none of those threads is dedicated to one request. Full output in [`docs/output/demo5.txt`](output/demo5.txt). ## Why ThreadLocal genuinely cannot work here Scenarios A and B make the failure concrete rather than asserted. A sets `SecurityContextHolder` (a `ThreadLocal`) on the calling thread and reads it back immediately -- works, because nothing has moved threads yet. B does the identical setup, but inserts a single `.publishOn(Schedulers.boundedElastic())` between the write and the read -- exactly what a real WebFlux event loop does routinely between operators -- and the context is gone: ``` A) SecurityContextHolder (ThreadLocal), no scheduler hop: authenticated as alice B) SecurityContextHolder (ThreadLocal), AFTER publishOn to a different thread: NO AUTHENTICATION (lost) ``` This is the actual mechanism behind the post's line "`ThreadLocal` doesn't work because operations jump between threads" -- not a general reactive-programming caveat, a specific, reproducible failure with a specific operator. ## ReactiveSecurityContextHolder: the fix, and why it survives the same hop `ReactiveSecurityContextHolder.getContext()` doesn't read a `ThreadLocal` at all -- it reads Project Reactor's own `Context`, which is attached to the *subscription*, not a thread, and which Reactor propagates through every operator in the chain regardless of which `Scheduler` runs which step. `ReactiveSecurityContextHolder.withAuthentication(auth)` produces a `Context` you attach with `.contextWrite(...)`. Scenarios C and D repeat A and B with this mechanism instead, and D survives the identical `publishOn` hop that killed B: ``` C) ReactiveSecurityContextHolder + contextWrite, no scheduler hop: Hello, carol D) ReactiveSecurityContextHolder + contextWrite, AFTER publishOn to a different thread: Hello, dave ``` This is the post's `getProfile()` example, reproduced verbatim as `Demo5ReactiveContext.getProfile()`: ```java static Mono getProfile() { return ReactiveSecurityContextHolder.getContext() .map(securityContext -> "Hello, " + securityContext.getAuthentication().getName()) .defaultIfEmpty("Anonymous"); } ``` ## Edge case: `map()` cannot emit `null` Writing this demo's JUnit counterpart in [Chapter 8](08-testing-contract.md) hit a real `NullPointerException` on the first attempt: `Mono.map(...)` throws if the mapper function returns `null` (Reactor treats a `null` signal as a programming error, not an empty result -- that's what `Mono.empty()`/`defaultIfEmpty()` are for). A lambda that reads `SecurityContextHolder.getContext().getAuthentication()` and returns it directly breaks the moment the authentication is absent. The fix used throughout this demo and its test is to map to a descriptive `String` ("NO AUTHENTICATION (lost)") instead of passing a possibly-null domain object through a reactive operator. This is a real trap for exactly the kind of diagnostic code you'd add while debugging a context-propagation bug in a reactive pipeline. ## Edge case: no context ever written Scenario E calls `getProfile()` with no `.contextWrite(...)` anywhere upstream at all -- `ReactiveSecurityContextHolder.getContext()` completes **empty**, not with an error, so `.defaultIfEmpty("Anonymous")` fires cleanly: ``` E) getProfile() with no contextWrite() upstream at all: Anonymous ``` This matters for the post's `/profile` endpoint: an anonymous request to a permitted path never throws inside `getProfile()`, it degrades to "Anonymous" -- the `defaultIfEmpty` isn't defensive boilerplate, it's covering a real, reachable case. ## What this means for `@AuthenticationPrincipal Mono` The post's second example, `getUser(@AuthenticationPrincipal Mono user)`, is the same mechanism at one more remove: Spring Security resolves that `Mono` parameter by reading `ReactiveSecurityContextHolder` internally before your method runs, so it inherits everything in this chapter for free. If a `Mono` argument comes back empty in a WebFlux controller for a request you expected to be authenticated, the two things worth checking first are exactly A and B above: is a `ThreadLocal`-based mechanism (yours or a library's) trying to read the context after a scheduler hop, and is `.contextWrite(...)` actually upstream of the read in the chain that populates it (`SecurityWebFilterChain` normally handles this for you, but a hand-rolled `WebFilter` that reorders operators can break it).