So far we have only dealt with hardcoded users that were stored in memory. The latest applications often support the registration process and store user credentials with their associated roles/authorities permanently in a database.

In this topic, you’ll learn how to connect a custom user store that will be used by Spring Security in the authentication process. When a user tries to access an app with a username and a password, Spring Security will check if the user is present in the store and their credentials are correct. Based on the result, it will decide if the access should be granted or not. We won’t use a database to store users; instead, they will be stored in a thread-safe Map. Understanding how this works will allow you to connect any user store (a file, a database, an external server). Our program will also have a simple registration endpoint – POST /register receives username, password, and role, and saves them in the user store.

We will start by revising the pieces that should be already familiar to you and then deal with new features.

Initial setup

Let’s assume that we already created a Spring Boot project with web and security dependencies included.

The first thing we’ll implement is a controller that will allow us to check that our program works as intended. The controller has one endpoint GET /test available only to authenticated users with the ROLE_USER role. Here is the controller:

@RestController
public class TestController {

    @GetMapping("/test")
    public String test() {
        return "/test is accessed";
    }
}

Now we will deal with the security config. The code below shows our implementation of WebSecurityConfigurerAdapter where we specify that /test requires the ROLE_USER role, make /register available to everyone, provide a bean with a password encoder, and specify some additional configurations.

@EnableWebSecurity
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/test").hasAnyRole("USER")
                .anyRequest().permitAll() // make remaining endpoints public (including POST /register)
                .and()
                .csrf().disable() // disabling CSRF will allow sending POST request using Postman
                .httpBasic(); // enables basic auth.
    }

    @Bean
    public PasswordEncoder getEncoder() {
        return new BCryptPasswordEncoder();
    }
}

The code above is part of our security config. As for authentication, we will configure it a bit later.

As we mentioned before, we’ll store user data in a thread-safe Map. The keys for the Map will be usernames, and the values will be objects of the User class that has 3 fields: username, password, role. Here is the User class:

class User {
    private String username;
    private String password;
    private String role; // should be prefixed with ROLE_

    // constructors, getters and setters
}

The Map with users will be located in a separate class named UserRepository:

@Component
public class UserRepository {
    final private Map<String, User> users = new ConcurrentHashMap<>();

    public User findUserByUsername(String username) {
        return users.get(username);
    }

    public void save(User user) {
        users.put(user.getUsername(), user);
    }
}

As you can see, our user repository also has two convenience methods. The first method returns a user by username or null if the user wasn’t found. The second method receives a User object and adds it to the Map. You’ll see why we added these methods soon.

Now we will deal with the registration. Here is our registration controller with the POST /register endpoint that receives username, password, and role:

@RestController
public class RegistrationController {
    @Autowired
    UserRepository userRepo;
    @Autowired
    PasswordEncoder encoder;

    @PostMapping("/register")
    public void register(@RequestBody User user) {
        // input validation omitted for brevity

        user.setPassword(encoder.encode(user.getPassword()));

        userRepo.save(user);
    }
}

As shown above, we inject UserRepository and PasswordEncoder in the controller. The register method receives an instance of User class that stores the inputted username/password/role. Then we encode a password and use the save method of the UserRepository to save an instance of User in the Map.

That took quite a few lines of code, so before continuing to implement our program, let’s talk a little about the current state of the program. We implemented the /test endpoint and made it available only to authenticated users with the ROLE_USER role. We also have the registration endpoint that receives username/password/role and stores them in the user store. If we run the program at this step, register one user, and try to access the /test endpoint with the username and password we used to register, we will fail to do that and get the 401 Unauthorized status code. That’s because we haven’t configured the authentication: Spring Security doesn’t know about our user store and can’t confirm that the inputted credentials are valid. To make this work, we need to connect our user store to Spring Security.

Our first step will be writing a class that will be used to store and transfer user information from the user store to Spring Security. The class should implement the UserDetails interface because Spring Security can only recognize users of this type.

