Spring webflux custom authentication for API
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:
@Configurationpublic 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:
@Componentpublic 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:
@Componentpublic 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@EnableReactiveMethodSecuritypublic 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(); }
@Autowiredprivate 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
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).
@Overridepublic 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:
@Componentpublic class MyService { @PreAuthorize("hasRole('ADMIN')") public Mono<String> checkAdmin() { // my secure method }}