SpringBoot creating UserDetailsService authentication

There are plenty of online tutorials showing how to create database-based authentication for Spring. Some of them use SQL query to authenticate user and retrieve its roles, some use DAO… but none of them worked well for me and all of them had some major problems, even like SQL Injection. So, in this post I will explain my approach and present final solution with a database (MySQL), User and Role class and UserDetailsService implementation.

The goal is to create basic webpage with login form and signup form (which includes fields validation) that handles different roles. Then, you and me can use it as a template project.

This tutorial doesn’t include steps how to setup your IDE and build environment. We’re going straight to code, and I will try to avoid as much boilterplate as possible, so the code won’t include getters and setters. If you still write them by hand, time to learn about Lombok project (which I use here) or any other code generator.

So, there it is.

Spring UserDetailsService uses UserDetails and GrantedAuthority classes, but we have to extend them to add features we need.

 

User class implementation

Let’s start from implementing our User class. The class must implement UserDetails (from Spring, UserDetails already implements Serializable, so you don’t have to do it again).

UserDetails interface enforces writing implementation of these methods:

Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();

We need to add corresponding fields to our class and first version of the User class is as below:

@Data
@Entity
@Table(name = "appuser")
@NoArgsConstructor
public class User implements UserDetails {
 
    private static final long serialVersionUID = 3709949243021685681L;
 
    @Id
    @Column(name = "user_id", nullable = false, updatable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(nullable = false, unique = true, length = 20)
    @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters long")
    private String username;
 
    @Column(nullable = false)
    private String password;
 
    @Column(nullable = false, unique = true)
    @Email(message = "Incorrect email format")
    private String email;
 
    @ManyToMany(cascade = CascadeType.MERGE)
    @JoinTable(name = "appuser_role",
            joinColumns = @JoinColumn(name = "appuser_id"),
            inverseJoinColumns = @JoinColumn(name = "approle_id"))
    private List authorities;
 
    @Column
    private boolean isAccountNonExpired;
 
    @Column
    private boolean isAccountNonLocked;
 
    @Column
    private boolean isCredentialsNonExpired;
 
    @Column
    private boolean isEnabled = true;

This class will contain password encrypted by PasswordEncoder, so it doesn’t contain validation. Role class is our next class to implement. All other fields should be self-explanatory, if they’re not, please leave a comment.

 

Role implementation

Second class that’s enforced by Spring is implementation of GrantedAuthority. You can just skip this part and use SimpleGrantedAuthority, but I want to have type-control over created and persisted authorities. SimpleGrantedAuthority uses String as authority name, which I think is not enough to control authorities; it’s too easy to make a typo or forget about “ROLE_” prefix… Oh yes, Spring wants us to use a prefix in authority name, by default it’s “ROLE_”, so we will stuck to that.

@Data
@Entity
@Table(name = "approle")
@NoArgsConstructor
public class Role implements GrantedAuthority {
 
    private static final long serialVersionUID = 6410095741748080177L;
 
    private static final String PREFIX = "ROLE_";
 
    @Id
    @Column(name = "approle_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @NotEmpty
    @Column(unique = true, nullable = false)
    private String authority;
 
    @ManyToMany(mappedBy = "authorities")
    private Set users = new HashSet<>(0);
 
    public Role(RoleEnum authority) {
        this.authority = PREFIX + authority.toString();
    }
 
    @Override
    public String toString() {
        return authority;
    }
 
}

You should notice now what one thing is missing. It’s RoleEnum. That’s what I meant by type-control. When you pass a String to Role constructor, you need to to know if you need to pass “ROLE_ADMIN” or just “ADMIN” or maybe “admin”. When I control roles by Enum, I always know what I need to pass. What you can also see here is many-to-many relationship between User and Role classes.

The RoleEnum contains hardcoded role names, so it’s not possible to add another one on runtime. That’s a disadvantage of this approach, just wanted to make you aware of it.

public enum RoleEnum {
    ADMIN,
    USER
}

 

Communication with database

Another important thing in database-based authentication is to use database, obviously. Spring offers great interfaces like CrudRepository or JpaRepository that generate “SQL” query from method name. Basically, you just write a method definition: No need to write implementation of that method or SQL! Checkout more queries you can generate from templates here.

To save and retrieve user from database we need to define our interface that works around User and Roles.
Let’s call the first one UserRepository:

interface UserRepository extends CrudRepository<User, Long> {
 
    User findByEmail(String email);
 
    User findByUsername(String userName);
 
}

and the second one RoleRepository:

interface RoleRepository extends JpaRepository<Role, Long> {
 
    Role findByAuthority(String authority);
 
}

The repositories are not ready and should not be used without wrapper classes. Repository doesn’t provide any validation or access to database session; it’s unable to handle exceptions; it just generates and passes queries to database drivers; it does only CRUD operations. To solve the problem, we need to implement User and Role services.

The services will be @Transactional. Services are places where you should add validation like de-duplication, creating hibernate-proxied objects. That’s a very basic RoleService that will work for us:

Service
@Transactional
public class RoleService {
 
    @Autowired
    private RoleRepository roleRepo;
 
    public List findAll() {
        return roleRepo.findAll();
    }
 
    private Role findByAuthority(@NotNull String authority) {
        return roleRepo.findByAuthority(authority);
    }
 
    public Role findByAuthority(@NotNull RoleEnum authority) {
        return roleRepo.findByAuthority(authority.toString());
    }
 
    public Role findOne(@NotNull Long id) {
        return roleRepo.findOne(id);
    }
 
    public Role save(@NotNull Role role) {
        return roleRepo.save(role);
    }
 
    public Role createRoleIfNotFound(RoleEnum roleEnum) {
        Role role = this.findByAuthority(roleEnum.toString());
        if (role == null) {
            role = new Role(roleEnum);
            this.save(role);
        }
        return role;
    }
}

Our UserService will be very similar:

@Service
@Transactional
public class UserService {
 
    private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
 
    @Autowired
    private UserRepository userRepo;
 
    @Autowired
    private RoleService roleService;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    public Long registerNewUser(User user) {
        user.setPassword(passwordEncoder.encode(user.getPassword()));
 
        final Role USER_ROLE = roleService.findByAuthority(RoleEnum.USER);
        user.setAuthorities(Collections.singletonList(USER_ROLE));
 
        return this.save(user);
    }
 
    public Long save(User user) {
        try {
            user.setPassword(passwordEncoder.encode(user.getPassword()));
            userRepo.save(user);
            return user.getId();
        } catch (Exception e) {
            LOGGER.error(e.toString(), e);
            return -1L;
        }
    }
 
    public Iterable findAll() {
        return userRepo.findAll();
    }
 
    public User findByEmail(String email) {
        return userRepo.findByEmail(email);
    }
 
    public User findByUsername(String username) {
        return userRepo.findByUsername(username);
    }
 
    public User findById(Long id) {
        return userRepo.findOne(id);
    }
 
}

0You can see there that method registerNewUser automatically creates User entity with role USER. User with role ADMIN must be created “manually”. We will come to this later in Application.java code.

UserDetailsService implementation

UserDetailsService is used by AuthenticationManagerBuilder to authenticate users. Forget about the best answer you saw on StackOverflow that promotes writing SQL for that…
Our class will just extend basic `UserDetailsService`, implement one method and that’s it.

@Component
public class CustomUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserService userService;
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        final User user = userService.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("No such user in database");
        }
        return user;
    }
 
}

