Custom Password Encoder in Spring Security

In this tutorial, you will learn how to create a custom password encoder in a Spring Boot application that uses Spring Security.

Table of contents

  1. Create a Spring Boot project and add database connection properties,
  2. Add a User model,
  3. Create a User repository,
  4. Implement a custom PasswordEncoder,
  5. Create a service class that implements UserDetailService,
  6. Add a config class that extends WebSecurityConfigurerAdapter ,
  7. Create a User controller class,
  8. Testing the application.

Create a Spring Boot project and add database connection properties

Go to Spring Initializr  and create a Spring Boot project with Spring Web, Spring Security, Spring Data JPA, and MySQL Driver maven dependencies. You can follow a video tutorial if you need help with it.

Connection Properties

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/user_database
spring.datasource.username=username
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true

Watch the following tutorial, “Using H2 in-memory database with Spring Boot“, to learn how to use a simpler, in-memory database.

Add a User Model

The user class creates an instance of user details.

import jakarta.persistence.*;

@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private int id;

    @Column(name = "first_name")
    private String firstName;

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    //Generate Getters, Setters, construc and toString methods
}

Create a User Repository

UserRepository class to save and retrieves a user object from the database.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User,Integer> {
}

Implement a Custom PasswordEncorder

Below is an example of a class that implements a PasswordEncoder interface. We will use this class to implement our own custom password encoder. A PasswordEncoder is an interface in Spring Security that we can use to make our class provide an implementation of our own password encoder.

Implementations of the password encoder include BcryptPasswordEncoder, NoOpPasswordEncoder and StandardPasswordEncoder.

The default and custom implementations must override encode and matches methods in the password encoder.

The encoding method encodes the raw password. Generally, a good encoding algorithm applies an SHA-1  or greater hash combined with an 8-byte or greater randomly generated salt.

The matches method verifies that the encoded password obtained from storage matches the submitted raw password after it has been encoded. Return true if the password match and false if they do not. The stored password itself does not get decoded.

To use this password encoder, create a bean of custom passwords and inject it into the user service, where the user password is encoded before saving it to the database.

import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.password.PasswordEncoder;

public class CustomPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence plainTextPassword) {
        return BCrypt.hashpw(plainTextPassword.toString(),BCrypt.gensalt(8));
    }

    @Override
    public boolean matches(CharSequence plainTextPassword, String passwordInDatabase) {
        return BCrypt.checkpw(plainTextPassword.toString(),passwordInDatabase);
    }
}

Create a service class that implements UserDetailService

UserDetailService provides a method to find a user by username.

The method finds a user during authentication, but since our interest is in password encoding, we will return null user details and authorities.

The Register and Find Methods

The register method receives a user object from the controller, encodes the plain text password and then persists the user in the database.

The find method returns the user object to the controller, which the client can view using Postman.

import java.util.Collections;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
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 com.security.SpringSecurity.CustomPasswordEncoder;
import com.security.UserRepository.UserRepository;

@Service
public class UserService implements UserDetailsService {

    private UserRepository userRepository;
    private CustomPasswordEncoder customPasswordEncoder;

    @Autowired
    public UserService(UserRepository userRepository,
                       @Lazy CustomPasswordEncoder customPasswordEncoder){
        this.userRepository = userRepository;
        this.customPasswordEncoder = customPasswordEncoder;

    }

    public void registerUser(User user) {
        User newUser = new User();
        newUser.setId(user.getId());
        newUser.setFirstName(user.getFirstName());
        newUser.setLastName(user.getLastName());
        newUser.setEmail(user.getEmail());
        newUser.setUsername(user.getUsername());
        newUser.setPassword(customPasswordEncoder.encode(user.getPassword()));
        userRepository.save(newUser);

    }

    public User findUserById(int id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new NullPointerException("user not found"));
    }

  @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity userEntity = usersRepository.findByEmail(username);
	if(userEntity == null) throw new UsernameNotFoundException(username);

            return new org.springframework.security.core.userdetails.User(username,
                    null,Collections.emptyList());

    }
}

Create a Custom Password Encoder Configuration Class

In the code example below:

  • The Dao authentication provider retrieves user details from a user detail service,
  • Authentication manager builder adds authentication providers,
  • HTTP security allows configuring web-based security for specific HTTP requests,
  • Creating a password encoder ensures we can inject it into the user service class to encode the user password.
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.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

import com.security.Service.UserService;
import com.security.SpringSecurity.CustomPasswordEncoder;

@Configuration
@EnableWebSecurity
public class CustomPasswordEncoderConfig {

    private UserService userService;


    @Autowired
    public CustomPasswordEncoderConfig(UserService userService){
        this.userService = userService;
    }

    @Bean
    public SecurityFilterChain configure(HttpSecurity http) throws Exception { 

 

        http.csrf().disable();
        http.authorizeHttpRequests()
                .requestMatchers("/user/**")
                .permitAll()
                .and()
                .authenticationManager(daoAuthenticationProvider());

         return http.build();
    }

 
 

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(customPasswordEncoder());
        daoAuthenticationProvider.setUserDetailsService(userService);
        return daoAuthenticationProvider;
    }

    @Bean
    public CustomPasswordEncoder customPasswordEncoder(){
        return new CustomPasswordEncoder();
    }
}

Create a User Controller Class

In the code example below, the @RestController class exposes two API endpoints:

  1. /user/register – to create a new user record,
  2. /user/find/{id} – to find user by user id.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.security.Service.UserService;


@RestController
@RequestMapping("/user")
public class UserController {

    private UserService userService;

    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }

    @PostMapping("/register")
    public void registerUser(@RequestBody User user){
        userService.registerUser(user);
    }

    @GetMapping("/find/{id}")
    public User findUserById(@PathVariable("id") int id){
        return userService.findUserById(id);
    }
}

Testing the Application

To create a new user record, start up your application and then send HTTP GET request to the following URL: http://localhost:8080/user/register. If you need help, watch the following video tutorial “@PostMapping, @GetMapping, @PutMapping, @DeleteMapping“.

The image below illustrates HTTP request details using the Postman HTTP client.

To retrieve the user record by user id, you can send HTTP GET request to the following URL:  http://localhost:8080/user/find/1

Notice that the user’s password returned is encoded.

Conclusion

Password encoding is a security measure that ensures data integrity and confidentiality is safe from attackers. Security should also safeguard from other vulnerabilities such as cross-site scripting, SQL injection, denial of service, and cross-site request forgery attacks. To ensure the safety of our application, we need to incorporate a secure authentication and authorization mechanism and ensure password hashing.

Leave a Reply

Your email address will not be published. Required fields are marked *