The PasswordEncoder
interface from Spring Security is used to perform one-way translation of the
password to ensure that it is stored securely. Because PasswordEncoder
provides one-way conversion, it
is not intended for cases where password conversion must be two-way (i.e., storing credentials used for database
authentication). Typically PasswordEncoder
is used to store a password that needs to be compared with
the user's password during authentication.
History of Saved Passwords
The standard password storage mechanism has evolved over the years . In the beginning, passwords were stored in plain text. The passwords were assumed to be secure because credentials were required to access the data store in which the passwords were stored. However, attackers have been able to find ways to obtain large "data dumps" of usernames and passwords using SQL Injection attacks. As more and more user credentials were leaked into the public domain, it became clear to security experts that it was necessary to strengthen the protection of user passwords.
Developers were advised to store passwords after passing them through a one-way hashing algorithm, for example SHA-256. When a user attempted to authenticate, the hashed password was compared to the hash of the password that user entered. This meant that the system only needed to store a one-way hash of the password. If hacked, only one-way password hashes would be exposed. Since the hashes were one-way, and guessing a password from a hash was computationally difficult, there was no point in spending the effort to guess every password in the system. To break into this new system, attackers decided to create lookup tables known as rainbow tables. Instead of guessing every password every time, they calculated the password once and stored it in a lookup table.
To reduce the effectiveness of rainbow tables, developers were encouraged to use salted passwords. Instead of exclusively using the password as input to the hashing function, random bytes (known as a salt) were generated for each user's password. The user's salt and password were run through a hashing function, resulting in a unique hash. The salt was stored along with the user's password in clear text. Then, if a user attempted to authenticate, the hashed password was compared with the hash of the stored salt and the password that user entered. The unique salt meant that rainbow tables were no longer effective because the hash was different for each salt and password combination.
In modern times, we understand that cryptographic hashes (such as the SHA-256 algorithm) are no longer secure . The reason is that with modern hardware it is possible to perform billions of hash calculations per second. This means that each password can be easily cracked individually.
Developers are now encouraged to use adaptive computationally irreversible functions to store the password. Validating passwords using adaptive, computationally irreversible functions is inherently resource-intensive (i.e., CPU, memory, etc.). An adaptive, computationally irreversible function allows you to configure a “labor-cost ratio” that can grow as hardware improves. It is recommended to adjust the "effort and cost ratio" so that it takes about 1 second to verify a password on your system. The trade-off is to make it more difficult for attackers to crack your password, but not put an undue burden on your own system due to huge resource costs. Spring Security attempts to provide an adequate starting point for the "Effort to Cost Ratio", but users are advised to tailor the "Effort to Cost Ratio" to their own systems as performance will vary greatly from system to system. Examples of adaptive computationally irreversible functions that should be used are bcrypt, PBKDF2, scrypt, and argon2.
Because adaptive computationally irreversible functions themselves require large computational resources, validating the username and password for each request significantly reduces application performance. Spring Security (or any other library) cannot in any way affect the speedup of password validation, since the appropriate level of security is achieved precisely due to the fact that validation is a resource-intensive task. Users are encouraged to exchange long-term credentials (i.e. username and password) for short-term credentials (i.e. session, OAuth token, etc.). Short-term credentials can be quickly validated without compromising security.
DelegatingPasswordEncoder
Prior to Spring Security 5.0, the default was NoOpPasswordEncoder
,
which required plain text passwords . Based on the Password History section, you would expect the default to be
something like BCryptPasswordEncoder
. However, this ignores three real problems:
There are many applications that use old password encodings that are difficult to migrate.
Best practice password storage will change again
Being a framework, Spring Security cannot make breaking changes frequently
Instead, Spring Security introduces DelegatingPasswordEncoder
, which solves all these problems:
Providing password encodings using current password storage best practices
Allowing validation of passwords in modern and legacy formats
Allowing future encoding updates
Create an instance of DelegatingPasswordEncoder
you can easily use
PasswordEncoderFactories
.
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
In addition, you can create your own custom instance. For example:
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
Password storage format
General format password is as follows:
{id}encodedPassword
For example, id
– is the identifier used to find a suitable PasswordEncoder
to use,
and encodedPassword
is the original encoded password for the selected PasswordEncoder
.
id
must be at the beginning of the password, starting with {
and ending with
}
. If id
cannot be found, id
will be null. For example, below is a possible
list of passwords encoded using different id
s. All initial passwords are "password".
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
{noop}password
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv 7BeL1QxwRpY5Pc=
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cffafaf8410849f27605abcbc0
- The
PasswordEncoder
identifier for the first password will bebcrypt
, and the encrypted password itself will be represented as$2a $10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
. If there is a match, it will be passed toBCryptPasswordEncoder
- The
PasswordEncoder
identifier for the second password will benoop
, and the encrypted password itself will be presented in the formpassword
. If there is a match, it will be passed toNoOpPasswordEncoder
- The
PasswordEncoder
identifier for the third password will bepbkdf2
, and the encoded password itself will be presented in the form5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
. If there is a match, it will be passed toPbkdf2PasswordEncoder
- The
PasswordEncoder
identifier for the fourth password will bescrypt
, and the encrypted password itself will be presented in the form$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwR pY5Pc=
. If there is a match, it will be passed toSCryptPasswordEncoder
- The
PasswordEncoder
identifier for the resulting password will besha256
, and the encrypted password itself will be presented in the form97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cffafaf8410849f27605abcbc0
. If there is a match, it will be passed toStandardPasswordEncoder
Some users may be concerned that the storage format is open for a
potential hacker. There is no need to worry about this, since storing the password does not depend on whether the
algorithm is private. In addition, an attacker can easily calculate most formats without a prefix. For example,
BCrypt passwords often begin with $2a$
.
Password encoding
Passed to the idForEncode
constructor defines which PasswordEncoder
will be used to encode passwords. In the case of the DelegatingPasswordEncoder
we created above, this means that the encoding result of password
will be passed to BCryptPasswordEncoder
and prefixed with {bcrypt}
. The final result will look like this:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Password guessing
Matching is done based on {id}
and mapping id
to
PasswordEncoder
, specified in the constructor. By default, the result of calling matches(CharSequence,
String)
with a password and an id
that was not mapped (including a null id) will result in
an IllegalArgumentException
. This operating logic can be configured using DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)
.
Using id
, we can match any password encoding, but encode passwords using the most modern password
encoding. This is important because, unlike encryption, password hashes are organized in such a way that it is
extremely difficult to recover plain text. Since there is no way to recover plain text, this will make it difficult
to transfer passwords. While users can easily port NoOpPasswordEncoder
, we decided to add it by default
to make it easier to get started.
Getting Started
If you're preparing a demo -version or sample, it will be a little problematic to spend time hashing your users' passwords. There are supporting mechanisms that make this task easier, but they are still not intended for production deployment.
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
If you are creating multiple users, you can also reuse the constructor.
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
.username("user")
.password("password")
.roles("USER")
.build();
User admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
This allows the stored password to be hashed, but the passwords are still exposed in memory and in the compiled source code. Therefore, this method is still not considered safe in a production environment. In a production deployment, you should hash passwords externally.
Encoding using the Spring Boot CLI
The easiest way to properly encode a password is to use Spring Boot CLI.
For example, the following command encodes password password
for use with
DelegatingPasswordEncoder:
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
Troubleshooting
The following error occurs if one of the stored passwords does not have an ID.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
The easiest way to fix the error is to switch to an explicit PasswordEncoder
that encodes
your passwords. The easiest way to resolve this issue is to figure out how your passwords are currently stored and
explicitly specify the correct PasswordEncoder
.
If you are upgrading from Spring Security 4.2.x,
then You can return to the previous logic by opening the NoOpPasswordEncoder
bean.
In addition,
you can provide all passwords with the correct identifier and continue to use DelegatingPasswordEncoder
.
For example, if you are using BCrypt, you could transfer your password from something like:
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
in
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
For a complete list of mappings, see the Javadoc at PasswordEncoderFactories.
BCryptPasswordEncoder
The BCryptPasswordEncoder
implementation makes extensive use of supported bcrypt
algorithm for hashing passwords. To make it more resistant to hacking, bcrypt is deliberately slowed down. Like
other adaptive computationally irreversible functions, it should be finely tuned so that password verification on
your system takes about 1 second. The default implementation of BCryptPasswordEncoder
uses the security
parameter with a value of 10, as specified in the Javadoc to BCryptPasswordEncoder. It is recommended that you configure and test the strength
setting on your system so that password verification takes approximately 1 second.
// Create an encoder with the reliability parameter 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with reliability parameter 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Argon2PasswordEncoder
The implementation of Argon2PasswordEncoder
uses the
algorithm Argon2 for password hashing. Argon2 is
the winner of the Password
Hashing Competition. To combat password cracking on user hardware, Argon2 was deliberately slowed down and
requires a large amount of memory. Like other adaptive computationally irreversible functions, it should be finely
tuned so that password verification on your system takes about 1 second. The current implementation of Argon2PasswordEncoder
requires BouncyCastle.
// Create an encoder with all default settings
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all default settings
val encoder = Argon2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Pbkdf2PasswordEncoder
The implementation of Pbkdf2PasswordEncoder
uses the
algorithm PBKDF2 for password hashing. To combat
password cracking, PBKDF2 has been deliberately slowed down. Like other adaptive computationally irreversible
functions, it should be finely tuned so that password verification on your system takes about 1 second. This
algorithm is an excellent choice if FIPS certification is required.
// Create an encoder with all default settings
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all default settings
val encoder = Pbkdf2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
SCryptPasswordEncoder
The SCryptPasswordEncoder
implementation uses the scrypt for hashing passwords. To
combat password cracking on user equipment, this algorithm has been deliberately slowed down and requires a large
amount of memory. Like other adaptive computationally irreversible functions, it should be finely tuned so that it
takes about 1 second to verify a password on your system.
// Create an encoder with all default settings
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all default settings
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Other PasswordEncoders
There are a significant number of other PasswordEncoder
implementations that are intended solely for backward compatibility. All of them are outdated, which no longer
allows them to be considered safe. However, there are no plans to remove them because migrating existing legacy
systems is difficult.
Configuration for storing passwords
Spring Security uses
DelegatingPasswordEncoder by default. However, this can be configured by opening PasswordEncoder
as a
Spring bean.
If you are upgrading from Spring Security 4.2.x, you can revert to the previous logic by opening
the NoOpPasswordEncoder
bean.
Reverting to NoOpPasswordEncoder
is not considered safe.
Instead, you should switch to using DelegatingPasswordEncoder
to ensure secure password encoding.
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
class="org.springframework.security .crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}
The XML configuration requires that the bean name NoOpPasswordEncoder
was passwordEncoder
.
Changing the password configuration
Most applications that allow the user to set a password also require an update feature this password.
Widely known URL for changing passwords denotes the mechanism by which password managers can discover the password update endpoint for a specific application.
You can configure Spring Security to pass this discovery endpoint. For example, if the
password change endpoint in your application is - /change-password
, then you can configure Spring
Security as follows:
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
passwordManagement { }
}
Then when the password manager goes to /.well-known/change-password
, Spring Security will
forward your endpoint to /change-password
.
Or if your endpoint is not
/change-password
, then you can set it as follows:
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management change-password-page="/update-password"/>
http {
passwordManagement {
changePasswordPage = "/update-password"
}
}
With the above configuration, when the password manager goes to /.well- known/change-password
,
Spring Security will redirect to /update-password
.