I've tried working with both ReactiveAuthenticationManager
and a WebFilter
. In both cases, the execution flow is some form of this:
@Component
@RequiredArgsConstructor
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String token = authentication.getCredentials().toString();
if (!jwtTokenProvider.validateToken(token)) {
System.out.println("Invalid token");
return Mono.empty();
}
String username = jwtTokenProvider.getUsernameFromToken(token);
return userRepository.findByEmail(username)
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.map(user -> {
System.out.println("hydrated user from token. File is JRAM");
System.out.println(user); // this prints user
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user.getUsername(),
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
return authenticationToken;
});
}
}
As commented above, token is decoded and user is hydrated correctly. I believe the problem stems from binding it to the ReactiveSecurityContextHolder
. I tried different variations of the below:
SecurityContextImpl securityContext = new SecurityContextImpl(authentication);
Context context = ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext));
In one instance, I tried setting security context and returning that to the mono. In another instance, I tried just setting the authentication directly on ReactiveSecurityContextHolder.withAuthentication
. No dice. When I was working with ReactiveAuthenticationManager
, I also tried ServerSecurityContextRepository
@Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
String authHeader = swe.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return authenticationManager.authenticate(auth).map(SecurityContextImpl::new);
} else {
return Mono.empty();
}
}
Didn't help. Trying to read authentication from the ReactiveSecurityContextHolder
in a subsequent mono method fails with a NullPointerException
. So I'm guessing the setting breaks but I can't debug that cuz switchIfEmpty
and onErrorResume
just print out whatever they're given all the same.
Most stack overflow answers on the subject are outdated, using deprecated methods. Also none of these work for me while retrieving the user. Authentication is probably failing but at some point, I could access user parameter although it was blank. Other times, request just backs off with 401
Below is the securityConfig
http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.cors(httpSecurityCorsConfigurer -> httpSecurityCorsConfigurer.configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
// bunch of stuff
return config;
}))
/*.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)*/
.authorizeExchange(authorizeExchangeSpec -> {
authorizeExchangeSpec.pathMatchers(
"api/v1/user/login",
"api/v1/user/register","webjars/**",
"/v3/api-docs/**", "/swagger-ui.html",
"/swagger-ui/**").permitAll();
authorizeExchangeSpec.anyExchange().authenticated();
})
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.build();
Kindly help spell out the necessary steps to pass my user to the security context and retrieve it from controller methods. I'm on spring boot 3.3.4. Thanks
With a simplistic version of your JwtReactiveAuthenticationManager
and the below code, I'm able to access Authentication
inside a controller.
I hope I understood the issue correct.
SecurityConfig
import no.mycompany.webflux.JwtReactiveAuthenticationManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import reactor.core.publisher.Mono;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
private final JwtReactiveAuthenticationManager authenticationManager;
public SecurityConfig(JwtReactiveAuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.anyExchange().hasRole("USER")
)
.addFilterAt(jwtAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.build();
}
private AuthenticationWebFilter jwtAuthenticationFilter() {
var authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(exchange -> {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
var token = authHeader.substring(7);
// instance is passed to JwtReactiveAuthenticationManager#authenticate
return Mono.just(new UsernamePasswordAuthenticationToken(token, token));
}
return Mono.empty();
});
return authenticationWebFilter;
}
}
Controller
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api")
public class SomeController {
@GetMapping
public Mono<String> sayHello(Authentication authentication) {
return Mono.just("Hello World " + authentication.getName());
}
}
Update Added implementation of my mock JwtReactiveAuthenticationManager
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Collections;
@Component
public class JwtReactiveAuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// to get token: authentication.getCredentials().toString();
// for this demo, we return a fixed user
return Mono.just(new UsernamePasswordAuthenticationToken(
"user1",
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")))
);
}
}
build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7"
}
group = "no.mycompany"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<Test> {
useJUnitPlatform()
}