Home | Send Feedback

Upgrade password hashes with Spring Security

Published: 4. May 2020  •  Updated: 21. May 2021  •  java, spring

In a system with username/password authentication, you have a database where the username and password are stored. When the user logs in, the application compares the entered password with the stored password and gives the user access if these two match.

The consensus is that passwords are not stored in plain text. Instead an application runs them through a one-way function first and then stores the result in the database. When a user signs in, the application runs the entered password through the same function and compares the result with the stored value in the database. If an attacker copies the database, only the hashes of the passwords are exposed. Because a one-way function was used, an attacker can only run a brute force attack by checking billions of combinations, run them through the same one-way function and compare the result.

Several years ago, the standard was to use hash functions like MD5, SHA-1, or SHA-256 as one-way function. In today's world, this is no longer recommend, and they are no longer considered secure. Modern hardware can perform billions of hash calculations a second, and attackers can easily crack a lot of stored passwords.

The current standard is to leverage adaptive one-way functions to store passwords. These functions are intentionally designed to be resource-intensive. An adaptive one-way function can be configured to use more resources like CPU or memory. This way, they can adapt to hardware that gets better. The recommendation is that such a function should be tuned, so it takes about 1 second to verify a password on your system. This trade-off is to make it difficult for attackers to crack the password, but not so costly it puts an excessive burden on your system. Examples of adaptive one-way functions that should be used include bcrypt, PBKDF2, scrypt, and Argon2.


In this blog post, we take a look at how Spring Security can help us re-encode passwords that are stored using a hash function like MD5 to an adaptive one-way function.

MD5

Let's assume you have to migrate a several years old application to Spring Boot. After examining the database, you see that all the passwords are stored as MD5 hashes.

md5

You create a typical Spring Boot application with WebMVC, Hibernate, and Spring Security. For Spring Security, you create an UserDetails and UserDetailsService implementation and write the following Spring Security configuration.

@Configuration
@EnableWebSecurity
class SecurityConfig {

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new MessageDigestPasswordEncoder("MD5");
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(customizer -> customizer.anyRequest().authenticated())
        .formLogin(Customizer.withDefaults()).build();
  }

}

SecurityConfig.java

The application uses the provided form login authentication system from Spring Security. Relevant here is that we configure the correct PasswordEncoder. Because the passwords are currently stored as MD5 hashes, we have to use MessageDigestPasswordEncoder.

You find the complete source code of this application on GitHub:
https://github.com/ralscha/blog2020/tree/master/hashupgrade/md5

Note that the application is configured to use a MariaDB database. If you want to use a different database, change the database parameters in src/main/resources/application.properties and add the JDBC driver to the pom.xml.

Start the Spring Boot application with ./mvnw spring-boot:run. Open http://localhost:8080 in a browser and log in with user and password.

Argon2

You finished the main task of the migration, and the application works with Spring Boot. Next, you want to migrate the MD5 encoded passwords and re-encode them with an advanced adaptive one-way function. You decide to use Argon2 as the new password encoding function. Spring Security has built-in support for Argon2, but it depends on the Bouncycastle library, which is not automatically added to the classpath by Spring Boot. Make sure that you add this library manually to the classpath.

    <dependency>
      <groupId>org.bouncycastle</groupId>
      <artifactId>bcprov-jdk15on</artifactId>
      <version>1.70</version>
    </dependency>    

pom.xml

One problem we have when it comes to password migration we can't simply decode the old passwords and then re-encode them with the new function. MD5 is a one-way function, and the only way to crack them is with brute force. Technically we could do this and for sure could crack a lot of the passwords. But when you have security-conscious users, they might use very good passwords. Very long passwords (20+ characters) consisting of random upper and lower case characters, special characters, and numbers. Even when they are encoded in MD5, it would still require a lot of computing power to crack these passwords.

The only feasible solution is to re-encode the passwords when the users sign in. Because then the application has access to the cleartext password. When we introduce this change, the passwords in our system will be encoded with different PasswordEncoders. The passwords of users that have logged in after the software upgrade will be stored as Argon2 encoded values and passwords of users that haven't logged in yet are still encoded with MD5.

