Spring webflux custom authentication for API
Thanks Jan you helped me a lot with your example to customize authentication in my Spring Webflux application and secure access to apis.
In my case I just need to read a header to set user roles and I want Spring security to check user authorizations to secure access to my methods.
You gave the key with custom http.securityContextRepository(this.securityContextRepository);
in SecurityConfiguration (no need of a custom authenticationManager).
Thanks to this SecurityContextRepository I was able to build and set a custom authentication (simplified below).
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
String role = serverWebExchange.getRequest().getHeaders().getFirst("my-header");
Authentication authentication =
new AnonymousAuthenticationToken("authenticated-user", someUser, AuthorityUtils.createAuthorityList(role) );
return Mono.just(new SecurityContextImpl(authentication));
}
And thus I can secure my methods using these roles:
@Component
public class MyService {
@PreAuthorize("hasRole('ADMIN')")
public Mono<String> checkAdmin() {
// my secure method
}
}
After a lot of searching and trying I think I have found the solution:
You need a bean of SecurityWebFilterChain
that contains all configuration.
This is mine:
@Configuration
public class SecurityConfiguration {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private SecurityContextRepository securityContextRepository;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
// Disable default security.
http.httpBasic().disable();
http.formLogin().disable();
http.csrf().disable();
http.logout().disable();
// Add custom security.
http.authenticationManager(this.authenticationManager);
http.securityContextRepository(this.securityContextRepository);
// Disable authentication for `/auth/**` routes.
http.authorizeExchange().pathMatchers("/auth/**").permitAll();
http.authorizeExchange().anyExchange().authenticated();
return http.build();
}
}
I've disabled httpBasic, formLogin, csrf and logout so I could make my custom authentication.
By setting the AuthenticationManager
and SecurityContextRepository
I overridden the default spring security configuration for checking if a user is authenticated/authorized for a request.
The authentication manager:
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
// JwtAuthenticationToken is my custom token.
if (authentication instanceof JwtAuthenticationToken) {
authentication.setAuthenticated(true);
}
return Mono.just(authentication);
}
}
I am not fully sure where the authentication manager is for, but I think for doing the final authentication, so setting authentication.setAuthenticated(true);
when everything is right.
SecurityContextRepository:
@Component
public class SecurityContextRepository implements ServerSecurityContextRepository {
@Override
public Mono<Void> save(ServerWebExchange serverWebExchange, SecurityContext securityContext) {
// Don't know yet where this is for.
return null;
}
@Override
public Mono<SecurityContext> load(ServerWebExchange serverWebExchange) {
// JwtAuthenticationToken and GuestAuthenticationToken are custom Authentication tokens.
Authentication authentication = (/* check if authenticated based on headers in serverWebExchange */) ?
new JwtAuthenticationToken(...) :
new GuestAuthenticationToken();
return new SecurityContextImpl(authentication);
}
}
In the load I will check based on the headers in the serverWebExchange
if the user is authenticated. I use https://github.com/jwtk/jjwt. I return a different kind of authentication token if the user is authenticated or not.
For those that have same issue(Webflux + Custom Authentication + JWT
) I solved using AuthenticationWebFilter
, custom ServerAuthenticationConverter
and ReactiveAuthenticationManager
, following the code hope could help someone in the future.
Tested with latest version(spring-boot 2.2.4.RELEASE
).
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SpringSecurityConfiguration {
@Bean
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
return http
.csrf()
.disable()
.headers()
.frameOptions().disable()
.cache().disable()
.and()
.authorizeExchange()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated()
.and()
.addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.build();
}
@Autowired private lateinit var userDetailsService: ReactiveUserDetailsService
class CustomReactiveAuthenticationManager(userDetailsService: ReactiveUserDetailsService?) : UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService) {
override fun authenticate(authentication: Authentication): Mono<Authentication> {
return if (authentication.isAuthenticated) {
Mono.just<Authentication>(authentication)
} else super.authenticate(authentication)
}
}
private fun responseError() : ServerAuthenticationFailureHandler{
return ServerAuthenticationFailureHandler{ webFilterExchange: WebFilterExchange, _: AuthenticationException ->
webFilterExchange.exchange.response.statusCode = HttpStatus.UNAUTHORIZED
webFilterExchange.exchange.response.headers.addIfAbsent(HttpHeaders.LOCATION,"/")
webFilterExchange.exchange.response.setComplete();
}
}
private AuthenticationWebFilter authenticationWebFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
authenticationWebFilter.setServerAuthenticationConverter(new JwtAuthenticationConverter(tokenProvider));
NegatedServerWebExchangeMatcher negateWhiteList = new NegatedServerWebExchangeMatcher(ServerWebExchangeMatchers.pathMatchers(AUTH_WHITELIST));
authenticationWebFilter.setRequiresAuthenticationMatcher(negateWhiteList);
authenticationWebFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
authenticationWebFilter.setAuthenticationFailureHandler(responseError());
return authenticationWebFilter;
}
}
public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
private final TokenProvider tokenProvider;
public JwtAuthenticationConverter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
private Mono<String> resolveToken(ServerWebExchange exchange) {
log.debug("servletPath: {}", exchange.getRequest().getPath());
return Mono.justOrEmpty(exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION))
.filter(t -> t.startsWith("Bearer "))
.map(t -> t.substring(7));
}
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
return resolveToken(exchange)
.filter(tokenProvider::validateToken)
.map(tokenProvider::getAuthentication);
}
}
public class CustomReactiveAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager {
public CustomReactiveAuthenticationManager(ReactiveUserDetailsService userDetailsService) {
super(userDetailsService);
}
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
if (authentication.isAuthenticated()) {
return Mono.just(authentication);
}
return super.authenticate(authentication);
}
}
PS: The TokenProvider class you find at https://github.com/jhipster/jhipster-registry/blob/master/src/main/java/io/github/jhipster/registry/security/jwt/TokenProvider.java