UserDetails

The UserDetails interface has 7 quite self-explanatory methods that we need to implement. Here are the first three:

  • String getUsername() returns the username used to authenticate the user.
  • String getPassword() returns the password used to authenticate the user.
  • Collection<GrantedAuthority> getAuthorities() returns the authorities and roles granted to the user.

Note that roles and authorities are stored together in one container and have the same type named GrantedAuthority (interface). To convert a String role/authority to GrantedAuthority we can use the SimpleGrantedAuthority class that implements the interface and has a constructor that receives a role/authority of the String type, for example, new SimpleGrantedAuthority("ROLE_USER").

Also, now it should be more clear why we prefix a role with ROLE_. For the framework the difference between a role and an authority is minimal and they are stored together. To tell them from one another we prefix a role with ROLE_. If there is no prefix, we’re dealing with an authority.

All remaining methods of the UserDetails interface enable or disable the account for different reasons:

  • boolean isEnabled() indicates whether the user is enabled or disabled.
  • boolean isAccountNonExpired() indicates whether the user’s account has expired.
  • boolean isAccountNonLocked() indicates whether the user is locked or unlocked.
  • boolean isCredentialsNonExpired() indicates whether the user’s credentials (password) have expired.

Not all applications have accounts that expire or get locked under certain conditions. If you don’t need to implement these functionalities in your application, you can simply make these four methods return true, which means that the users are active. In this topic, we will not implement this functionality.

Now let’s provide our implementation of the UserDetails interface. As we’ve mentioned before, the class that implements the interface will be used to store and transfer core user information from the user store to Spring Security. In the UserRepository the info about one user is stored in a User object with 3 fields. Now we need to provide a converter from User to UserDetails. We will do it in a constructor that will receive a User object and store its content. Here is the implementation:

public class UserDetailsImpl implements UserDetails {
    private final String username;
    private final String password;
    private final List<GrantedAuthority> rolesAndAuthorities;
    
    public UserDetailsImpl(User user) {
        username = user.getUsername();
        password = user.getPassword();
        rolesAndAuthorities = List.of(new SimpleGrantedAuthority(user.getRole()));
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return rolesAndAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    // 4 remaining methods that just return true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

As you can see, the first three methods return user info that was retrieved from a User object. All remaining methods just return true, which means that the users are active. Also, note that we converted a String role to SimpleGrantedAuthority that implements the GrantedAuthority interface. For simplicity, we have only one role but big applications can have a lot of roles/authorities, and all of them should be converted to GrantedAuthority.

In simple applications, instead of providing a separate class that implements the UserDetails interface, as we did above, we can use the implementation of the interface that Spring Security provides, namely the User class. The class implements the builder pattern and can make your code more concise. It is located in the org.springframework.security.core.userdetails package. If you want to learn about the User class, here is the link.

Our next step is to implement UserDetailsService that is used to retrieve user data from storage.

UserDetailsService

This interface has only one method that we need to override:

  • UserDetails loadUserByUsername(String username) throws UsernameNotFoundException receives a username and returns a UserDetails object.

This method will be used by Spring Security when someone tries to authenticate. In the method, we need to retrieve user data by username from the storage and convert that data to UserDetails. If the user with a specified username is not found, we throw UsernameNotFoundException. Here is our implementation of UserDetailsService:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    UserRepository userRepo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepo.findUserByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException("Not found: " + username);
        }

        return new UserDetailsImpl(user);
    }
}

As you can see, we injected UserRepository to use it in the loadUserByUsername method to retrieve user info. Also, note that the class is annotated with the @Service annotation.

Why do we retrieve user data just by username without checking the password or authorities/roles? The answer is they will be checked automatically by Spring Security. Our only task is to return the UserDetails object that stores user info. It doesn’t matter how we convert user data to UserDetails, and where our users are stored.

There is one more step left before we can run and test our program.

AuthenticationManagerBuilder