Internally spring can use multiple authentication services. If authentication fails, it should throw `UsernameNotFoundException` to notify AuthenticationManager to use another defined authentication source, like LDAP or in-memory datasource.

Before we jump to WebSecurityConfig, we just need to add custom authentication provider. This one is used to build your own User-class object and validate it with our custom fields. Otherwise authentication provider would return you UserDetails object which is not compatible with existing implementation.
Authentication provider is a place where you validate password, roles, username, password expiry dates and all other things that happen before user actually logs-in.

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
 
    @Autowired
    private UserService userService;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        final String username = (String) authentication.getPrincipal();
        final String password = (String) authentication.getCredentials();
 
        User user = userService.findByUsername(username);
        if (user == null) {
            return null; // next auth provider will be tested
        }
        if (!user.isEnabled()) {
            throw new DisabledException("User is disabled");
        }
        if (!passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("Incorrect username or password");
        }
        return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
    }
 
    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

That’s done! In the next step we will configure security filter.

Security filter

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    private static final String USER = RoleEnum.USER.toString();
 
    private static final String ADMIN = RoleEnum.ADMIN.toString();
 
    @Autowired
    private CustomUserDetailsService userDetailsService;
 
    @Autowired
    private CustomAuthenticationProvider authProvider;
 
    @Autowired
    private PasswordEncoder passwordEncoder;
 
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception {
        authenticationManagerBuilder
                .authenticationProvider(this.authProvider)
                .userDetailsService(this.userDetailsService)
                .passwordEncoder(this.passwordEncoder);
    }
 
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder)
            throws Exception {
        authenticationManagerBuilder
                .authenticationProvider(this.authProvider)
                .userDetailsService(this.userDetailsService)
                .passwordEncoder(this.passwordEncoder);
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //@formatter:off
        http
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .failureUrl("/login?error=true")
                .permitAll()
        .and()
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login")
                .permitAll()
        .and()
        .authorizeRequests()
            .mvcMatchers("/").permitAll()
            .mvcMatchers("/signup").permitAll()
            .mvcMatchers("/logout").authenticated()
            .mvcMatchers("/users").hasAnyRole(ADMIN)
            .mvcMatchers("/error", "/thisOneThrowsExceptionToTestExceptionHandling").permitAll()
            .anyRequest().permitAll();
        //@formatter:on
    }
}

Hurray! That’s done! You just probably got what you wanted from this tutorial. That’s also probably the place where you stop reading and copying, but our goal was to make a complete application with login, signup! That’s correct. You can find my complete application with some tests on Gitlab : )
It contains:

  • webpages for login, signup and home page written in Thymeleaf
  • gradle project
  • SignupUser class that is validated on Signupdate with basic Hibernate and custom rules
  • application.properties suited for thymeleaf and with jdbc sources
  • gradle plugins to find updates and vulnerabilities in dependencies
  • tests
  • jacoco configuration
  • Log4J configuration