Themes

Select a theme

Spring Security - JPA implemenation of UserDetailsService For DaoAuthenticationProvider

Spring security provides DaoAuthenticationProvider for username and password authentication.

Spring Security provides DaoAuthenticationProvider which requires a UserDetailsService and a passwordEncoder bean to perform username and password authentication.

Please note — we will use a spring boot project. You can access the maven dependency here to initialize the project.

This tutorial will focus on -

  • How to leverage Spring Data JPA to connect to a persistent database such as MySQL
  • How to create UserDetails Entity using JPA annotations.
  • How to create custom UserDetailsManager implementation which fetches data using JPA repository.

  

Configure the dataSource

Add the below configuration in the application.properties file. I have provided a demo config for configuring a MySQL database. you are free to use a database of your choice. Make sure you give the correct url, username and password.

spring.datasource.url=jdbc:mysql://localhost:3306/nauth_db
spring.datasource.username=user
spring.datasource.password=pass

  

Create an Entity of UserDetail type

Let’s implement UserDetails interface and implement all the methods.

@Entity
public class AuthUserDetails implements UserDetails {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String username;
  private String password;
  private boolean accountNonExpired;
  private boolean accountNonLocked;
  private boolean credentialsNonExpired;
  private boolean enabled;

  @OneToMany(mappedBy = "authUserDetails", fetch = FetchType.EAGER)
  private Set<AuthGrantedAuthority> authorities;

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

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

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

  @Override
  public boolean isAccountNonExpired() {
    return this.accountNonExpired;
  }

  @Override
  public boolean isAccountNonLocked() {
    return this.accountNonLocked;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return this.credentialsNonExpired;
  }

  @Override
  public boolean isEnabled() {
    return this.enabled;
  }

//setters 
}

   Since UserDetails can have a set of authorities. we will also need another entity for GrantedAuthorities to maintain a one-to-many relationship.

@Entity
public class AuthGrantedAuthority implements GrantedAuthority {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id", nullable = false)
  private Long id;

  private String authority;

  @ManyToOne
  @JoinColumn(name = "auth_user_detail_id")
  private AuthUserDetails authUserDetails;

		//constructors
		//getters and setters

}

  

Create the Repositories for the above entities.

We will also have to create a repository for both UserDetails and GrantedAuthorities Entities which implements JpaRepository interface.

findByUsername and findByPassword methods are required later when we will create a Custom UserDetailsManager

@Repository
public interface AuthUserDetailsRepository extends JpaRepository<AuthUserDetails, Long> {
  Optional<AuthUserDetails> findByUsername(String username);
  Optional<AuthUserDetails> findByPassword(String password);
}

The repository interface for GrantedAuthorities does not require any custom methods.

public interface AuthGrantedAuthorityRepository extends JpaRepository<AuthGrantedAuthority, Long> {
}

  

Create an implementation of UserDetailsManager

UserDetailsManager interface extends to UserDetailsService class. So we are ideally creating an implementation of UserDetailsService with some extra methods.

@Service
public class JpaUserDetailsManager implements UserDetailsManager {

  @Autowired
  private AuthUserDetailsRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    repository.findByUsername(username);
    .orElseThrow(
      () -> new UsernameNotFoundException("No user found with username = " + username));
  }

  @Override
  public void createUser(UserDetails user) {
    repository.save((AuthUserDetails) user);
  }

  @Override
  public void updateUser(UserDetails user) {
    repository.save((AuthUserDetails) user);
  }

  @Override
  public void deleteUser(String username) {
    AuthUserDetails userDetails = repository.findByUsername(username)
      .orElseThrow(() -> new UsernameNotFoundException("No User found for username -> " + username));
    repository.delete(userDetails);
  }

  /**
   * This method assumes that both oldPassword and the newPassword params
   * are encoded with configured passwordEncoder
   *
   * @param oldPassword the old password of the user
   * @param newPassword the new password of the user
   */
  @Override
  @Transactional
  public void changePassword(String oldPassword, String newPassword) {
    AuthUserDetails userDetails = repository.findByPassword(oldPassword)
      .orElseThrow(() -> new UsernameNotFoundException("Invalid password "));
    userDetails.setPassword(newPassword);
    repository.save(userDetails);
  }

  @Override
  public boolean userExists(String username) {
    return repository.findByUsername(username).isPresent();
  }
}

Next , we will create a configuration which will extend to WebSecurityConfigurerAdapter. We will also add JpaUserDetailsManager as a dependency to it.

We will also add passwordEncoder bean to it.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
  private JpaUserDetailsManager jpaUserDetailsManager;

@Bean
  public BCryptPasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(4);
  }

}

  

Create a bean of DaoAuthenticationProvider

@Autowired
private JpaUserDetailsManager jpaUserDetailsManager; 

@Bean
  public DaoAuthenticationProvider jpaDaoAuthenticationProvider() {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(jpaUserDetailsManager);
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    return daoAuthenticationProvider;
  }

Finally, override the configure(AuthenticationManagerBuilder auth) of the WebSecurityConfigurerAdapter class and add the jpaDaoAuthenticationProvider bean to AuthenticationManagerBuilder.

@Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(jpaDaoAuthenticationProvider());
  }

  

Configure multiple AuthenticationProviders

If you have come across a scenario where you would require to search different data stores for user details for authentication. You can create the custom implementations of UserDetailsManager as we did in this post.

After that you can add multiple AuthenticationProviders to AuthenticationManagerBuilder in the configure method.

@Override
  protected void configure(AuthenticationManagerBuilder auth) {
    auth.authenticationProvider(jpaDaoAuthenticationProvider());
		auth.authenticationProvider(inMemoryDaoAuthenticationProvider());
		auth.authenticationProvider(firebaseDaoAuthenticationProvider());
  }

You can also go through the post about in-memory username and password authentication using spring security and try adding that AuthenticationProvider in the configure method.

Finally - Add the data to Database in Spring boot application

Since we have not created any users for testing.

you can either —

  • directly add the userdetails in database
  • or use data.sql file in the resources to provide the initial data.
  • or use the commandLineRunner to add data when the application starts.

Below is an example of commandLineRunner bean.

@Configuration
public class DBInitializerConfig {
  @Autowired
  private AuthUserDetailsRepository authUserDetailsRepository;

  @Autowired
  private AuthGrantedAuthorityRepository authGrantedAuthorityRepository;

  @Autowired
  private PasswordEncoder passwordEncoder;

  // initialize the user in DB
  @Bean
  public CommandLineRunner initializeJpaData() {
    return (args)->{
      System.out.println("application started");

      //uncomment if required

      AuthUserDetails user2 = new AuthUserDetails();
      user2.setUsername("user2");
      user2.setPassword(passwordEncoder.encode("password"));
      user2.setEnabled(true);
      user2.setCredentialsNonExpired(true);
      user2.setAccountNonExpired(true);
      user2.setAccountNonLocked(true);

      AuthGrantedAuthority grantedAuthority = new AuthGrantedAuthority();
      grantedAuthority.setAuthority("USER");
      grantedAuthority.setAuthUserDetail(user2);
      authUserDetailsRepository.save(user2);
      authGrantedAuthorityRepository.save(grantedAuthority);
      user2.setAuthorities(Collections.singleton(grantedAuthority));
    };

  }
}

If you have any feedback, let me know in the comments below and don't forget to give a thumbs up.