Handling Passwords with Spring Boot and Spring Security

Table Of Contents

Systems with user management require authentication. If we use password-based authentication, we have to handle users' passwords in our system. This article shows how to encode and store passwords securely with Spring Security.

Example Code

This article is accompanied by a working code example on GitHub.

Password Handling

If we want to authenticate the user on the server side, we have to follow these steps:

  1. Get the user name and password from the user who wants to authenticate.
  2. Find the user name in the storage, usually a database.
  3. Compare the password the user provided with the user’s password from the database.

Let’s have a look at some best (and worst) practices of how to do that.

Saving Passwords as Plain Text

We have to deal with the fact that we have to save users' passwords in our system for comparison during authentication.

Obviously, it is a bad idea to save passwords as plain text in the database.

We should assume that an attacker can steal the database with passwords or get access to the passwords by other methods like SQL injection.

In this case, the attacker could use the password right away to access the application. So we need to save the passwords in a form that the attacker can’t use it for authentication.

Hashing

Hashing solves the problem of immediate access to the system with exposed passwords.

Hashing is a one-way function that converts the input to a line of symbols. Normally the length of this line is fixed.

If the data is hashed, it’s very hard to convert the hash back to the original input and it’s also very hard to find the input to get the desired output.

We have to hash the password in two cases:

  • When the user registers in the application we hash the password and save it to the database.
  • When the user wants to authenticate, we hash the provided password and compare it with the password hash from the database.

Now, when attackers get the hash of a password, they are not able to use it for accessing the system. Any attempt to find the plain text from the hash value requires a huge effort from the attacker. A brute force attack can be very expensive if the hash is long enough.

Using rainbow tables, attackers still can have success, however. A rainbow table is a table with precomputed hashes for many passwords. There are many rainbow tables available on the internet and some of them contain millions of passwords.

Salting the Password

To prevent an attack with rainbow tables we can use salted passwords. A salt is a sequence of randomly generated bytes that is hashed along with the password. The salt is stored in the storage and doesn’t need to be protected.

Whenever the user tries to authenticate, the user’s password is hashed with the saved salt and the result should match the stored password.

The probability that the combination of the password and the salt is precomputed in a rainbow table is very small. If the salt is long and random enough, it is impossible to find the hash in a rainbow table.

Since the salt is not a secret, attackers are still able to start a brute force attack, though.

A salt can make the attack difficult for the attacker, but hardware is getting more efficient. We must assume fast-evolving hardware with which the attacker can calculate billions of hashes per second.

Thus, hashing and salting are necessary - but not enough.

Password Hashing Functions

Hash functions were not created to hash only passwords. The inventor of hash functions did a very good job and made the hash function very fast.

If we can hash passwords very fast, though, then an attacker can run the brute force attack very fast too.

The solution is to make password hashing slow.

But how slow can it be? It should not be so slow as to be unacceptable for the user, but slow enough to make a brute force attack take infinite time.

We don’t need to develop the slow hashing on our own. Several algorithms have been developed especially for password hashing:

  • bcrypt,
  • scrypt,
  • PBKDF2,
  • argon2,
  • and others.

They use a complicated cryptographic algorithm and allocate resources like CPU or memory deliberately.

Work Factor

The work factor is a configuration of the encoding algorithms that we can increase with growing hardware power.

Every password encoding has its own work factor. The work factor influences the speed of the password encoding. For instance, bcrypt has the parameter strength. The algorithm will make 2 to the power of strength iterations to calculate the hash value. The bigger the number, the slower the encoding.

Password Handling with Spring Security

Now let’s see how Spring Security supports these algorithms and how we can handle passwords with them.

Password Encoders

First, let’s have a look at the password encoders of Spring Security. All password encoders implement the interface PasswordEncoder.

This interface defines the method encode() to convert the plain password into the encoded form and the method matches() to compare a plain password with the encoded password.

Every encoder has a default constructor that creates an instance with the default work factor. We can use other constructors for tuning the work factor.

BCryptPasswordEncoder

 int strength = 10; // work factor of bcrypt
 BCryptPasswordEncoder bCryptPasswordEncoder =
  new BCryptPasswordEncoder(strength, new SecureRandom());
 String encodedPassword = bCryptPasswordEncoder.encode(plainPassword);

BCryptPasswordEncoder has the parameter strength. The default value in Spring Security is 10. It’s recommended to use a SecureRandom as salt generator, because it provides a cryptographically strong random number.

The output looks like this:

$2a$10$EzbrJCN8wj8M8B5aQiRmiuWqVvnxna73Ccvm38aoneiJb88kkwlH2

Note that in contrast to simple hash algorithms like SHA-256 or MD5, the output of bcrypt contains meta-information about the version of the algorithm, work factor, and salt. We don’t need to save this information separately.

