PART 4: Integrate the database with Spring Security.

Spring Security Basics: Implementing Authentication and Authorization

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:

  1. Add the password encoder bean

  2. Update the plain text password to encrypted password

  3. Configure user details service

  4. 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.
  1. Drop the user_roles table

  2. Drop the roles table

  3. Drop the application_user table

  4. Add the PasswordEncoder class dependency in DataBaseUtilityRunner

  5. 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

  1. Fetch the user from database based on the parameter username.

  2. Retrieve the roles associated with the user

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

  4. 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,

  1. Link the databaseUserDetailsService

  2. 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.

  1. http://localhost:8080/api/hello

  2. http://localhost:8080/api/protected

  3. 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.