UserDetailsService : Loading UserDetails from database

In the last post, We have seen how easy it is to set up an in-memory UserDetailsService and dynamically add users to the applications. However, we all know that the implementation is only good for demos and short-lived applications. Once these applications are stopped, All the information about the users are lost. This is why most of the enterprise applications keep their users account information in a database. With that being said, We will see how we can load user details from a database table.

Database entities

In order to load user information from the database, we need to use spring JDBC or spring JPA. For the sake of completeness, I’m using spring JPA and here is a simple UserAccount and UserRole entities.

@Entity
@Data
public class UserAccount {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Column(unique = true)
    private String username;

    private String password;

    private boolean active;

    @OneToMany
    private List<UserRole> userRoles;
}
@Entity
@Data
public class UserRole {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @ManyToOne
    private UserAccount userAccount;

    private String role;
}
  1. Here the UserRole is to show how a single user can have multiple roles (@OneToMany).
  2. The username column is marked as unique due to the nature of how usernames should be. However, it’s up to you on how you want to design the database entries.
  3. I’m using Lombok hence there are no getters and setters.

With the entities ready, let’s write the necessary repository methods for our CustomUserDetailService. The following definition would return a UserAccount entity based on the username.

@Repository
public interface UserAccountRepository extends JpaRepository<UserAccount, Integer> {
    UserAccount findByUsername(String username);
}

UserDetails

The UserDetailsService service interface is supposed to return an implementation of org.springframework.security.core.userdetails.UserDetails. So first we need to define a CustomUserDetails class backed by an UserAccount. Here is how I implemented them. However, it is up to you to implement this class differently if you have to.

public class CustomUserDetails implements UserDetails {

    private final UserAccount userAccount;


    public CustomUserDetails(UserAccount userAccount) {
        this.userAccount = userAccount;
    }

    @Override
    public String getUsername() {
        return userAccount.getUsername();
    }

    @Override
    public String getPassword() {
        return userAccount.getPassword();
    }

    @Override
    public boolean isAccountNonExpired() {
        return userAccount.isActive();
    }

    @Override
    public boolean isAccountNonLocked() {
        return userAccount.isActive();
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return userAccount.isActive();
    }

    @Override
    public boolean isEnabled() {
        return userAccount.isActive();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singletonList(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return "USER";
            }
        });
    }
}

The getAuthorities() method of UserDetails needs a list of GrantedAuthority. For now, I have hardcoded it to return only USER role. Also, I have written a getUserAccount() so that I can use this to get hold of current user entity.

Loading userDetails from database

With all the above set, All we need is a UserDetailService implementation. As we already established our database entities and repositories, let’s write our implementation and mark it a bean with the help of @Component annotation.

@Component
public class DatabaseUserDetailsService implements UserDetailsService {
    private final
    UserAccountRepository userAccountRepository;

    public DatabaseUserDetailsService(UserAccountRepository userAccountRepository) {
        this.userAccountRepository = userAccountRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserAccount userAccount = userAccountRepository.findByUsername(username);
        if (userAccount == null) {
            throw new UsernameNotFoundException("User with username [" + username + "] not found in the system");
        }
        return new CustomUserDetails(userAccount);
    }
}

This is as simple as it can get. The contract for this method is that if the system is not able to find a user for a given username, the method should throw a UsernameNotFoundException along with a message. Once the methods gets a UserAccount record, It is converted into CustomUserDetails and presented to security context.

Register and login

To simulate user creation in the DB, Let’s create a simple registration form and a /register endpoint.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Register a new user</title>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.3/css/bootstrap.min.css"/>
</head>
<body class="container mt-5">
<h1 class="text-center">Register</h1>
<form method="post" action="/register" autocomplete="off">
    <div class="row">
        <div class="col form-group">
            <label for="firstName">First Name</label>
            <input type="text" class="form-control" id="firstName" name="firstName" autocomplete="new-password" required
                   placeholder="First Name">
        </div>
        <div class="col form-group">
            <label for="lastName">Last Name</label>
            <input type="text" class="form-control" id="lastName" name="lastName" autocomplete="new-password" required
                   placeholder="Last Name">
        </div>
    </div>
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" class="form-control" id="username" name="username" autocomplete="new-password" required
               placeholder="Enter username">
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" class="form-control" id="password" name="password" placeholder="Enter password" required
               autocomplete="new-password">
    </div>

    <input type="submit" class="btn btn-primary btn-block btn-lg" value="Register"/>
</form>
</body>
</html>
@RestController
public class UserController {


    private final UserAccountRepository userAccountRepository;
    private final PasswordEncoder passwordEncoder;

    public UserController(UserAccountRepository userAccountRepository, PasswordEncoder passwordEncoder) {
        this.userAccountRepository = userAccountRepository;
        this.passwordEncoder = passwordEncoder;
    }


    @PostMapping("/register")
    public UserAccount register(@RequestParam("username") String username, @RequestParam("password") String password,
                                @RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName) {
        UserAccount userAccount = new UserAccount();
        userAccount.setFirstName(firstName);
        userAccount.setLastName(lastName);
        userAccount.setUsername(username);
        userAccount.setPassword(passwordEncoder.encode(password));
        userAccount.setActive(true);
        return userAccountRepository.save(userAccount);
    }
}

Storing password in DB as plain text is not advised for production deployments. This is why spring boot requires a passwordEncoder to encode and store and validate the password in the database. The current industry standard is to use BCryptPasswordEncoder. You need to defined it as a bean somewhere in your @Configuration class as shown below. You can find details about password encoders in an upcoming post.

    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

To make sure register.html and /register accessible without requiring login, exclude them from web security.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .antMatchers(HttpMethod.GET, "/register.html")
                .antMatchers(HttpMethod.POST, "/register");
    }
}

The Result

With all the above in place let’s test by first registering a user and then logging in using the same application.

For the sake of the demo, I used the h2 embedded database. However, with JPA it doesn’t matter which database you want to use. As long as appropriate jdbc drivers are available and table structure is matching your JPA entities, you are good. 

You can tryout this project yourself by checking out this github repository.

Leave a Comment