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:
- Get the user name and password from the user who wants to authenticate.
- Find the user name in the storage, usually a database.
- 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.