In this series of posts, we'll dive into how exactly to configure Spring Security for reactive and stateless WebFlux REST APIs. Specifically, we'll look at how to:

  1. Have Spring Security pick users from our user table, rather than its default in-memory user-store.
  2. Support form-login to obtain a token — i.e., a user should be able to call POST /login with username and password to receive a token.
  3. Configure authorization at request level, as well as method level.
  4. Make the API stateless, i.e. not storing the Spring Context in the session.
  5. Return a 401 Unauthorized response instead of redirecting to a login page when an unauthenticated user tries to access a restricted page, or in case of an authentication failure.
  6. Configure custom token authentication (for stateless authentication, using Authorization Bearer JWT/JWE tokens).
  7. Configure stuffs like CSRF, CORS, logout etc.

For code examples, we’ll refer to Spring Lemon. If you haven’t heard of Spring Lemon, you should give it a look. It’s a library encapsulating the sophisticated non-functional code and configuration that’s needed when developing real-world RESTful web services using the Spring framework and Spring Boot.

So, let the adventure begin!

The Basics

Spring Security has documented here a minimal version of configuration for WebFlux applications, which looks as below:

@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {
    @Bean
    public MapReactiveUserDetailsService userDetailsService() {
        UserDetails user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("user")
            .roles("USER")
            .build();
        return new MapReactiveUserDetailsService(user);
    }
    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
            .authorizeExchange()
                .anyExchange().authenticated() // 1
                .and()
            .httpBasic().and() // 2
            .formLogin(); // 3
        return http.build(); //4
    }
}

In summary, we'll need to provide a couple of beans:

  1. A ReactiveUserDetailsService connecting to our user DB
  2. A SecurityWebFilterChain for configuring access rules etc.

Spring Boot auto configuration

Default beans similar to above get auto configured when using Spring Boot — as documented here. But, to meet our requirements, we of course need to replace those with ours.

Also note that the @EnableWebFluxSecurity annotation isn't required in Spring Boot applications.

A Better ReactiveUserDetailsService

As you see above, we need to configure a ReactiveUserDetailsService, so that Spring Security finds our users. A map based user details service is configured above, but in real world we'll need to code it to access our user store. For example, here is a minimal sample derived from Spring Lemon's user details service:

@Service
public class MyReactiveUserDetailsService implements ReactiveUserDetailsService {
    @Autowired
    private final UserRepository userRepository;
    @Override
    public Mono<UserDetails> findByUsername(String username) {
        return userRepository.findByUsername(username).switchIfEmpty(Mono.defer(() -> {
            return Mono.error(new UsernameNotFoundException("User Not Found"));
        })).map(User::toUserDetails);
    }
}

The above code assumes that we have a User domain class, which has a toUserDetails method that returns a UserDetails object. So, we'll need to define the User class and an implementation of UserDetails. The User class could look something like this::

public class User {
    private String username;
    private String password;
    private Collection<String> roles;
    // Getters and setters
    public UserDetails toUserDetails() {
          // returns a UserDetails object
    }
}

The toUserDetails method above should return an object that should have implemented UserDetails, looking something like this:

public class MyUserDetails implements UserDetails {
    private String username;
    private String password;
    private Collection<String> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // return authorities derived from roles;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

This being same as traditional Spring Security, here we'll not discuss the details.

So, next step will be configuring our SecurityWebFilterChain bean, which we'll take up in the next post.