Guide to UserDetailsService in Spring Security

Learn about the role of UserDetailsService, default configuration provided by Spring security and further customizing it according to requirements.

1. UserDetailsService in Spring Security Architecture

The below given picture shows the main actors in the Spring Security architecture and the relationships among them. This architecture is the core concept of implementing authentication with Spring Security.

As shown in the image, the AuthenticationFilter delegates the authentication request to the AuthenticationManager which uses AuthenticationProvider to process authentication.

The AuthenticationProvider uses UserDetailsService that implements the user management responsibility. Its primary responsibility is to find a user by its username from the cache or underlying storage.

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

2. Default Implementation of UserDetailsService

A default implementation of the AuthenticationProvider uses the default implementations provided for the UserDetailsService and the PasswordEncoder.

The default implementation of UserDetailsService only registers the default credentials in the memory of the application. These default credentials are "user" with a default password that’s a randomly generated universally unique identifier (UUID) written to the application console when the spring context loads.

Using generated security password: 78nh23h-sd56-4b98-86ef-dfas8f8asf8

Note that UserDetailsService is always associated with a PasswordEncoder that encodes a supplied password and verifies if the password matches an existing encoding. When we replace the default implementation of the UserDetailsService, we must also specify a PasswordEncoder.

3. In-memory UserDetailsService

The first very basic example of overriding the UserDetailsService is InMemoryUserDetailsManager. This class stores credentials in the memory, which can then be used by Spring Security to authenticate an incoming request.

A UserDetailsManager extends the UserDetailsService contract. Beyond the inherited behavior, it also provides the methods for creating a user and modifying or deleting a user’s password. The UserDetailsService is only responsible for retrieving the user by username.

@Configuration
public class AppConfig {

  @Bean
  public UserDetailsService userDetailsService() {
    var userDetailsService =
        new InMemoryUserDetailsManager();

    var user = User.withUsername("user")
            .password("password")
            .authorities("USER_ROLE")
            .build();

    userDetailsService.createUser(user); 

    return userDetailsService;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

The above configuration registers the beans of type UserDetailsService and PasswordEncoder into the spring context and the authentication provider uses them automatically. Spring allows us to set the user service and password encoder directly to the authentication manager if we prefer to do so.

The previous configuration can be re-written as follows:

@Configuration
class AppConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws  Exception {

    var userDetailsService = new InMemoryUserDetailsManager();

    var user = User.withUsername("user")
        .password("password")
        .authorities("USER_ROLE")
        .build();

    userDetailsService.createUser(user);

    auth.userDetailsService(userDetailsService)
        .passwordEncoder(NoOpPasswordEncoder.getInstance());
  }
}

We can verify the above configuration with a simple test.

@Test
public void expectOKResponse_WhenAuthenticaionManagerIsTestedWithCorrectDetails() {
  AuthenticationManager authenticationManager = this.spring.getContext()
    .getBean(AuthenticationManager.class);

  Authentication authentication = authenticationManager
    .authenticate(UsernamePasswordAuthenticationToken
      .unauthenticated("user", "password"));

  assertThat(authentication.isAuthenticated()).isTrue();
}

Note that InMemoryUserDetailsManager isn’t meant for production-ready applications. Use this class only for examples or proof of concepts.

4. Database-backed UserDetailsService

To store and retrieve the username and passwords from a SQL database, we use JdbcUserDetailsManager class. It connects to the database directly through JDBC.

By default, it creates two tables in the database:

  • USERS
  • AUTHORITIES

The default schema file is in the file users.ddl. The file location is given in the constant JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION.

To customize the default schema and create initial users, we can write our own schema.sql and data.sql files and place them in the application’s /src/main/resources folder. Spring Boot automatically runs these files when we start the application.

Note that the JdbcUserDetailsManager needs a DataSource to connect to the database so we need to define it as well.

@EnableWebSecurity
public class AppSecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean
  public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
      .setType(EmbeddedDatabaseType.H2)
      .addScript(JdbcDaoImpl.DEFAULT_USER_SCHEMA_DDL_LOCATION)
      .build();
  }

  @Bean
  public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {

    UserDetails user = User
      .withUsername("user")
      .password("password")
      .roles("USER_ROLE")
      .build();

    JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
    users.createUser(user);
    return users;
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
  }
}

We can also write custom SQL queries to fetch the user and authorities’ details if we are using a custom DDL schema that uses different table or column names.

@Bean
public UserDetailsService jdbcUserDetailsService(DataSource dataSource) {
  String usersByUsernameQuery = "select username, password, enabled from tbl_users where username = ?";
  String authsByUserQuery = "select username, authority from tbl_authorities where username = ?";
      
  JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);

  userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
  userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);

  return users;
}

5. Conclusion

In this tutorial, we learned about the basic architecture of spring security-based authentication. We learned about the contract that spring security expects from UserDetailsService and PasswordEncoder interfaces.

We learned how UserDetailsManager extends UserDetailsService and provides the capability to create users and modify the passwords.

We also learned the initial defaults and basic customizations of InMemoryUserDetailsManager and JdbcUserDetailsManager classes.

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode