×
☰ See All Chapters

Password Encoding in Spring Security

In real-world projects, storing passwords as plain text is a big security risk because they can be easily stolen if someone gains unauthorized access to the database. To mitigate this risk, passwords are usually transformed in a way that makes it difficult for attackers to read or steal them.

In Spring Security, the PasswordEncoder contract is used to manage password encryption and validation. It's essentially a blueprint that defines how to handle passwords securely. Here's a breakdown of the PasswordEncoder contract:

public interface PasswordEncoder {
   
String encode(CharSequence rawPassword);
   
boolean matches(CharSequence rawPassword, String encodedPassword);
   
default boolean upgradeEncoding(String encodedPassword) {
       
return false;
   }
}

encode(CharSequence rawPassword): This method takes a raw password (in plain text) and transforms it into a secure format, typically through encryption or hashing. This ensures that the password is not stored as plain text in the database, making it more secure against unauthorized access.

matches(CharSequence rawPassword, String encodedPassword): This method is used to check if a provided raw password matches the encoded password stored in the database. During the authentication process, the user-supplied password is compared against the stored encoded password using this method. If they match, the user is authenticated.

upgradeEncoding(CharSequence encodedPassword): This method, by default, returns false in the contract. It's used to improve password security by re-encoding an already encoded password. If you override this method to return true, it indicates that the encoded password should be re-encoded for better security. This can be useful if your application's security requirements change over time.

In summary, the PasswordEncoder contract provides a standardized way to handle password encryption and validation in Spring Security. By implementing this contract, you ensure that passwords are securely managed within your application, reducing the risk of unauthorized access and data breaches.

In the below implementation we are just returning same password without encoding from encode method. This implementation is same as the NoOpPasswordEncoder which we used in previous chapters.

public class PlainTextPasswordEncoder implements PasswordEncoder {
   
@Override
   
public String encode(CharSequence rawPassword) {
       
return rawPassword.toString();
   }

   
@Override
   
public boolean matches(CharSequence rawPassword, String encodedPassword) {
       
return rawPassword.equals(encodedPassword);
   }
}

Password Encoding Example in Spring Security

In the example below, we utilize AES encryption to encrypt passwords. The AES encryption algorithm retrieves the encryption secret key from the application.properties file. In real-world projects, it is common practice to securely manage secrets. During the initialization of the Spring Boot application, we insert same records into the database, wherein passwords are encrypted using the same secret key.

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
     
xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <
modelVersion>4.0.0</modelVersion>
  <
parent>
     <
groupId>org.springframework.boot</groupId>
     <
artifactId>spring-boot-starter-parent</artifactId>
     <
version>2.6.0</version>
     <
relativePath/> <!-- lookup parent from repository, not local -->
 
</parent>
  <
groupId>com.java4coding</groupId>
  <
artifactId> PasswordEncodingInSpringSecurity</artifactId>
  <
version>0.0.1-SNAPSHOT</version>
  <
name> PasswordEncodingInSpringSecurity</name>
  <
description>Managing Users in Spring Security</description>
  <
properties>
     <
java.version>11</java.version>
     <
project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <
project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <
spring-boot.version>2.6.0</spring-boot.version>
     <
lombok.version>1.18.28</lombok.version>
  </
properties>
  <
dependencies>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-web</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-security</artifactId>
     </
dependency>
     <
dependency>
        <
groupId>org.projectlombok</groupId>
        <
artifactId>lombok</artifactId>
        <
version>${lombok.version}</version>
        <
scope>provided</scope>
     </
dependency>
     <
dependency>
        <
groupId>com.h2database</groupId>
        <
artifactId>h2</artifactId>
        <
scope>runtime</scope>
     </
dependency>
     <
dependency>
        <
groupId>org.springframework.boot</groupId>
        <
artifactId>spring-boot-starter-data-jpa</artifactId>
     </
dependency>
  </
dependencies>
  <
dependencyManagement>
     <
dependencies>
        <
dependency>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-dependencies</artifactId>
           <
version>${spring-boot.version}</version>
           <
type>pom</type>
           <
scope>import</scope>
        </
dependency>
     </
dependencies>
  </
dependencyManagement>

  <
build>
     <
plugins>
        <
plugin>
           <
groupId>org.springframework.boot</groupId>
           <
artifactId>spring-boot-maven-plugin</artifactId>
           <
version>${spring-boot.version}</version>
           <
executions>
              <
execution>
                 <
id>build-info</id>
                 <
goals>
                    <
goal>build-info</goal>
                    <
goal>repackage</goal>
                 </
goals>
              </
execution>
           </
executions>
        </
plugin>
     </
plugins>
  </
build>

</
project>

 

 

SpringBootDemo.java

package com.java4coding;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan
@EnableJpaRepositories
public class SpringBootDemo {
   
public static void main(String[] args) {
       
SpringApplication.run(SpringBootDemo.class, args);
   }
}

 

 

ApplicationConfig.java

package com.java4coding.config;

import com.java4coding.repository.UserRepository;
import com.java4coding.security.AESEncryptor;
import com.java4coding.security.PasswordEncoderImpl;
import com.java4coding.security.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class ApplicationConfig {

   
@Bean
   
public UserDetailsService userDetailsService(UserRepository userRepository) {
       
return new UserDetailsServiceImpl(userRepository);
   }

   
@Bean
   
public PasswordEncoder passwordEncoder(AESEncryptor aesEncryptor) {
       
PasswordEncoderImpl passwordEncoder = new PasswordEncoderImpl();
       
passwordEncoder.setAesEncryptor(aesEncryptor);
       
return passwordEncoder;
   }
}

 

 

ApplicationWebSecurityConfigurerAdapter.java

package com.java4coding.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

   
/*
   Since H2 has its own authentication provider, you can skip the Spring Security for the
   path of h2 console entirely in the same way that you do for your static content.
   In order to do that, in your Spring security config, you have to override the configuration
    method which takes an instance of org.springframework.security.config.annotation.web.builders.WebSecurity
    as a parameter instead of the one which takes an instance of org.springframework.security.config.annotation.web.builders.HttpSecurity
    */
   
@Override
   
public void configure(WebSecurity web) throws Exception {
       web.ignoring().antMatchers(
"/h2-console/**");
   }

   
/*
    Spring Security enables Cross-Site Request Forgery (CSRF) protection by default.
    CSRF is an attack that tricks the victim into submitting a malicious request and
    uses the identity of the victim to perform an undesired function on their behalf.
    If the CSRF token, which is used to protect against this type of attack, is missing
    or incorrect, the server may also respond with error 403.
    */
   
@Override
   
protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .anyRequest()
               .authenticated()
               .and()
               .httpBasic();
       http.csrf().disable();
   }

}

 

 

UserController.java

package com.java4coding.controller;

import com.java4coding.dto.ResponseDto;
import com.java4coding.dto.UserDto;
import com.java4coding.entity.User;
import com.java4coding.repository.UserRepository;
import com.java4coding.security.AESEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class UserController {

   
@Autowired
   
private UserRepository userRepository;
   
@Autowired
   
private AESEncryptor aesEncryptor;

   
@GetMapping(value = "/testAccess")
   
public String testAccess() {
       
return "Hurray! You are Authorized.";
   }

   
@PostMapping(value = "/addUser")
   
public ResponseEntity<ResponseDto> addUser(@RequestBody UserDto userDto) {
       
User user = new User();
       
user.setPassword(aesEncryptor.encrypt(userDto.getPassword()));
       
user.setUsername(userDto.getUsername());
       
user.setAuthority(userDto.getAuthority());
       
userRepository.save(user);
       
return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build());
   }

   
@PostMapping(value = "/updateUser")
   
public ResponseEntity<ResponseDto> updateUser(@RequestBody UserDto userDto) {
       
List<User> users = userRepository.findByUsername(userDto.getUsername());
       
if (users == null || users.isEmpty()) {
           
return ResponseEntity.ok(ResponseDto.builder().result("No User Exists").code("400").build());
       }
       
User existingUser = users.get(0);
       
existingUser.setPassword(aesEncryptor.encrypt(userDto.getUsername()));
       
existingUser.setUsername(userDto.getNewUsername());
       
existingUser.setAuthority(userDto.getAuthority());
       
userRepository.save(existingUser);
       
return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build());
   }

   
@GetMapping(value = "/deleteUser/{userName}")
   
public ResponseEntity<ResponseDto> deleteUser(@PathVariable("userName") String userName) {
       
List<User> users = userRepository.findByUsername(userName);
       
if (users == null || users.isEmpty()) {
           
return ResponseEntity.ok(ResponseDto.builder().result("No User Exists").code("400").build());
       }
       
User existingUser = users.get(0);
       
userRepository.delete(existingUser);
       
return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build());
   }
}

 

ResponseDto.java

package com.java4coding.dto;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@Builder
public class ResponseDto {
   
private String result;
   
private String code;
}

 

 

UserDto.java

package com.java4coding.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class UserDto {
   
private String username;
   
private String newUsername;
   
private String password;
   
private String authority;
}

 

 

User.java

package com.java4coding.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Setter
@Getter
public class User {
   
@Id
   @GeneratedValue
(strategy = GenerationType.IDENTITY)
   
private Long id;
   
private String username;
   
private String password;
   
private String authority;
}

 

 

UserRepository.java

package com.java4coding.repository;

import com.java4coding.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
   
List<User> findByUsername(String userName);
}

 

 

AESEncryptor.java

package com.java4coding.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

@Component
public class AESEncryptor {
   
private static SecretKeySpec secretKey;
   
private static byte[] key;
   
private static final String ALGORITHM = "AES";
   
@Value("${spring.security.secret}")
   
private String secret;

   
public void prepareSecreteKey(String myKey) {
       
MessageDigest sha = null;
       
try {
           
key = myKey.getBytes(StandardCharsets.UTF_8);
           
sha = MessageDigest.getInstance("SHA-1");
           
key = sha.digest(key);
           
key = Arrays.copyOf(key, 16);
           
secretKey = new SecretKeySpec(key, ALGORITHM);
       }
catch (NoSuchAlgorithmException e) {
           e.printStackTrace();
       }
   }

   
public String encrypt(String strToEncrypt) {
       
try {
           prepareSecreteKey(
secret);
           
Cipher cipher = Cipher.getInstance(ALGORITHM);
           
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
           
return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8")));
       }
catch (Exception e) {
           
System.out.println("Error while encrypting: " + e.toString());
       }
       
return null;
   }

   
/*
   public String decrypt(String strToDecrypt, String secret) {
       try {
           prepareSecreteKey(secret);
           Cipher cipher = Cipher.getInstance(ALGORITHM);
           cipher.init(Cipher.DECRYPT_MODE, secretKey);
           return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt)));
       } catch (Exception e) {
           System.out.println("Error while decrypting: " + e.toString());
       }
       return null;
   }

    */
}

 

 

PasswordEncoderImpl.java

package com.java4coding.security;

import lombok.Getter;
import lombok.Setter;
import org.springframework.security.crypto.password.PasswordEncoder;

@Setter
@Getter
public class PasswordEncoderImpl implements PasswordEncoder {

   
private AESEncryptor aesEncryptor;

   
@Override
   
public String encode(CharSequence rawPassword) {
       
return aesEncryptor.encrypt(rawPassword.toString());
   }

   
@Override
   
public boolean matches(CharSequence rawPassword, String encodedPassword) {
       
String hashedPassword = encode(rawPassword);
       
return encodedPassword.equals(hashedPassword);
   }
}

 

 

SecurityUser.java

package com.java4coding.security;

import com.java4coding.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

public class SecurityUser implements UserDetails {
   
private final User user;

   
public SecurityUser(User user) {
       
this.user = user;
   }

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

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

   
@Override
   
public Collection<? extends GrantedAuthority> getAuthorities() {
       
return List.of(() -> user.getAuthority());
   }

   
@Override
   
public boolean isAccountNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isAccountNonLocked() {
       
return true;
   }

   
@Override
   
public boolean isCredentialsNonExpired() {
       
return true;
   }

   
@Override
   
public boolean isEnabled() {
       
return true;
   }
}

 

 

UserDetailsServiceImpl.java

package com.java4coding.security;

import com.java4coding.entity.User;
import com.java4coding.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import java.util.List;

@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
   
private UserRepository userRepository;

   
@Override
   
public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException {
       
List<User> usersByUserName = userRepository.findByUsername(username);
       
return new SecurityUser(usersByUserName.get(0));
   }
}

 

 

application.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console/

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=pass
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
#spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=none

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.properties.hibernate.format_sql=true

 

 

schema.sql

create table user (
  id bigint generated
by default as identity,
   authority
varchar(255),
   password
varchar(255),
   username
varchar(255),
   
primary key (id)
);

 

 

data.sql

INSERT INTO User (id, username, password, authority) VALUES (1, 'manu', '3BayxFSV4Pu9HNSwNrSe5w==', 'read');
INSERT INTO User (id, username, password, authority) VALUES (2, 'advith', 'Nqu2KPw66lw57/7TzwadEg==', 'read');
INSERT INTO User (id, username, password, authority) VALUES (3, 'aashvith', 'Nqu2KPw66lw57/7TzwadEg==', 'read');

 

Project Structure

password-encoding-in-spring-security-0
 

Output

password-encoding-in-spring-security-1
 
password-encoding-in-spring-security-2
 

Choosing the Right PasswordEncoder Implementation in Spring Security

When selecting a PasswordEncoder implementation in Spring Security, it's important to choose the most suitable one for your application. Here's a brief overview of the provided implementations:

  1. NoOpPasswordEncoder: This implementation doesn't actually encode the password; it stores it in plain text. It's only intended for examples and should never be used in real-world scenarios due to its inherent security risks. 

  2. StandardPasswordEncoder: This implementation uses SHA-256 to hash passwords. However, it's deprecated because SHA-256 is considered less secure compared to newer hashing algorithms. 

  3. Pbkdf2PasswordEncoder: This implementation uses the Password-Based Key Derivation Function 2 (PBKDF2), which is a strong cryptographic function designed for securely hashing passwords. 

  4. BCryptPasswordEncoder: This implementation uses the bcrypt algorithm, which is a widely recommended and secure hashing function for password storage. It's suitable for most applications and provides a good balance between security and performance. 

  5. SCryptPasswordEncoder: This implementation uses the scrypt hashing function, which is designed to be memory-intensive and resistant to brute-force attacks. It offers strong security guarantees but may be slower in terms of computation compared to other algorithms. 

When choosing a PasswordEncoder implementation, it's generally recommended to use one of the secure options like PBKDF2PasswordEncoder, BCryptPasswordEncoder, or SCryptPasswordEncoder, depending on your specific security requirements and performance considerations. Avoid using deprecated or insecure implementations like StandardPasswordEncoder or NoOpPasswordEncoder in production environments.

 


All Chapters
Author