
Reactive Security: WebFlux & JwtAuthenticationToken
Explore WebFlux security patterns using ReactiveSecurityContextHolder and JwtAuthenticationToken for non-blocking authentication.
Reactive Security: WebFlux, ReactiveSecurityContextHolder, and JwtAuthenticationToken
In traditional Spring MVC, authentication state lives in a ThreadLocal. The container thread that handles the request holds the SecurityContext, and any code running on that thread can access it. This model collapses in Spring WebFlux. WebFlux runs on an event loop (typically Netty), where a single thread handles thousands of concurrent requests by switching between them during I/O waits. If you try to use SecurityContextHolder.getContext() in a reactive pipeline, you risk accessing context from the wrong thread or blocking the event loop, causing system-wide latency spikes.
This is Part 7 of the Spring Security Filter Chain Mastery series.
The solution is ReactiveSecurityContextHolder. It treats the security context not as a static variable, but as a reactive stream (Mono<SecurityContext>). This allows the authentication state to be passed through the reactive pipeline using Reactor’s ContextView and ContextWrite mechanisms, ensuring that downstream operators have access to the authenticated user without blocking or relying on thread affinity.
Why Thread-Local Security Fails in Reactive Streams
To understand ReactiveSecurityContextHolder, we must first look at what it replaces. In Spring MVC, SecurityContextHolder uses a ThreadLocal by default. When a request arrives, the framework sets the context on the handling thread. When a controller calls a service, the service inherits the same thread and thus the same context.
In WebFlux, execution is asynchronous. A Mono or Flux may switch threads between operators. If operator A runs on thread event-loop-1 and operator B runs on thread event-loop-2, a ThreadLocal set in A is invisible to B. Furthermore, blocking the event loop to fetch security data (e.g., hitting a database to verify a user) starves other requests.
ReactiveSecurityContextHolder solves this by binding the context to the Reactor Context. Every Mono and Flux carries an immutable context map. When you subscribe to a security context, you are reading from this map, which is propagated automatically through the reactive chain.
The JWT Authentication Filter
The entry point for reactive authentication is usually a custom WebFilter. This filter intercepts every request, extracts the JWT from the Authorization header, and validates it. Token extraction logic must handle malformed headers gracefully, checking for null values and ensuring the Bearer prefix exists before attempting parsing.
Consider a filter that validates a JWT and produces a JwtAuthenticationToken. The key mechanism here is the transformation of the raw token into a reactive authentication object.
public class JwtAuthenticationFilter implements WebFilter {
private final ReactiveJwtDecoder jwtDecoder;
public JwtAuthenticationFilter(ReactiveJwtDecoder jwtDecoder) {
this.jwtDecoder = jwtDecoder;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = extractToken(exchange.getRequest());
if (token == null || !token.startsWith("Bearer ")) {
return chain.filter(exchange);
}
// Decode and validate the JWT
Mono<Jwt> jwtMono = Mono.just(token.substring(7))
.flatMap(jwtDecoder::decode);
return jwtMono
.map(jwt -> new JwtAuthenticationToken(jwt, Collections.emptyList()))
.flatMap(authentication -> {
// Place the authentication into the reactive context
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
})
.onErrorResume(JwtException.class, e -> {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
});
}
}In this code, jwtMono represents the asynchronous validation of the token. Once validated, we create a JwtAuthenticationToken. This token is an implementation of Authentication that holds the JWT claims. Note that we provide an empty list of authorities; in production, you would map JWT claims to Spring Security GrantedAuthority objects.
Propagating Context with contextWrite
The critical line is .contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)). This does not set a global variable. Instead, it returns a new Mono<Void> that carries the authentication data in its Reactor context.
When chain.filter(exchange) executes, it returns a Mono<Void> representing the rest of the filter chain and the controller logic. By chaining contextWrite, we ensure that any operator downstream of this filter can access the SecurityContext via ReactiveSecurityContextHolder.getContext(). The Reactor context is immutable; each contextWrite creates a new context instance that merges with the previous one. This allows multiple filters to contribute distinct pieces of data (e.g., user details, request metadata) without overwriting each other, provided they use distinct keys.
Accessing the Context Downstream
In a controller or service, you do not inject SecurityContext. You subscribe to ReactiveSecurityContextHolder.getContext(). This returns a Mono<SecurityContext> that resolves to the context associated with the current reactive stream.
@GetMapping("/profile")
public Mono<UserProfile> getProfile() {
return ReactiveSecurityContextHolder.getContext()
.flatMap(ctx -> {
JwtAuthenticationToken auth = (JwtAuthenticationToken) ctx.getAuthentication();
String userId = auth.getToken().getSubject();
return userService.findById(userId);
});
}Here, ReactiveSecurityContextHolder.getContext() reads from the Reactor context established by the filter. If the filter did not write the context, this mono would emit an empty context or throw an error, depending on configuration. Because the context is propagated via the reactive stream, this call is non-blocking and safe to use anywhere in the pipeline. To handle cases where authentication is optional, you might use .defaultIfEmpty(new SecurityContextImpl()) or .onErrorResume to provide fallback logic, ensuring the application remains robust even when security headers are absent.
Conclusion
ReactiveSecurityContextHolder is the bridge between traditional security concepts and reactive programming. It shifts the model from thread-bound state to stream-bound data. By using contextWrite in filters and getContext() in consumers, you maintain secure, non-blocking authentication flows that scale efficiently on event loops. This pattern ensures that security checks are performed once, and the resulting context is available throughout the request lifecycle without blocking threads or risking cross-thread contamination.
Related posts
Spring Security with Reactive WebFlux: Security for Reactive Applications
An examination of Spring Security integrated with Reactive WebFlux, covering authentication, reactive JWT, and securing reactive applications.
SecurityFilterChain in Spring Security 6
A technical walkthrough of configuring SecurityFilterChain in Spring Security 6 using the Lambda DSL and RequestMatchers for Java developers.
Building a Custom Authentication Provider in Spring Security
This article covers the implementation of a custom authentication mechanism within Spring Security using a dedicated AuthenticationProvider.