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?
answer
- AuthenticationWebFilter -> ServerSecurityContextRepository
- ReactorContextWebFilter = the contextWrite bridge
- read = Mono.deferContextual lookup
- test seed: withAuthentication / withSecurityContext + contextWrite
- contextWrite goes at the TAIL, flows upstream
basics
~10 sSpring 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.
solid answer
~40 sOn the reactive stack, WebFilters run before your handler. AuthenticationWebFilter validates credentials and stores the resulting SecurityContext through a ServerSecurityContextRepository; ReactorContextWebFilter then loads that Mono<SecurityContext> and injects it into the request's Reactor Context using contextWrite under the key ReactiveSecurityContextHolder reads. Because contextWrite writes to the upstream chain, everything downstream — your controller, services, @PreAuthorize checks — sees it via Mono.deferContextual. For tests you don't run the filter chain, so you seed the context yourself: chain .contextWrite(ReactiveSecurityContextHolder.withAuthentication(token)) onto the Mono under test, or use WebTestClient with @WithMockUser / mutateWith(mockUser()). The key insight is that population is just a contextWrite performed by a filter, and reading is a deferContextual lookup — nothing thread-bound anywhere.
code
java · 22 lines@Service
class GreetingService {
Mono<String> currentUsername() {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getName());
}
}
class GreetingServiceTest {
private final GreetingService service = new GreetingService();
@Test
void readsSeededPrincipal() {
var auth = new TestingAuthenticationToken("alice", "pw", "ROLE_USER");
StepVerifier.create(
service.currentUsername()
// seed at the tail: contextWrite propagates upstream to the reader
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)))
.expectNext("alice")
.verifyComplete();
}
}go deeper
Enough to know tests use @WithMockUser or withAuthentication + contextWrite.
Explain that a filter writes the context and getContext() reads it; know the test seeding helpers.
Name ReactorContextWebFilter and ServerSecurityContextRepository and explain upstream contextWrite semantics and correct placement.
Discuss stateless vs session-backed ServerSecurityContextRepository choices and how custom filters must preserve context propagation.
## The write side: how the principal gets in On WebFlux, the equivalent of the Servlet filter chain is a chain of **`WebFilter`** beans assembled by `SecurityWebFilterChain` (configured via `ServerHttpSecurity`). The relevant pieces for context propagation: 1. **`AuthenticationWebFilter`** — extracts credentials (Basic header, bearer token, form login, etc.), calls a `ReactiveAuthenticationManager`, and on success produces an `Authentication`. It saves the resulting `SecurityContext` via a **`ServerSecurityContextRepository`** (e.g. `WebSessionServerSecurityContextRepository`, or a stateless no-op for token auth). 2. **`ReactorContextWebFilter`** — this is the bridge. It reads `SecurityContext` from the `ServerSecurityContextRepository` and **writes it into the Reactor Context** via `contextWrite`, under the internal key that `ReactiveSecurityContextHolder` looks up. From that point on, everything downstream in the subscription can read it. Because Reactor's `contextWrite` mutates the context for the **upstream** portion of the chain (context flows from subscriber toward source), and your handler is downstream of these filters within the same subscription, your controller and services transparently see the value. ## The read side `ReactiveSecurityContextHolder.getContext()` is effectively: ```java Mono.deferContextual(ctx -> ctx.hasKey(SECURITY_CONTEXT_KEY) ? ctx.<Mono<SecurityContext>>get(SECURITY_CONTEXT_KEY) : Mono.empty()); ``` `deferContextual` reads the `ContextView` of the current subscription. If no security context was written (unauthenticated / anonymous), it returns an empty Mono. ## Manual population (tests and edge cases) In a unit test you typically call a service method directly without the filter chain, so nothing seeds the context. `ReactiveSecurityContextHolder` gives you helpers that produce a `Context` fragment to pass to `contextWrite`: - `ReactiveSecurityContextHolder.withAuthentication(Authentication)` — convenience for a single `Authentication`. - `ReactiveSecurityContextHolder.withSecurityContext(Mono<SecurityContext>)` — full control. - `ReactiveSecurityContextHolder.clearContext()` — remove it. ```java StepVerifier.create( service.currentUsername() .contextWrite(ReactiveSecurityContextHolder.withAuthentication( new TestingAuthenticationToken("alice", "pw", "ROLE_USER")))) .expectNext("alice") .verifyComplete(); ``` For slice/integration tests with `WebTestClient`, use `spring-security-test`: - `@WithMockUser` / `@WithUserDetails` on the test method (works reactively via `WithSecurityContextTestExecutionListener` + `ReactorContextTestExecutionListener`). - `webTestClient.mutateWith(SecurityMockServerConfigurers.mockUser("alice").roles("USER"))`. ## Critical gotcha: contextWrite placement `contextWrite` affects only the chain **above** where it's placed and only that subscription. If you write it *after* the operator that reads the context, or on a **different** Mono that you subscribe independently, the reader sees nothing. Always place `contextWrite` at the *end* of the reactive chain being tested (closest to the subscriber), because it propagates upstream to the readers. ```java // WRONG: contextWrite is upstream of the reader in the wrong position service.currentUsername() // reads context here // ... but a NEW independent subscription with its own context won't help // RIGHT: write at the tail, it flows up to the reader service.currentUsername() .contextWrite(ReactiveSecurityContextHolder.withAuthentication(token)); ``` ## When to know this Senior candidates should be able to name `ReactorContextWebFilter` as the bridge and explain the write-upstream semantics, because it explains both how production auth flows and why tests must seed the context at the tail of the chain.
- Why must contextWrite be placed at the end (tail) of the chain rather than the beginning?Reactor's context propagates from the subscriber upstream toward the source. contextWrite modifies the context seen by everything above it. Placing it at the tail (closest to subscribe) makes the seeded value visible to the deferContextual reads earlier in the pipeline; placing it before/above the reader wouldn't reach it.
- Which filter is responsible for moving the SecurityContext from the repository into the Reactor Context?ReactorContextWebFilter. AuthenticationWebFilter authenticates and persists via a ServerSecurityContextRepository; ReactorContextWebFilter loads it and does the contextWrite so ReactiveSecurityContextHolder can read it downstream.
saying these in an interview costs you the question
- Saying contextWrite should go at the top of the chain (it propagates upstream, so tail placement is required).
- Claiming @WithMockUser works by setting a ThreadLocal for the reactive test.
- Thinking you can seed context on one Mono and read it on a separately-subscribed Mono.
- Believing the controller sets the security context rather than the filter chain.