The authentication system needs to run the following steps after a user entered the password.

  1. Read encoded password from the database.
  2. Determine the encoding.
  3. Run the entered password through the same password encoder.
  4. Compare the two values.
  5. If the passwords match, check if stored password needs an upgrade
  6. If yes, re-encode the password with new password encoder and store it in the database

With this workflow in place, the passwords will be re-encoded over time with a more advanced one-way function. This is also a good way to weed out old users. If, after some time, there are still old MD5 passwords stored in our system, we can delete or inactivate these users, because we know that they never logged in after installing the new application.


To support passwords encoded with different PasswordsEncoders, Spring Security provides the DelegatingPasswordEncoder class. This is as the name suggests a PasswordEncoder implementation that does not do the encoding by itself; instead, it delegates the work to other encoders.

To determine what password encoder to use, the DelegatingPasswordEncoder adds an id as prefix to the encoded password.

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.2...
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412...
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfO...

When the DelegatingPasswordEncoder validates a password, he reads the id and selects the corresponding PasswordEncoder implementation. With the example above that would be either the BCryptPasswordEncoder, or the Pbkdf2PasswordEncoder or the SCryptPasswordEncoder.


In our application we only need two PasswordEncoders, MessageDigestPasswordEncoder and Argon2PasswordEncoder. Therefore we configure the DelegatingPasswordEncoder in the following fashion

  @Bean
  public PasswordEncoder passwordEncoder() {
    MessageDigestPasswordEncoder md5PE = new MessageDigestPasswordEncoder("MD5");

    Argon2PasswordEncoder argon2PE = new Argon2PasswordEncoder(16, 32, 1, 1 << 17, 5);

    Map<String, PasswordEncoder> encoders = Map.of("argon2", argon2PE);

    DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(
        "argon2", encoders);
    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(md5PE);
    return delegatingPasswordEncoder;
  }

SecurityConfig.java

You pass the supported encoders in a map to the DelegatingPasswordEncoder constructor. You can choose the id arbitrarily. In this example, I use "argon2". The first argument you pass to the DelegatingPasswordEncoder constructor is the id of the encoder that should be used for encoding new passwords.

Make sure that you configure the Argon2PasswordEncoder appropriately so that validation runs for about 1 second on your system. The last two arguments, memory and iterations increase the runtime.


We have two options for how we want to configure the MessageDigestPasswordEncoder.

First option is to the add encoder to the encoders map

Map<String, PasswordEncoder> encoders = Map.of("argon2", argon2PE, "md5", md5PE);

And then add the id to the stored passwords with an SQL UPDATE statement

UPDATE app_user SET password_hash = CONCAT('{md5}', password_hash)

This works, but the DelegatingPasswordEncoder provides a more convenient way. We can configure the MessageDigestPasswordEncoder encoder as the standard encoder for validation.

delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(md5PE);

This configuration tells the DelegatingPasswordEncoder that if it can't find an {id}, use this default password encoder for validation. This way, we don't have to change the values in the database.

Note that setDefaultPasswordEncoderForMatches() only affects validation. The PasswordEncoder used for encoding new passwords is determined by the id you pass as the first argument to the DelegatingPasswordEncoder constructor.


Update Passwords

This configuration alone is not enough for upgrading the passwords. The DelegatingPasswordEncoder does not automatically upgrade the values in the database. This is something we have to implement. But Spring Security informs us when it's time to upgrade a user's password.

For this purpose, you have to create a Spring bean implementing the interface UserDetailsPasswordService. Spring Boot automatically takes care of wiring this bean with the other Spring Security configuration.

package ch.rasc.hashupgrade;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionTemplate;

import ch.rasc.hashupgrade.entity.QUser;

@Component
public class JpaUserDetailsPasswordService implements UserDetailsPasswordService {
  private final UserDetailsService userDetailsService;

  private final JPAQueryFactory jpaQueryFactory;

  private final TransactionTemplate transactionTemplate;

  public JpaUserDetailsPasswordService(UserDetailsService userDetailsService,
      JPAQueryFactory jpaQueryFactory, TransactionTemplate transactionTemplate) {
    this.userDetailsService = userDetailsService;
    this.jpaQueryFactory = jpaQueryFactory;
    this.transactionTemplate = transactionTemplate;
  }

  @Override
  public UserDetails updatePassword(UserDetails user, String newPassword) {
    return this.transactionTemplate.execute(state -> {
      JpaUserDetails userDetails = (JpaUserDetails) user;

      this.jpaQueryFactory.update(QUser.user).set(QUser.user.passwordHash, newPassword)
          .where(QUser.user.id.eq(userDetails.getId())).execute();

      return this.userDetailsService.loadUserByUsername(user.getUsername());
    });
  }
}

JpaUserDetailsPasswordService.java

The updatePassword() method is called each time the DelegatingPasswordEncoder encounters an encoded password that requires an upgrade. An upgrade is required when the {id} is not present or when the {id} does not match the id configured as the encoder for new passwords (the first argument passed to the DelegatingPasswordEncoder constructor). Spring Security then automatically encodes the password with the new password encoder and passes the encoded password together with the UserDetails to the updatePassword() method.

In the updatePassword(), you need to write code that updates the database. In this implementation, we use Querydsl to send an UPDATE statement to the database. Note that newPassword is already prefixed with the correct {id}.


Make sure that the column in the database is large enough to store the updated encoding. In the first example, the database field password_hash has a data type of varchar(32), which is the size of an MD5 hash encoded as a hex string. This is too short for storing Argon2 encoded passwords. For these examples, I use Liquibase for managing database changes. The following changeset increases the size of the field to 106.

  <changeSet author="dev" id="3">
       <modifyDataType tableName="app_user" columnName="password_hash" newDataType="varchar(106)"/>
  </changeSet>

1.xml


You find the source code of this application on GitHub:
https://github.com/ralscha/blog2020/tree/master/hashupgrade/argon2

Start the Spring Boot application with ./mvnw spring-boot:run. Open http://localhost:8080 in a browser and log in with user and password.

When you check the database, you notice that the encoded password changed.

argon2

Future

After a few months in production, you decide to move your application to a more powerful server. As mentioned before, the PasswordEncoder should always be configured so that the validation runs for about 1 second. You decide to increase the last two arguments memory and iterations of the Argon2PasswordEncoder. These two arguments directly affect the runtime of the validation process.

  /*
   * @Bean public PasswordEncoder passwordEncoder() { MessageDigestPasswordEncoder md5PE =
   * new MessageDigestPasswordEncoder("MD5");
   *
   * Argon2PasswordEncoder argon2PE = new Argon2PasswordEncoder(16, 32, 1, 1 << 18, 6);
   *
   * Map<String, PasswordEncoder> encoders = Map.of("argon2", argon2PE);
   *
   * DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(
   * "argon2", encoders);
   * delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(md5PE); return
   * delegatingPasswordEncoder; }
   */

SecurityConfig.java

Note that even we did not change the {id} this still triggers a password update and the method updatePassword() of the UserDetailsPasswordService is called. If you are not sure when an update is triggered, check the implementation of the password encoder. Search for the upgradeEncoding() implementation. In the case of the Argon2PasswordEncoder, you see that an update is triggered when either the memory or iterations parameter changes.

And to reiterate the statement from before an upgrade is always triggered when the {id} of the stored password is different from the first argument passed to the DelegatingPasswordEncoder constructor.


In several years Argon2 might no longer be one of the recommended one-way functions for hashing passwords, and some other algorithm takes its place. Let's assume this hypothetical new function is called nextGen, and Spring Security provides a corresponding NextGenEncoder. In your code, you can simply migrate to this new encoder with the following configuration.

  @Bean
  public PasswordEncoder passwordEncoder() {
    MessageDigestPasswordEncoder md5PE = new MessageDigestPasswordEncoder("MD5");

    Argon2PasswordEncoder argon2PE = new Argon2PasswordEncoder(16, 32, 1, 1 << 18, 6);
    NextGenEncoder nextGenPE = new NextGenEncoder();

    Map<String, PasswordEncoder> encoders = Map.of("argon2", argon2PE, "next", nextGenPE);

    DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(
                "next", encoders);
    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(md5PE);
    return delegatingPasswordEncoder;
  }

You instantiate the new encoder, put it together with an id to the map of encoders, and pass it to the DelegatingPasswordEncoder constructor. Don't forget to change the first constructor argument, so that the upgrade process is triggered and also new passwords are encoded with the modern password encoder.

Standalone

You can also use the DelegatingPasswordEncoder outside of the standard Spring Security setup. When you follow my blog posts, you sometimes see me disabling Spring Boot's autoconfiguration of the Spring Security authentication system.

@Configuration
@EnableWebSecurity
class SecurityConfig {

  @Bean
  AuthenticationManager authenticationManager() {
    return authentication -> {
      throw new AuthenticationServiceException("Cannot authenticate " + authentication);
    };
  }

SecurityConfig.java

In these applications, I handle the authentication in my code, but still, use Spring Security for authorization. In this scenario, you can use the password encoder like in the examples before. The configuration is the same.

  @Bean
  public PasswordEncoder passwordEncoder() {
    MessageDigestPasswordEncoder md5PE = new MessageDigestPasswordEncoder("MD5");

    Argon2PasswordEncoder argon2PE = new Argon2PasswordEncoder(16, 32, 1, 1 << 17, 5);

    Map<String, PasswordEncoder> encoders = Map.of("argon2", argon2PE);

    DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(
        "argon2", encoders);
    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(md5PE);
    return delegatingPasswordEncoder;
  }

SecurityConfig.java

The password encoder is then injected into the controller that handles login requests.

@Controller
public class AuthController {

  private final PasswordEncoder passwordEncoder;

  private final DSLContext dsl;

  private final String userNotFoundEncodedPassword;

  private final SecurityContextRepository securityContextRepository;

  public AuthController(PasswordEncoder passwordEncoder, DSLContext dsl,
      SecurityContextRepository securityContextRepository) {
    this.passwordEncoder = passwordEncoder;
    this.dsl = dsl;
    this.securityContextRepository = securityContextRepository;
    this.userNotFoundEncodedPassword = this.passwordEncoder
        .encode("userNotFoundPassword");
  }

AuthController.java

The HTTP endpoint that handles the login request first validates if the entered password is correct (matches()). The call to passwordEncoder.upgradeEncoding() checks if a password upgrade is required. If it is, the application sends a SQL UPDATE statement with the upgraded encoding to the database.

  @PostMapping("/signin")
  public String signin(@RequestParam String username, @RequestParam String password,
      HttpServletRequest request, HttpServletResponse response) {

    AppUserRecord appUserRecord = this.dsl.selectFrom(APP_USER)
        .where(APP_USER.USER_NAME.eq(username)).fetchOne();

    if (appUserRecord != null) {
      boolean pwMatches = this.passwordEncoder.matches(password,
          appUserRecord.getPasswordHash());
      if (pwMatches) {

        // upgrade password
        if (this.passwordEncoder.upgradeEncoding(appUserRecord.getPasswordHash())) {
          this.dsl.update(APP_USER)
              .set(APP_USER.PASSWORD_HASH, this.passwordEncoder.encode(password))
              .where(APP_USER.ID.eq(appUserRecord.getId())).execute();
        }

        AppUserDetail detail = new AppUserDetail(appUserRecord);
        AppUserAuthentication userAuthentication = new AppUserAuthentication(detail);
        SecurityContextHolder.getContext().setAuthentication(userAuthentication);
        this.securityContextRepository.saveContext(SecurityContextHolder.getContext(),
            request, response);
        return "redirect:/index.html";
      }
    }
    else {
      this.passwordEncoder.matches(password, this.userNotFoundEncodedPassword);
    }

    return "redirect:/signin.html";
  }

AuthController.java

This is a demo application that uses jOOQ for accessing the database. If you are interested in this setup, check out my blog post about that topic.


That concludes this blog post about upgrading passwords with Spring Security. Visit the official Spring Security documentation if you want to learn more about this topic.

It's worth noting that even the best password encoding algorithm does not prevent users from using very weak passwords. A dictionary attack against Argon2 encoded passwords will still be feasible even the algorithm slows down an attack. But brute forcing passwords can easily be parallelized and a determined attacker can throw a lot of computer power against this problem.

In my opinion, you should, therefore, enforce a password policy when users sign up. The current NIST password guidelines suggest an eight-character minimum. Don't implement password policies that are too complex. If they are, users tend to write the passwords down on a piece of paper and stick it to the monitor.

For a more secure solution, I recommend implementing a second-factor authentication. Such a solution does not only depend on username/password for security but also a second factor, like a TOTP that users have to enter.