You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
593 lines
24 KiB
593 lines
24 KiB
[[authentication-password-storage]] |
|
= Password Storage |
|
|
|
Spring Security's `PasswordEncoder` interface is used to perform a one-way transformation of a password to let the password be stored securely. |
|
Given `PasswordEncoder` is a one-way transformation, it is not useful when the password transformation needs to be two-way (such as storing credentials used to authenticate to a database). |
|
Typically, `PasswordEncoder` is used for storing a password that needs to be compared to a user-provided password at the time of authentication. |
|
|
|
[[authentication-password-storage-history]] |
|
== Password Storage History |
|
|
|
Throughout the years, the standard mechanism for storing passwords has evolved. |
|
In the beginning, passwords were stored in plaintext. |
|
The passwords were assumed to be safe because the data store the passwords were saved in required credentials to access it. |
|
However, malicious users were able to find ways to get large "`data dumps`" of usernames and passwords by using attacks such as SQL Injection. |
|
As more and more user credentials became public, security experts realized that we needed to do more to protect users' passwords. |
|
|
|
Developers were then encouraged to store passwords after running them through a one way hash, such as SHA-256. |
|
When a user tried to authenticate, the hashed password would be compared to the hash of the password that they typed. |
|
This meant that the system only needed to store the one-way hash of the password. |
|
If a breach occurred, only the one-way hashes of the passwords were exposed. |
|
Since the hashes were one-way and it was computationally difficult to guess the passwords given the hash, it would not be worth the effort to figure out each password in the system. |
|
To defeat this new system, malicious users decided to create lookup tables known as https://en.wikipedia.org/wiki/Rainbow_table[Rainbow Tables]. |
|
Rather than doing the work of guessing each password every time, they computed the password once and stored it in a lookup table. |
|
|
|
To mitigate the effectiveness of Rainbow Tables, developers were encouraged to use salted passwords. |
|
Instead of using just the password as input to the hash function, random bytes (known as salt) would be generated for every user's password. |
|
The salt and the user's password would be run through the hash function to produce a unique hash. |
|
The salt would be stored alongside the user's password in clear text. |
|
Then when a user tried to authenticate, the hashed password would be compared to the hash of the stored salt and the password that they typed. |
|
The unique salt meant that Rainbow Tables were no longer effective because the hash was different for every salt and password combination. |
|
|
|
In modern times, we realize that cryptographic hashes (like SHA-256) are no longer secure. |
|
The reason is that with modern hardware we can perform billions of hash calculations a second. |
|
This means that we can crack each password individually with ease. |
|
|
|
Developers are now encouraged to leverage adaptive one-way functions to store a password. |
|
Validation of passwords with adaptive one-way functions are intentionally resource-intensive (they intentionally use a lot of CPU, memory, or other resources). |
|
An adaptive one-way function allows configuring a "`work factor`" that can grow as hardware gets better. |
|
We recommend that the "`work factor`" be tuned to take about one 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 that it puts excessive burden on your own system or irritates users. |
|
Spring Security has attempted to provide a good starting point for the "`work factor`", but we encourage users to customize the "`work factor`" for their own system, since the performance varies drastically from system to system. |
|
Examples of adaptive one-way functions that should be used include <<authentication-password-storage-bcrypt,bcrypt>>, <<authentication-password-storage-pbkdf2,PBKDF2>>, <<authentication-password-storage-scrypt,scrypt>>, and <<authentication-password-storage-argon2,argon2>>. |
|
|
|
Because adaptive one-way functions are intentionally resource intensive, validating a username and password for every request can significantly degrade the performance of an application. |
|
There is nothing Spring Security (or any other library) can do to speed up the validation of the password, since security is gained by making the validation resource intensive. |
|
Users are encouraged to exchange the long term credentials (that is, username and password) for a short term credential (such as a session, and OAuth Token, and so on). |
|
The short term credential can be validated quickly without any loss in security. |
|
|
|
|
|
[[authentication-password-storage-dpe]] |
|
== DelegatingPasswordEncoder |
|
|
|
Prior to Spring Security 5.0, the default `PasswordEncoder` was `NoOpPasswordEncoder`, which required plain-text passwords. |
|
Based on the <<authentication-password-storage-history,Password History>> section, you might expect that the default `PasswordEncoder` would now be something like `BCryptPasswordEncoder`. |
|
However, this ignores three real world problems: |
|
|
|
- Many applications use old password encodings that cannot easily migrate. |
|
- The best practice for password storage will change again. |
|
- As a framework, Spring Security cannot make breaking changes frequently. |
|
|
|
Instead Spring Security introduces `DelegatingPasswordEncoder`, which solves all of the problems by: |
|
|
|
- Ensuring that passwords are encoded by using the current password storage recommendations |
|
- Allowing for validating passwords in modern and legacy formats |
|
- Allowing for upgrading the encoding in the future |
|
|
|
You can easily construct an instance of `DelegatingPasswordEncoder` by using `PasswordEncoderFactories`: |
|
|
|
.Create Default DelegatingPasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
PasswordEncoder passwordEncoder = |
|
PasswordEncoderFactories.createDelegatingPasswordEncoder(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() |
|
---- |
|
====== |
|
|
|
Alternatively, you can create your own custom instance: |
|
|
|
.Create Custom DelegatingPasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
String idForEncode = "bcrypt"; |
|
Map encoders = new HashMap<>(); |
|
encoders.put(idForEncode, new BCryptPasswordEncoder()); |
|
encoders.put("noop", NoOpPasswordEncoder.getInstance()); |
|
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); |
|
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); |
|
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()); |
|
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); |
|
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()); |
|
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()); |
|
encoders.put("sha256", new StandardPasswordEncoder()); |
|
|
|
PasswordEncoder passwordEncoder = |
|
new DelegatingPasswordEncoder(idForEncode, encoders); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val idForEncode = "bcrypt" |
|
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf() |
|
encoders[idForEncode] = BCryptPasswordEncoder() |
|
encoders["noop"] = NoOpPasswordEncoder.getInstance() |
|
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5() |
|
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1() |
|
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2() |
|
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
encoders["sha256"] = StandardPasswordEncoder() |
|
|
|
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders) |
|
---- |
|
====== |
|
|
|
[[authentication-password-storage-dpe-format]] |
|
=== Password Storage Format |
|
|
|
The general format for a password is: |
|
|
|
.DelegatingPasswordEncoder Storage Format |
|
[source,text,attrs="-attributes"] |
|
---- |
|
{id}encodedPassword |
|
---- |
|
|
|
`id` is an identifier that is used to look up which `PasswordEncoder` should be used and `encodedPassword` is the original encoded password for the selected `PasswordEncoder`. |
|
The `id` must be at the beginning of the password, start with `{`, and end with `}`. |
|
If the `id` cannot be found, the `id` is set to null. |
|
For example, the following might be a list of passwords encoded using different `id` values. |
|
All of the original passwords are `password`. |
|
|
|
.DelegatingPasswordEncoder Encoded Passwords Example |
|
[source,text,attrs="-attributes"] |
|
---- |
|
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG // <1> |
|
{noop}password // <2> |
|
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc // <3> |
|
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= // <4> |
|
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 // <5> |
|
---- |
|
|
|
<1> The first password has a `PasswordEncoder` id of `bcrypt` and an `encodedPassword` value of `$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG`. |
|
When matching, it would delegate to `BCryptPasswordEncoder` |
|
<2> The second password has a `PasswordEncoder` id of `noop` and `encodedPassword` value of `password`. |
|
When matching, it would delegate to `NoOpPasswordEncoder` |
|
<3> The third password has a `PasswordEncoder` id of `pbkdf2` and `encodedPassword` value of `5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc`. |
|
When matching, it would delegate to `Pbkdf2PasswordEncoder` |
|
<4> The fourth password has a `PasswordEncoder` id of `scrypt` and `encodedPassword` value of `$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=` |
|
When matching, it would delegate to `SCryptPasswordEncoder` |
|
<5> The final password has a `PasswordEncoder` id of `sha256` and `encodedPassword` value of `97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0`. |
|
When matching, it would delegate to `StandardPasswordEncoder` |
|
|
|
[NOTE] |
|
==== |
|
Some users might be concerned that the storage format is provided for a potential hacker. |
|
This is not a concern because the storage of the password does not rely on the algorithm being a secret. |
|
Additionally, most formats are easy for an attacker to figure out without the prefix. |
|
For example, BCrypt passwords often start with `$2a$`. |
|
==== |
|
|
|
[[authentication-password-storage-dpe-encoding]] |
|
=== Password Encoding |
|
|
|
The `idForEncode` passed into the constructor determines which `PasswordEncoder` is used for encoding passwords. |
|
In the `DelegatingPasswordEncoder` we constructed earlier, that means that the result of encoding `password` is delegated to `BCryptPasswordEncoder` and be prefixed with `+{bcrypt}+`. |
|
The end result looks like the following example: |
|
|
|
.DelegatingPasswordEncoder Encode Example |
|
[source,text,attrs="-attributes"] |
|
---- |
|
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
|
---- |
|
|
|
[[authentication-password-storage-dpe-matching]] |
|
=== Password Matching |
|
|
|
Matching is based upon the `+{id}+` and the mapping of the `id` to the `PasswordEncoder` provided in the constructor. |
|
Our example in <<authentication-password-storage-dpe-format,Password Storage Format>> provides a working example of how this is done. |
|
By default, the result of invoking `matches(CharSequence, String)` with a password and an `id` that is not mapped (including a null id) results in an `IllegalArgumentException`. |
|
This behavior can be customized by using `DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)`. |
|
|
|
By using the `id`, we can match on any password encoding but encode passwords by using the most modern password encoding. |
|
This is important, because unlike encryption, password hashes are designed so that there is no simple way to recover the plaintext. |
|
Since there is no way to recover the plaintext, it is difficult to migrate the passwords. |
|
While it is simple for users to migrate `NoOpPasswordEncoder`, we chose to include it by default to make it simple for the getting-started experience. |
|
|
|
[[authentication-password-storage-dep-getting-started]] |
|
=== Getting Started Experience |
|
|
|
If you are putting together a demo or a sample, it is a bit cumbersome to take time to hash the passwords of your users. |
|
There are convenience mechanisms to make this easier, but this is still not intended for production. |
|
|
|
.withDefaultPasswordEncoder Example |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary",attrs="-attributes"] |
|
---- |
|
UserDetails user = User.withDefaultPasswordEncoder() |
|
.username("user") |
|
.password("password") |
|
.roles("user") |
|
.build(); |
|
System.out.println(user.getPassword()); |
|
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary",attrs="-attributes"] |
|
---- |
|
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 builder: |
|
|
|
.withDefaultPasswordEncoder Reusing the Builder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
UserBuilder users = User.withDefaultPasswordEncoder(); |
|
UserDetails user = users |
|
.username("user") |
|
.password("password") |
|
.roles("USER") |
|
.build(); |
|
UserDetails admin = users |
|
.username("admin") |
|
.password("password") |
|
.roles("USER","ADMIN") |
|
.build(); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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 does hash the password that is stored, but the passwords are still exposed in memory and in the compiled source code. |
|
Therefore, it is still not considered secure for a production environment. |
|
For production, you should <<authentication-password-storage-boot-cli,hash your passwords externally>>. |
|
|
|
[[authentication-password-storage-boot-cli]] |
|
=== Encode with Spring Boot CLI |
|
|
|
The easiest way to properly encode your password is to use the https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-cli.html[Spring Boot CLI]. |
|
|
|
For example, the following example encodes the password of `password` for use with <<authentication-password-storage-dpe>>: |
|
|
|
.Spring Boot CLI encodepassword Example |
|
[source,attrs="-attributes"] |
|
---- |
|
spring encodepassword password |
|
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6 |
|
---- |
|
|
|
[[authentication-password-storage-dpe-troubleshoot]] |
|
=== Troubleshooting |
|
|
|
The following error occurs when one of the passwords that are stored has no `id`, as described in <<authentication-password-storage-dpe-format>>. |
|
|
|
---- |
|
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 resolve it is to figure out how your passwords are currently being stored and explicitly provide the correct `PasswordEncoder`. |
|
|
|
If you are migrating from Spring Security 4.2.x, you can revert to the previous behavior by <<authentication-password-storage-configuration,exposing a `NoOpPasswordEncoder` bean>>. |
|
|
|
Alternatively, you can prefix all of your passwords with the correct `id` and continue to use `DelegatingPasswordEncoder`. |
|
For example, if you are using BCrypt, you would migrate your password from something like: |
|
|
|
---- |
|
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
|
---- |
|
|
|
to |
|
|
|
[source,attrs="-attributes"] |
|
---- |
|
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG |
|
---- |
|
|
|
For a complete listing of the mappings, see the Javadoc for |
|
https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/security/crypto/factory/PasswordEncoderFactories.html[`PasswordEncoderFactories`]. |
|
|
|
[[authentication-password-storage-bcrypt]] |
|
== BCryptPasswordEncoder |
|
|
|
The `BCryptPasswordEncoder` implementation uses the widely supported https://en.wikipedia.org/wiki/Bcrypt[bcrypt] algorithm to hash the passwords. |
|
To make it more resistant to password cracking, bcrypt is deliberately slow. |
|
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. |
|
The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentioned in the Javadoc of https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html[`BCryptPasswordEncoder`]. You are encouraged to |
|
tune and test the strength parameter on your own system so that it takes roughly 1 second to verify a password. |
|
|
|
.BCryptPasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
// Create an encoder with strength 16 |
|
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); |
|
String result = encoder.encode("myPassword"); |
|
assertTrue(encoder.matches("myPassword", result)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
// Create an encoder with strength 16 |
|
val encoder = BCryptPasswordEncoder(16) |
|
val result: String = encoder.encode("myPassword") |
|
assertTrue(encoder.matches("myPassword", result)) |
|
---- |
|
====== |
|
|
|
[[authentication-password-storage-argon2]] |
|
== Argon2PasswordEncoder |
|
|
|
The `Argon2PasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Argon2[Argon2] algorithm to hash the passwords. |
|
Argon2 is the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[Password Hashing Competition]. |
|
To defeat password cracking on custom hardware, Argon2 is a deliberately slow algorithm that requires large amounts of memory. |
|
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. |
|
The current implementation of the `Argon2PasswordEncoder` requires BouncyCastle. |
|
|
|
.Argon2PasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); |
|
String result = encoder.encode("myPassword"); |
|
assertTrue(encoder.matches("myPassword", result)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
val result: String = encoder.encode("myPassword") |
|
assertTrue(encoder.matches("myPassword", result)) |
|
---- |
|
====== |
|
|
|
[[authentication-password-storage-pbkdf2]] |
|
== Pbkdf2PasswordEncoder |
|
|
|
The `Pbkdf2PasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/PBKDF2[PBKDF2] algorithm to hash the passwords. |
|
To defeat password cracking PBKDF2 is a deliberately slow algorithm. |
|
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. |
|
This algorithm is a good choice when FIPS certification is required. |
|
|
|
.Pbkdf2PasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8(); |
|
String result = encoder.encode("myPassword"); |
|
assertTrue(encoder.matches("myPassword", result)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
val result: String = encoder.encode("myPassword") |
|
assertTrue(encoder.matches("myPassword", result)) |
|
---- |
|
====== |
|
|
|
[[authentication-password-storage-scrypt]] |
|
== SCryptPasswordEncoder |
|
|
|
The `SCryptPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Scrypt[scrypt] algorithm to hash the passwords. |
|
To defeat password cracking on custom hardware, scrypt is a deliberately slow algorithm that requires large amounts of memory. |
|
Like other adaptive one-way functions, it should be tuned to take about 1 second to verify a password on your system. |
|
|
|
.SCryptPasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8(); |
|
String result = encoder.encode("myPassword"); |
|
assertTrue(encoder.matches("myPassword", result)); |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
// Create an encoder with all the defaults |
|
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8() |
|
val result: String = encoder.encode("myPassword") |
|
assertTrue(encoder.matches("myPassword", result)) |
|
---- |
|
====== |
|
|
|
[[authentication-password-storage-other]] |
|
== Other ``PasswordEncoder``s |
|
|
|
There are a significant number of other `PasswordEncoder` implementations that exist entirely for backward compatibility. |
|
They are all deprecated to indicate that they are no longer considered secure. |
|
However, there are no plans to remove them, since it is difficult to migrate existing legacy systems. |
|
|
|
[[authentication-password-storage-configuration]] |
|
== Password Storage Configuration |
|
|
|
Spring Security uses <<authentication-password-storage-dpe>> by default. |
|
However, you can customize this by exposing a `PasswordEncoder` as a Spring bean. |
|
|
|
|
|
If you are migrating from Spring Security 4.2.x, you can revert to the previous behavior by exposing a `NoOpPasswordEncoder` bean. |
|
|
|
[WARNING] |
|
==== |
|
Reverting to `NoOpPasswordEncoder` is not considered to be secure. |
|
You should instead migrate to using `DelegatingPasswordEncoder` to support secure password encoding. |
|
==== |
|
|
|
.NoOpPasswordEncoder |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public static NoOpPasswordEncoder passwordEncoder() { |
|
return NoOpPasswordEncoder.getInstance(); |
|
} |
|
---- |
|
|
|
XML:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<b:bean id="passwordEncoder" |
|
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/> |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun passwordEncoder(): PasswordEncoder { |
|
return NoOpPasswordEncoder.getInstance(); |
|
} |
|
---- |
|
====== |
|
|
|
[NOTE] |
|
==== |
|
XML Configuration requires the `NoOpPasswordEncoder` bean name to be `passwordEncoder`. |
|
==== |
|
|
|
[[authentication-change-password-configuration]] |
|
== Change Password Configuration |
|
|
|
Most applications that allow a user to specify a password also require a feature for updating that password. |
|
|
|
https://w3c.github.io/webappsec-change-password-url/[A Well-Known URL for Changing Passwords] indicates a mechanism by which password managers can discover the password update endpoint for a given application. |
|
|
|
You can configure Spring Security to provide this discovery endpoint. |
|
For example, if the change password endpoint in your application is `/change-password`, then you can configure Spring Security like so: |
|
|
|
.Default Change Password Endpoint |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.passwordManagement(Customizer.withDefaults()) |
|
---- |
|
|
|
XML:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:password-management/> |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
passwordManagement { } |
|
} |
|
---- |
|
====== |
|
|
|
Then, when a password manager navigates to `/.well-known/change-password` then Spring Security will redirect your endpoint, `/change-password`. |
|
|
|
Or, if your endpoint is something other than `/change-password`, you can also specify that like so: |
|
|
|
.Change Password Endpoint |
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.passwordManagement((management) -> management |
|
.changePasswordPage("/update-password") |
|
) |
|
---- |
|
|
|
XML:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<sec:password-management change-password-page="/update-password"/> |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
passwordManagement { |
|
changePasswordPage = "/update-password" |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
With the above configuration, when a password manager navigates to `/.well-known/change-password`, then Spring Security will redirect to `/update-password`.
|
|
|