We’ve used the inMemoryAuthentication() method of AuthenticationManagerBuilder to configure an in-memory user store (in other words, an authentication provider). Apart from an in-memory user store, the builder also allows for configuring a JDBC-based user store, an LDAP-backed user store, allows for connecting custom user details service, and has some additional methods. In this topic we are interested in the userDetailsService() method of AuthenticationManagerBuilder, which is used to connect custom UserDetailsService. We need to pass the instance of our UserDetailsService to this method.

We can also have multiple user stores (authentication providers) at once. For example, we can configure an in-memory user store with a couple of users and connect 2 different databases with user data.

The code below is the missing piece in our WebSecurityConfigurerImpl class that we implemented earlier. Here we override one more configure method that receives AuthenticationManagerBuilder and connect our UserDetailsService. Additionally, we configure an in-memory user store.

@EnableWebSecurity
public class WebSecurityConfigurerImpl extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(userDetailsService) // user store 1
                .passwordEncoder(getEncoder());

        auth
                .inMemoryAuthentication() // user store 2
                .withUser("Admin").password("hardcoded").roles("USER")
                .and().passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    // the second configure method that receives HttpSecurity

    // password encoder
}

Here we injected UserDetailsService and passed it to the userDetailsService method. Also, note that our app uses two different password encoders. The first user store uses BCryptPasswordEncoder and the second one uses NoOpPasswordEncoder that does nothing and returns the password as it was.

What happens in the authentication process when a user provides a username and a password? The simple answer is the username is passed to the loadUserByUsername method. The method checks if a user with the provided username is present in the store (when multiple user stores are defined, they will be queried in the order they’re declared). If the user is not found, UsernameNotFoundException is thrown and access is denied: we see the 401 Unauthorized status code. If the user is found in the user store, user data is retrieved, converted to UserDetails, and returned. After that, PasswordEncoder and its method matches will be used automatically to check if the inputted raw password matches the stored encoded one, and then the roles are compared. If everything is correct, access is granted.

How can we connect a database instead of a Map? There are a lot of ways to do it. In our case, we can remove the Map with user data and connect the findUserByUsername and save methods of UserRepository with a database. To learn how to work with databases, see our topics on Spring Data.

If you are using the H2 database in your app and want to use the H2 console (or something similar), you need to unblock it by disabling CSRF protection and X-Frame-Options (prevents clickjacking attacks). It can be done by calling the following methods on the HttpSecurity object: .csrf().disable().headers().frameOptions().disable(). Depending on your implementation, you may also need to append a call to .and() before the methods. Also, make sure the H2 console URLs aren’t blocked by Spring Security.

Now let’s run our program and test it using Postman.

Running the app

Our first step is to populate the user store with data using a public POST /register endpoint. Let’s create two users. The username and password of the first user are user1 and pass1. The username and password of the second user are user2 and pass2. Both users have the same role, ROLE_USER. Here is an example with the first user:

Now let’s try to access the /test endpoint protected with basic auth using the username/password of one of the registered users. Here is an example with the second user:

As we can see, we were able to access the endpoint and the status code is 200 OK. So it looks like our registration endpoint works as intended and UserDetails and UserDetailsService are implemented properly.

Additionally, let’s try to access the /test endpoint using a hardcoded user:

As shown here, this also works as intended.

Conclusion

In this topic, you’ve seen how we can attach a custom user store to Spring Security by implementing the UserDetails and UserDetailsService interfaces. UserDetails represents a user and stores core user information. UserDetailsService is used to retrieve user-related data from a user store and has only one method that receives a username and returns UserDetails or throws an exception if the user isn’t found. Additionally, we’ve implemented a simple registration endpoint that populates the user store with data, created one hard-coded user, and successfully tested the program.

If you’re familiar with Spring Data, you should know how to connect a database instead of a Map. If you’re not familiar with this Spring module yet, you can use a file to store user data permanently. Feel free to experiment with this.

Leave a Reply

Your email address will not be published.