Spring Boot Security With UserDetailsService and Authentication Provider
while working with spring boot security your have to deal with authentication and authorization and to perform these operations you need a user with username, password and authorities.
if you are working with spring boot application with spring boot security just for testing purpose it is fine to work with InMemory database or providing user details in application.properties file but if your are dealing with application which is not for just testing purpose you need a way to store the user details i.e. username, password and user roles in permanant place i.e Database it can be Oracle, Postgresql, MySQL etc.
to understand how the UserServiceDetails is used for authentication you can refer to below diagram as well as below blog mentioned.
Blog to Understand Security Flow : https://medium.com/@wankhedeshubham/spring-boot-security-flow-dbc3d51b0f2
let’s understand the same with an example,
from above diagram we can understand that we need UserDetailsService, UserRepository and our UserDetailsService implementation should return the UserDetails which can constructed using User object, so let’s follow the same approach but before that we need some other basic components
I’m having a rest application with rest controller which has two endpoints to add and fetch employee, along with existing two endpoints we are going to add one new endpoint to add the user which we are going to use for authentication.
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService empService;
@Autowired
private UserInfoServiceImpl userInfoService;
@GetMapping("/emps")
public ResponseEntity<List<Employee>> getAllEmps(){
return new ResponseEntity<>(empService.getAllEmps(), HttpStatus.OK);
}
@PostMapping(value = "/emps/add", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<EmployeeModel> addEmp(@RequestBody EmployeeModel employee, HttpServletRequest request){
EmployeeModel emp = empService.addEmp(employee);
return new ResponseEntity(emp,HttpStatus.CREATED);
}
@PostMapping("/users/add")
public ResponseEntity<?> addUser(@RequestBody UserInfo userInfo){
userInfoService.addUser(userInfo);
return new ResponseEntity<>(HttpStatus.OK);
}
}
the User class which we are going to add through addUser endpoint method has four fields having same datatype as String, they are id, username, password and roles, roles can be passed comma separated.
@Entity
@Data
public class UserInfo{
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(unique = true)
private String username;
@Column
private String password;
@Column
private String roles;
}
along with above entity class you will need a repository interface extending the Spring Data JPA repository interface.
public interface UserInfoRepository extends JpaRepository<UserInfo,String> {
Optional<UserInfo> findByUsername(String username);
}
findByUsername(-) method is needed because it we are going to pass username and password for authentication.
Creating UserDetailsService Implementation
if you look at the UserDetailsService interface here you will notice one abstract method.
//pre-defined interface from spring security
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
this abstract method has loadUserByUsername(-) method which expect us to return the instance of UserDetails class
so we have to create a model class which implements the UserDetails class which we then return from UserDetailsService class implementation.
UserDetailsModel.java
public class UserDetailModel implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorities;
public UserDetailModel(UserInfo user){
this.username = user.getUsername();
this.password = user.getPassword();
this.authorities = Stream.of(user.getRoles().split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
@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 false; }
@Override
public boolean isAccountNonLocked() { return false; }
@Override
public boolean isCredentialsNonExpired() { return false; }
@Override
public boolean isEnabled() { return false; }
}
UserDetailsServiceImpl.java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserInfoRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<UserInfo> user = userRepository.findByUsername(username);
return user.map(UserDetailModel::new).orElseThrow(()->new UsernameNotFoundException("Invalid Username"));
}
}
once we are done with UserDetails implementation and UserDetailsService implementation it is time to write spring security configuration.
In security configuration we are going allow access to /users/** api’s to ADMIN only and /emp/** to all authenticated users.
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/users/**")
.hasRole("ADMIN")
.and()
.authorizeHttpRequests()
.requestMatchers("/emps/**")
.authenticated().and()
.http()
.and()
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
one more thing which we should remember while add any new user i.e. we should encode the password of user and for that we can user above BCryptPasswordEncoder encoder and same can be returned through @Bean method.
our custom implementation of UserDetailsService implementation can also return through @Bean method which the can be use by AuthenticationProvider to call loadUserByUsername(-) method to authenticate it.
Until this point for most of you it should be good to test application and successfully able to add users and use those users to make /emps/** calls
but unfortunately for me it’s is not working, whenever I’m trying to make a call to add user it is giving me No Authentication Provider found,
Note: if you are making call though postman you can’t see above error so I’m showing it though browser
so what should we do now ? worry not.
we have two approaches we can follow here .
1. use predefined authentication provider and then make it use our UserDetailsService class implementation (DaoAuthenticationProvider)
2. we can create custom Authentication Provider by implementing the AuthenticationProvider interface.
let’s understand both of them
1. user pre-defined authentication provider and make it use our implementation of UserDetailsService class
if we want to use pre-defined AuthenticationProvider which can authenticate our User from database we can use DaoAuthenticationProvider and we register our user details service and password encoder to it
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/users/**")
.hasRole("ADMIN")
.and()
.authorizeHttpRequests()
.requestMatchers("/emps/**")
.authenticated().and()
.httpBasic()
.and()
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService());
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
}
2. create custom Authentication Provider by implementing the same AuthenticationProvider interface.
in this approach we have to implement our class with AuthenticationProvider interface and override it’s two abstract method
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try{
UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName());
return new UsernamePasswordAuthenticationToken(userDetails.getUsername(),userDetails.getPassword(),userDetails.getAuthorities());
}catch (UsernameNotFoundException e){
throw new BadCredentialsException("Invalid Credentials");
}
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}
here implementation of support method plays an important role to make Provider Manager to recognise our custom authentication provider
your next step is to tell your security config about this new AuthenticationProvider implementation
change your SecurityConfig.java to register authentication provider
@EnableWebSecurity
@Configuration
public class SecurityConfig {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.csrf().disable()
.authorizeHttpRequests()
.requestMatchers("/users/**")
.hasRole("ADMIN")
.and()
.authorizeHttpRequests()
.requestMatchers("/emps/**")
.authenticated().and()
.httpBasic()
.and()
//CustomAuthenticationProvider
.authenticationProvider(authenticationProvider)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
you can use any one of the 2 approaches and you are good to go
Testing [Postman API calls]
1. AddUser
We are successfully able to add user, here I’m using a user with admin role which I created previously and for which username and password can be provided through Authorization tab there select Basic Auth and provider credentials
2. Fetch All Employees using created user credentials.
Here you can see we are able to make a /emp call to fetch emp records with newly created user.
Hope you enjoyed the blog.