1. Introduction
With the latest Spring Security release, a lot has changed. One of those changes is how we can handle password encoding in our applications.
In this tutorial, we’re going to explore some of these changes.
Later, we’ll see how to configure the new delegation mechanism and how to update our existing password encoding, without our users recognizing it.
2. Relevant Changes in Spring Security 5.x
The Spring Security team declared the PasswordEncoder in org.springframework.security.authentication.encoding as deprecated. It was a logical move, as the old interface wasn’t designed for a randomly generated salt. Consequently, version 5 removed this interface.
Additionally, Spring Security changes the way it handles encoded passwords. In previous versions, each application employed one password encoding algorithm only.
By default, StandardPasswordEncoder dealt with that. It used SHA-256 for the encoding. By changing the password encoder, we could switch to another algorithm. But our application had to stick to exactly one algorithm.
Version 5.0 introduces the concept of password encoding delegation. Now, we can use different encodings for different passwords. Spring recognizes the algorithm by an identifier prefixing the encoded password.
Here’s an example of a bcrypt encoded password:
{bcrypt}$2b$12$FaLabMRystU4MLAasNOKb.HUElBAabuQdX59RWHq5X.9Ghm692NEi
Note how bcrypt is specified in curly braces in the very beginning.
3. Delegation Configuration
If the password hash has no prefix, the delegation process uses a default encoder. Hence, by default, we get the StandardPasswordEncoder.
That makes it compatible with the default configuration of previous Spring Security versions.
With version 5, Spring Security introduces PasswordEncoderFactories.createDelegatingPasswordEncoder(). This factory method returns a configured instance of DelegationPasswordEncoder.
For passwords without a prefix, that instance ensures the just mentioned default behavior. And for password hashes that contain a prefix, the delegation is done accordingly.
The Spring Security team lists the supported algorithms in the latest version of the corresponding JavaDoc.
Of course, Spring lets us configure this behavior.
Let’s assume we want to support:
- bcrypt as our new default
- scrypt as an alternative
- SHA-256 as the currently used algorithm.
The configuration for this set-up will look like this:
@Bean
public PasswordEncoder delegatingPasswordEncoder() {
PasswordEncoder defaultEncoder = new StandardPasswordEncoder();
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder(1, 1, 1, 1, 10));
DelegatingPasswordEncoder passworEncoder = new DelegatingPasswordEncoder(
"bcrypt", encoders);
passworEncoder.setDefaultPasswordEncoderForMatches(defaultEncoder);
return passworEncoder;
}
4. Migrating the Password Encoding Algorithm
In the previous section, we explored how to configure password encoding according to our needs. Therefore, now we’ll work on how to switch an already encoded password to a new algorithm.
Let’s imagine we want to change the encoding from SHA-256 to bcrypt, however, we don’t want our user to change their passwords.
One possible solution is to use the login request. At this point, we can access the credentials in plain text. That is the moment we can take the current password and re-encode it.
Consequently, we can use Spring’s AuthenticationSuccessEvent for that. This event fires after a user successfully logged into our application.
Here is the example code:
@Bean
public ApplicationListener<AuthenticationSuccessEvent>
authenticationSuccessListener( PasswordEncoder encoder) {
return (AuthenticationSuccessEvent event) -> {
Authentication auth = event.getAuthentication();
if (auth instanceof UsernamePasswordAuthenticationToken
&& auth.getCredentials() != null) {
CharSequence clearTextPass = (CharSequence) auth.getCredentials();
String newPasswordHash = encoder.encode(clearTextPass);
// [...] Update user's password
((UsernamePasswordAuthenticationToken) auth).eraseCredentials();
}
};
}
In the previous snippet:
- We retrieved the user password in clear text from the provided authentication details
- Created a new password hash with the new algorithm
- Removed the clear text password from the authentication token
By default, extracting the password in clear text wouldn’t be possible because Spring Security deletes it as soon as possible.
Hence, we need to configure Spring so that it keeps the cleartext version of the password.
Additionally, we need to register our encoding delegation:
@Configuration
public class PasswordStorageWebSecurityConfigurer {
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
authenticationManagerBuilder.eraseCredentials(false)
.userDetailsService(getUserDefaultDetailsService())
.passwordEncoder(passwordEncoder());
return authenticationManagerBuilder.build();
}
// ...
}
5. Conclusion
In this quick article, we talked about some new password encoding features available in 5.x.
We also saw how to configure multiple password encoding algorithms to encode our passwords. Furthermore, we explored a way to change the password encoding, without breaking the existing one.
Lastly, we described how to use Spring events to update encrypted user password transparently, allowing us to seamlessly change our encoding strategy without disclosing that to our users.
Lastly and as always, all code examples are available in our GitHub repository.