skip to content

What is ReactiveUserDetailsService, how does it fit into WebFlux authentication, and how do you provide one?

level: middleimportance: must knowfreq 60%

answer

  1. findByUsername -> Mono<UserDetails>
  2. reactive twin of UserDetailsService
  3. MapReactiveUserDetailsService = in-memory
  4. wrapped by UserDetailsRepositoryReactiveAuthenticationManager
  5. needs a PasswordEncoder; Mono.empty() = not found

basics

~10 s

ReactiveUserDetailsService loads a user by username and returns Mono<UserDetails> instead of blocking. You expose it as a bean; Spring wraps it in an authentication manager that checks the password for username/password logins.

solid answer

~40 s

ReactiveUserDetailsService is the reactive counterpart of UserDetailsService: its single method findByUsername(String) returns Mono<UserDetails> so lookups (DB, cache) stay non-blocking. In WebFlux, when you expose a ReactiveUserDetailsService bean (and a PasswordEncoder), Spring Boot auto-wires it into a UserDetailsRepositoryReactiveAuthenticationManager, which becomes the default authentication manager for username/password mechanisms like HTTP Basic and form login. That manager loads the user reactively, verifies the presented password against the stored (encoded) one via the PasswordEncoder, checks account flags (enabled, locked, expired), and emits an Authentication. For demos there's MapReactiveUserDetailsService (in-memory). For real systems you implement findByUsername yourself, typically backed by an R2DBC reactive repository, returning Mono.empty() when the user is absent so Spring raises the standard bad-credentials error.

code

java · 16 lines
java
@Bean
ReactiveUserDetailsService userDetailsService(AccountRepository repo) {
    // repo.findByUsername returns Mono<AccountEntity> (e.g. R2DBC)
    return username -> repo.findByUsername(username)
        .map(acc -> User.withUsername(acc.getUsername())
            .password(acc.getPasswordHash())   // already encoded
            .authorities(acc.getAuthorities())
            .accountLocked(!acc.isActive())
            .build());
    // absent user -> Mono.empty() -> bad credentials
}

@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

go deeper

for a junior

Know it loads a user reactively and returns Mono<UserDetails>.

for a middle

Explain how it's wired into the default reactive auth manager with a PasswordEncoder and how to back it with a DB.

for a senior

Discuss user-enumeration handling, status flags, and when token-based auth makes it unnecessary.

for a principal

Reason about non-blocking data access (R2DBC), caching lookups reactively, and interaction with custom ReactiveAuthenticationManagers.

## The abstraction `ReactiveUserDetailsService` is a functional interface: ```java public interface ReactiveUserDetailsService { Mono<UserDetails> findByUsername(String username); } ``` It is the reactive twin of the servlet `UserDetailsService` (`UserDetails loadUserByUsername(String)`). Returning **`Mono<UserDetails>`** lets the lookup be asynchronous/non-blocking (R2DBC, reactive Mongo, a remote call) so it never ties up a Netty event-loop thread. **`UserDetails`** describes the user: `getUsername()`, `getPassword()` (the **encoded** password), `getAuthorities()`, and status flags (`isEnabled`, `isAccountNonLocked`, `isAccountNonExpired`, `isCredentialsNonExpired`). ## How it plugs into authentication For username/password mechanisms (HTTP Basic, form login), authentication is coordinated by a `ReactiveAuthenticationManager`. The out-of-the-box implementation is **`UserDetailsRepositoryReactiveAuthenticationManager`**, which: 1. calls `findByUsername(username)`, 2. if `Mono.empty()`, raises `UsernameNotFoundException` (surfaced as bad credentials to avoid user enumeration), 3. otherwise checks the presented raw password against `userDetails.getPassword()` using the configured **`PasswordEncoder`**, 4. validates the status flags, 5. emits a fully-authenticated `Authentication` (an `UsernamePasswordAuthenticationToken` with authorities). With Spring Boot, **simply defining a `ReactiveUserDetailsService` bean plus a `PasswordEncoder` bean** is enough — Boot's reactive security auto-config builds that manager and wires it in. You usually don't construct the manager yourself. ## In-memory implementation ```java @Bean ReactiveUserDetailsService users(PasswordEncoder encoder) { UserDetails u = User.withUsername("alice") .password(encoder.encode("secret")) .roles("USER") .build(); return new MapReactiveUserDetailsService(u); } @Bean PasswordEncoder encoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } ``` `MapReactiveUserDetailsService` is the reactive analogue of `InMemoryUserDetailsManager` — great for tests/demos, not production. ## Database-backed implementation ```java @Bean ReactiveUserDetailsService userDetailsService(UserR2dbcRepository repo) { return username -> repo.findByUsername(username) .map(u -> User.withUsername(u.getUsername()) .password(u.getPasswordHash()) .authorities(u.getRoles().toArray(String[]::new)) .build()); } ``` Returning `Mono.empty()` when the row is missing is important — the manager translates it into an authentication failure. Never `.block()` inside the lambda. ## Relationship to ReactiveAuthenticationManager `ReactiveUserDetailsService` is **not** itself an authentication manager — it only *loads* users. The `ReactiveAuthenticationManager` (specifically `UserDetailsRepositoryReactiveAuthenticationManager`) *uses* it to *authenticate*. If you set a custom `authenticationManager` on `ServerHttpSecurity`/the auth filter, your user-details bean may be bypassed unless that manager consults it. ## Gotchas - Forgetting the `PasswordEncoder` bean -> startup/`IllegalArgumentException` about no mapped encoder (delegating encoder expects an `{id}` prefix on stored passwords). - Storing plaintext passwords -> encoder mismatch; always store encoded values. - Blocking inside `findByUsername` defeats the reactive model and can starve the event loop. - Distinguishing 'user not found' from 'wrong password' in responses enables **user enumeration** — Spring deliberately treats both as bad credentials. ## When to use Use `ReactiveUserDetailsService` for classic username/password auth in a reactive app. For token-based (JWT/OAuth2 resource server) auth you typically don't need it — you configure `oauth2ResourceServer` and a `ReactiveJwtDecoder` instead.

  • Which authentication manager consumes a ReactiveUserDetailsService by default?
    UserDetailsRepositoryReactiveAuthenticationManager — it calls findByUsername and verifies the password with the PasswordEncoder.
  • What must findByUsername return when the user does not exist, and why?
    Mono.empty(). Spring converts the empty signal into an authentication failure (bad credentials), avoiding user enumeration by not distinguishing missing-user from wrong-password.

saying these in an interview costs you the question

  • Thinking ReactiveUserDetailsService itself authenticates (it only loads the user).
  • Returning Mono.error or throwing instead of Mono.empty() for a missing user.
  • Omitting the PasswordEncoder bean or storing plaintext passwords.
  • Calling .block() inside findByUsername.

context