Skip to content

12.5.2 Configuring a reactive user details service

When extending WebSecurityConfigurerAdapter, you override one configure() method to declare web security rules and another configure() method to configure authentication logic, typically by defining a UserDetails object. As a reminder of what this looks like, consider the following overridden configure() method that uses an injected UserRepository object in an anonymous implementation of UserDetailsService to look up a user by username:

java
@Autowired
UserRepository userRepo;

@Override
protected void
    configure(AuthenticationManagerBuilder auth)
    throws Exception {
  auth
    .userDetailsService(new UserDetailsService() {
      @Override
      public UserDetails loadUserByUsername(String username)
                        throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username)
        if (user == null) {
          throw new UsernameNotFoundException(
                  username " + not found")
        }
        return user.toUserDetails();
      }
  });
}

In this nonreactive configuration, you override the only method required by UserDetailsService: loadUserByUsername(). Inside of that method, you use the given UserRepository to look up the user by the given username. If the name isn’t found, you throw a UsernameNotFoundException. But if it’s found, then you call a helper method, toUserDetails(), to return the resulting UserDetails object.

In a reactive security configuration, you don’t override a configure() method. Instead, you declare a ReactiveUserDetailsService bean. ReactiveUserDetailsService is the reactive equivalent to UserDetailsService. Like UserDetailsService, ReactiveUserDetailsService requires implementation of only a single method. Specifically, the findByUsername() method returns a Mono<UserDetails> instead of a raw UserDetails object.

In the following example, the ReactiveUserDetailsService bean is declared to use a given UserRepository, which is presumed to be a reactive Spring Data repository (which we’ll talk more about in the next chapter):

java
@Service
@Bean
public ReactiveUserDetailsService userDetailsService(
                    UserRepository userRepo) {
  return new ReactiveUserDetailsService() {
    @Override
    public Mono<UserDetails> findByUsername(String username) {
      return userRepo.findByUsername(username)
        .map(user -> {
          return user.toUserDetails();
        });
    }
  };
}

Here, a Mono<UserDetails> is returned as required, but the UserRepository.findByUsername() method returns a Mono<User>. Because it’s a Mono, you can chain operations on it, such as a map() operation to map the Mono<User> to a Mono<UserDetails>.

In this case, the map() operation is applied with a lambda that calls the helper toUserDetails() method on the User object published by the Mono. This converts the User to a UserDetails. As a consequence, the .map() operation returns a Mono<UserDetails>, which is precisely what the ReactiveUserDetailsService.findByUsername() requires. If findByUsername() can’t find a matching user, then the Mono returned will be empty, indicating no match and resulting in a failure to authenticate.

Released under the MIT License.