Pbkdf2PasswordEncoder

String pepper = "pepper"; // secret key used by password encoding
int iterations = 200000;  // number of hash iteration
int hashWidth = 256;      // hash width in bits

Pbkdf2PasswordEncoder pbkdf2PasswordEncoder =
  new Pbkdf2PasswordEncoder(pepper, iterations, hashWidth);
pbkdf2PasswordEncoder.setEncodeHashAsBase64(true);
String encodedPassword = pbkdf2PasswordEncoder.encode(plainPassword);

The PBKDF2 algorithm was not designed for password encoding but for key derivation from a password. Key derivation is usually needed when we want want to encrypt some data with a password, but the password is not strong enough to be used as an encryption key.

Pbkdf2PasswordEncoder runs the hash algorithm over the plain password many times. It generates a salt, too. We can define how long the output can be and additionally use a secret called pepper to make the password encoding more secure.

The output looks like this:

lLDINGz0YLUUFQuuj5ChAsq0GNM9yHeUAJiL2Be7WUh43Xo3gmXNaw==

The salt is saved within, but we have to save the number of iterations and hash width separately. The pepper should be kept as a secret.

The default number of iterations is 185000 and the default hash width is 256.

SCryptPasswordEncoder

int cpuCost = (int) Math.pow(2, 14); // factor to increase CPU costs
int memoryCost = 8;      // increases memory usage
int parallelization = 1; // currently not supported by Spring Security
int keyLength = 32;      // key length in bytes
int saltLength = 64;     // salt length in bytes

SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder(
  cpuCost, 
  memoryCost,
  parallelization,
  keyLength,
  saltLength);
String encodedPassword = sCryptPasswordEncoder.encode(plainPassword);

The scrypt algorithm can not only configure the CPU cost but also memory cost. This way, we can make an attack even more expensive.

The output looks like this:

$e0801$jRlFuIUd6eAZcuM1wKrzswD8TeKPed9wuWf3lwsWkStxHs0DvdpOZQB32cQJnf0lq/dxL+QsbDpSyyc9Pnet1A==$P3imAo3G8k27RccgP5iR/uoP8FgWGSS920YnHj+CRVA=

This encoder puts the parameter for work factor and salt in the result string, so there is no additional information to save.

Argon2PasswordEncoder

int saltLength = 16; // salt length in bytes
int hashLength = 32; // hash length in bytes
int parallelism = 1; // currently not supported by Spring Security
int memory = 4096;   // memory costs
int iterations = 3;

Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder(
  saltLength,
  hashLength,
  parallelism,
  memory,
  iterations);
String encodePassword = argon2PasswordEncoder.encode(plainPassword);

Argon2 is the winner of Password Hashing Competition in 2015. This algorithm, too, allows us to tune CPU and memory costs. The Argon2 encoder saves all the parameters in the result string. If we want to use this password encoder, we’ll have to import the BouncyCastle crypto library.

Setting Up a Password Encoder in Spring Boot

To see how it works in Spring Boot let’s create an application with REST APIs and password-based authentication supported by Spring Security. The passwords are stored in the relational database.

To keep it simple in this example we send the user credentials with every HTTP request. It means the application must start authentication whenever the client wants to access the API.

Configuring a Password Encoder

First, we create an API we want to protect with Spring Security:

@RestController
class CarResources {

  @GetMapping("/cars")
  public Set<Car> cars() {
    return Set.of(
      new Car("vw", "black"),
      new Car("bmw", "white"));
  }
}

Our goal is to provide access to the resource /cars for authenticated users only, so, we create a configuration with Spring Security rules:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Override
  protected void configure(HttpSecurity httpSecurity) throws Exception {
  httpSecurity
    .csrf()
    .disable()
    .authorizeRequests()
    .antMatchers("/registration")
    .permitAll()
    .anyRequest()
    .authenticated()
    .and()
    .httpBasic();
  }
  
  // ...

}

This code creates rules that requires authentication for all endpoints except /registration and enables HTTP basic authentication.

Whenever an HTTP request is sent to the application Spring Security now checks if the header contains Authorization: Basic <credentials>.

If the header is not set, the server responds with HTTP status 401 (Unauthorized).

If Spring Security finds the header, it starts the authentication.

To authenticate, Spring Security needs user data with user names and password hashes. That’s why we have to implement the UserDetailsService interface. This interface loads user-specific data and needs read-only access to user data:

@Service
class DatabaseUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;
  private final UserDetailsMapper userDetailsMapper;

  // constructor ...

  @Override
  public UserDetails loadUserByUsername(String username) 
                         throws UsernameNotFoundException {
    UserCredentials userCredentials =
                    userRepository.findByUsername(username);
    return userDetailsMapper.toUserDetails(userCredentials);
  }
}

In the service we implement the method loadUserByUsername(), that loads user data from the database.

An implementation of the AuthenticationProvider interface will use the UserDetailsService to perform the authentication logic.

There are many implementations of this interface, but we are interested in DaoAuthenticationProvider, because we store the data in the database:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  private final DatabaseUserDetailsService databaseUserDetailsService;
  
  // constructor ...
  
  @Bean
  public AuthenticationProvider daoAuthenticationProvider() {
    DaoAuthenticationProvider provider = 
      new DaoAuthenticationProvider();
    provider.setPasswordEncoder(passwordEncoder());
    provider.setUserDetailsService(this.databaseUserDetailsService);
    return provider;
  }
  
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
  
  // ...

}

We created a DaoAuthenticationProvider and passed in a BCryptPasswordEncoder. That’s all we need to do to enable password encoding and password matching.

Now we have to take one step more to complete the configuration. We set the DatabaseUserDetailsService service to the DaoAuthenticationProvider. After that, DaoAuthenticationProvider can get the user data to execute the authentication. Spring Security takes care of the rest.

If a client sends an HTTP request with the basic authentication header, Spring Security will read this header, load data for the user, and try to match the password using BCryptPasswordEncoder. If the password matches, the request will be passed through. If not, the server will respond with HTTP status 401.

Implementing User Registration

To add a user to the system, we need to implement an API for registration:

@RestController
class RegistrationResource {

  private final UserRepository userRepository;
  private final PasswordEncoder passwordEncoder;

  // constructor ...

  @PostMapping("/registration")
  @ResponseStatus(code = HttpStatus.CREATED)
  public void register(@RequestBody UserCredentialsDto userCredentialsDto) {
    UserCredentials user = UserCredentials.builder()
      .enabled(true)
      .username(userCredentialsDto.getUsername())
      .password(passwordEncoder.encode(userCredentialsDto.getPassword()))
      .roles(Set.of("USER"))
      .build();
    userRepository.save(user);
  }
}

As we defined in Spring Security rules, the access to /registration is open for everybody. We use the PasswordEncoder that is defined in the Spring Security configuration to encode the password.

In this example, the passwords are encoded with the bcrypt algorithm because we set the PasswordEncoder as the password encoder in the configuration. The code just saves the new user to the database. After that, the user is ready to authenticate.

Upgrading The Work Factor

There are cases where we should increase the work factor of the password encoding for an existing application that uses PasswordEncoder.

Maybe the work factor set years ago is not strong enough anymore today. Or maybe the work factor we use today will not be secure in a couple of years. In these cases, we should increase the work factor of password encoding.

Also, the application might get better hardware. In this case, we can increase work factors without significantly increasing authentication time. Spring Security supports the update of the work factor for many encoding algorithms.

To achieve this, we have to do two things. First, we need to implement UserDetailsPasswordService interface:

@Service
@Transactional
class DatabaseUserDetailPasswordService 
                implements UserDetailsPasswordService {

  private final UserRepository userRepository;
  private final UserDetailsMapper userDetailsMapper;

  // constructor ...

  @Override
  public UserDetails updatePassword(UserDetails user, String newPassword) {
    UserCredentials userCredentials =
              userRepository.findByUsername(user.getUsername());
    userCredentials.setPassword(newPassword);
    return userDetailsMapper.toUserDetails(userCredentials);
  }
}

In the method updatePassword() we just set the new password to the user in the database.

Second, we make this interface known to AuthenticationProvider:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  private final DatabaseUserDetailPasswordService userDetailsService;
  
  // constructor ...
  @Bean
  public AuthenticationProvider daoAuthenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setPasswordEncoder(passwordEncoder());
    provider.setUserDetailsPasswordService(
                this.databaseUserDetailPasswordService);
    provider.setUserDetailsService(this.databaseUserDetailsService);
    return provider;
  }
  
  // ...
}

That’s it. Now, whenever a user starts the authentication, Spring Security compares the work factor in the encoded password of the user with the current work factor of PasswordEncoder.

If the current work factor is stronger, the authentication provider will encode the password of the user with the current password encoder and update it using DatabaseUserDetailPasswordService automatically.

For example, if passwords are currently encoded with BCryptPasswordEncoder of strength 5, we can just add a password encoder of strength 10

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(10);
  }
  
  // ...
}

With each login, passwords are now migrated from strength 5 to 10 automatically.

Using Multiple Password Encodings in the Same Application

Some applications live very long. Long enough that the standards and best practices for password encoding change.

Imagine we support an application with thousands of users and this application uses a normal SHA-1 hashing for password encoding. It means all passwords are stored in the database as SHA-1 hashes.

Now, to raise security, we want to use scrypt for all new users.

To encode and match passwords using different algorithms in the same application, we can use DelegatingPasswordEncoder. This encoder delegates the encoding to another encoder using prefixes:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Bean
  public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }
  
  // ...
}

The simplest way is to let PasswordEncoderFactories generate the DelegatingPasswordEncoder for us. This factory generates a DelegatingPasswordEncoder that supports all encoders of Spring Security for matching.

DelegatingPasswordEncoder has one default encoder. The PasswordEncoderFactories sets BCryptPasswordEncoder as the default encoder. Now, when user data is saved during registration, the password encoder will encode the password and add a prefix at the beginning of the result string. The encoded password looks like this:

{bcrypt}$2a$10$4V9kA793Pi2xf94dYFgKWuw8ukyETxWb7tZ4/mfco9sWkwvBQndxW

When the user with this password wants to authenticate, DelegatingPasswordEncoder can recognize the prefix und choose the suitable encoder for matching.

In the example with the old SHA-1 passwords, we have to run a SQL-script that prefixes all password hashes with {SHA-1}. From this moment, DelegatingPasswordEncoder can match the SHA-1 password when the user wants to authenticate.

But let’s say we don’t want to use BCryptPasswordEncoder as the new default encoder, but SCryptPasswordEncoder instead. We can set the default password encoder after creating DelegatingPasswordEncoder:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Bean
  public PasswordEncoder passwordEncoder() {

    DelegatingPasswordEncoder delegatingPasswordEncoder = 
        (DelegatingPasswordEncoder) PasswordEncoderFactories
            .createDelegatingPasswordEncoder();

    delegatingPasswordEncoder
          .setDefaultPasswordEncoderForMatches(new SCryptPasswordEncoder());

    return delegatingPasswordEncoder;
  }
  
  // ...
}

We can also take full control of which encoders should be supported if we create a DelegatingPasswordEncoder on our own:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Bean
  public PasswordEncoder passwordEncoder() {
    String encodingId = "scrypt";
    Map<String, PasswordEncoder> encoders = new HashMap<>();
    encoders.put(encodingId, new SCryptPasswordEncoder());
    encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
    return new DelegatingPasswordEncoder(encodingId, encoders);
  }
  
  // ...
}

This code creates a password encoder that supports SHA-1 and scrypt for matching and uses scrypt for encoding new passwords. Now we have users in the database with both password encodings SHA-1 and scrypt and the application supports both.

Migrating Password Encoding

If the passwords in the database are encoded by an old, easily attackable, algorithm, then we might want to migrate the passwords to another encoding. To migrate a password to another encoding we have to encode the plain text password.

Of course, we don’t have the plain password in the database and we can’t compute it without huge effort. Also, we don’t want to force users to migrate their passwords. But we can start a slow gradual migration.

Luckily, we don’t need to implement this logic on our own. Spring Security can migrate passwords to the default password encoding. DelegatingPasswordEncoder compares the encoding algorithm after every successful authentication. If the encoding algorithm of the password is different from the current password encoder, the DaoAuthenticationProvider will update the encoded password with the current password encoder and override it in the database using DatabaseUserDetailPasswordService.

If the password encoder we’re currently using gets old and insecure in a couple of years, we can just set another, more secure password encoder as the default encoder. After that, Spring Security will gradually migrate all passwords to the new encoding automatically.

Calculating the Optimal Work Factor

How to choose the suitable work factor for the password encoder? Spring Security recommends tuning the password encoder to take about one second to verify the password. But this time depends on the hardware on which the application runs.

If the same application runs on different hardware for different customers, we can’t set the best work factor at compile time.

But we can calculate a good work factor when starting the application:

@Configuration
@EnableWebSecurity
class SecurityConfiguration extends WebSecurityConfigurerAdapter {
  
  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(
                bcCryptWorkFactorService.calculateStrength());
  }

  // ...
}

The method calculateStrength() returns the work factor that is needed to encode the password so that it takes about one second. The method is executed by starting the application on the current hardware. If the application starts on a different machine, the best work factor for that hardware will be found automatically. Note that this method can take several seconds. It means the start of the application will be slower than usual.

Conclusion

Spring Security supports many password encoders, for both old and modern algorithms. Also, Spring Security provides methods to work with multiple password encodings in the same application. We can change the work factor of password encodings or migrate from one encoding to another without affecting users.

You can find the example code on GitHub.

Written By:

Artur Kuksin

Written By:

Artur Kuksin

With many years of experience in software development I am always looking to learn new things. I like coding and exchanging knowledge.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more