Spring Security Basics: Implementing Authentication and Authorization-PART 4

RMAG news

Integrate the database with Spring Security

Up to this point, we have used the default user provided by Spring Security to log in to the application. In the previous sections, we added some sample users to the application_users table. Moving forward, we will use this table for logging in to the application. To achieve this, we need to configure Spring Security to recognize that the details of the application users are stored in the application_users table. This will enable Spring Security to retrieve and verify user credentials during authentication and authorization.

For that let’s do the following steps:

Add the password encoder bean

Update the plain text password to encrypted password

Configure user details service

Configure the authentication provider

Add the password encoder bean

In the SecurityConfig class add a bean of type PasswordEncoder

package com.gintophilip.springauth.web;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity securityConfig) throws Exception {
return securityConfig
.authorizeHttpRequests(auth->
auth.requestMatchers(“/api/hello”)
.permitAll()
.requestMatchers(“/api/admin”).hasRole(“ADMIN”)
.anyRequest().authenticated()
).formLogin(Customizer.withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

The BCryptPasswordEncoder is the preferred method for encoding passwords.

Update the plain text password to encrypted password.

While creating the sample users, the passwords were stored as plain text. In this step, let’s update the passwords to an encrypted form.

You may wonder why user creation is handled in this manner and not through an API. The reason is, to minimize the overhead of creating APIs for every function. The primary goal of this article is to demonstrate the fundamentals of integrating authentication and authorization mechanisms.

Drop the user_roles table

Drop the roles table

Drop the application_user table

Add the PasswordEncoder class dependency in DataBaseUtilityRunner

Encode the password

Drop theuser_rolestable

spring_access_db=# drop table user_roles;

Drop therolestable

spring_access_db=# drop table roles;

Drop theapplication_usertable

spring_access_db=# drop table application_user;

The drop tables command were executed via psql CLI

And also, do the following in DataBaseUtilityRunner class.

update the lines

user1.setPassword(“123456”);
adminUser.setPassword(“12345”);

as follows.

user1.setPassword(passwordEncoder.encode(“123456”));
adminUser.setPassword(passwordEncoder.encode(“12345”));

The DataBaseUtilityRunner after modification is as follows,

package com.gintophilip.springauth;

import com.gintophilip.springauth.entities.ApplicationUser;
import com.gintophilip.springauth.entities.Roles;
import com.gintophilip.springauth.repository.RoleRepository;
import com.gintophilip.springauth.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
public class DataBaseUtilityRunner implements CommandLineRunner {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserRepository usersRepository;
@Autowired
RoleRepository rolesRepository;
@Override
public void run(String args) throws Exception {
try {
Roles userRole = new Roles();
userRole.setRoleName(“USER”);
Roles adminRole = new Roles();
adminRole.setRoleName(“ADMIN”);
rolesRepository.save(userRole);
rolesRepository.save(adminRole);
ApplicationUser user1 = new ApplicationUser();
user1.setFirstName(“John”);
user1.setEmail(“john@test.com”);
user1.setPassword(passwordEncoder.encode(“123456”));
user1.setRole(userRole);

ApplicationUser adminUser = new ApplicationUser();
adminUser.setFirstName(“sam”);
adminUser.setEmail(“sam@test.com”);
adminUser.setPassword(passwordEncoder.encode(“12345”));

adminUser.setRole(adminRole);
usersRepository.save(user1);
usersRepository.save(adminUser);
}catch (Exception exception){

}

}
}

Run the application and query the application_user table. You will see the passwords are encoded now instead of plain text.

spring_access_db=# select * from application_user;
id | email | first_name | last_name | password
—–+—————+————+———–+————————————————————–
102 | john@test.com | John | | $2a$10$IBP9g8pOvNCvkEc7/EG6TO3j6gh49QMuO6uuw9Dd/P9dPRi5mxbiAsnG
103 | sam@test.com | sam | | $2a$10$WxM0l4qnjbYn1Vkgmrbte.hgYqPyHLm/y.9IGvEoiRkrL8.h47QKu
(2 rows)

Configure user details service

For making spring security to use the database for authentication and authorization a user details service needs to be implemented. This is nothing but a class which implements the interface UserDetailsService .

This interface has a method named loadUserByUsername which accepts a string parameter and returns a UserDetails object.

package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

Create a class named DatabaseUserDetailsService.java which implements the UserDetailsService interface.

public class DatabaseUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}

In the overridden method we do the following

Fetch the user from database based on the parameter username.

Retrieve the roles associated with the user

Create an instance of class User with the user details and the associated roles

Return the User object

package com.gintophilip.springauth.service;

import com.gintophilip.springauth.entities.ApplicationUser;
import com.gintophilip.springauth.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collections;

@Service
public class DatabaseUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ApplicationUser user = userRepository.findByEmail(username); //fetch user from db
if(user == null){
throw new UsernameNotFoundException(“User Not found”);
}
//Retrieve user roles
GrantedAuthority authority = new SimpleGrantedAuthority(“ROLE_”+user.getRole().getRoleName());
//create User object
User applicationUser = new User(user.getEmail(),user.getPassword(), Collections.singleton(authority));
return applicationUser;
}
}

The User class implements the UserDetails interface. Spring utilizes the UserDetails to generate an Authentication object. This Authentication object indicates whether the user is authenticated.

Next we need to configure an authentication provider.

Configure the authentication provider

For Spring Security to utilize the DatabaseUserDetailsService for retrieving user details, it must be linked with an authentication provider. For that we will create a bean of type DaoAuthenticationProvider in the SecurityConfig and bind it with DatabaseUserDetailsService within the SecurityConfig.

Bind the DatabaseUserDetailsService

@Autowired
DatabaseUserDetailsService databaseUserDetailsService;

Next, create an instance of DaoAuthenticationProvider which is an implementation of AUthenticationProvider given by Spring security. Then,

Link the databaseUserDetailsService

Link the passwordEncoder

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//set the user details object
authenticationProvider.setUserDetailsService(databaseUserDetailsService);
//set the password encoder
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}

After this the SecurityConfig looks like below.

package com.gintophilip.springauth.web;

import com.gintophilip.springauth.service.DatabaseUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
DatabaseUserDetailsService databaseUserDetailsService;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity securityConfig) throws Exception {
return securityConfig
.authorizeHttpRequests(auth->
auth.requestMatchers(“/api/hello”)
.permitAll()
.requestMatchers(“/api/admin”).hasRole(“ADMIN”)
.anyRequest().authenticated()
).formLogin(Customizer.withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
//set the user details object
authenticationProvider.setUserDetailsService(databaseUserDetailsService);
//set the password encoder
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
}

Next, run the application and access the APIs. When the login page appears, use the user details from the application_user table.

http://localhost:8080/api/hello

http://localhost:8080/api/protected

http://localhost:8080/api/admin

You will be successfully authenticated and able to view the response.

The /api/admin is only accessible to the user sam.