Spring webflux custom authentication for API Spring webflux custom authentication for API spring spring

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   }}