|
|
@@ -67,68 +67,12 @@ Instead Spring Security introduces `DelegatingPasswordEncoder`, which solves all
|
|
|
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()
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./DelegatingPasswordEncoderUsage[tag=createDefaultPasswordEncoder,indent=0]
|
|
|
|
|
|
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)
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./DelegatingPasswordEncoderUsage[tag=createCustomPasswordEncoder,indent=0]
|
|
|
|
|
|
[[authentication-password-storage-dpe-format]]
|
|
|
=== Password Storage Format
|
|
|
@@ -209,74 +153,12 @@ If you are putting together a demo or a sample, it is a bit cumbersome to take t
|
|
|
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
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./WithDefaultPasswordEncoderUsage[tag=createSingleUser,indent=0]
|
|
|
|
|
|
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()
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./WithDefaultPasswordEncoderUsage[tag=createMultipleUsers,indent=0]
|
|
|
|
|
|
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.
|
|
|
@@ -337,28 +219,7 @@ The default implementation of `BCryptPasswordEncoder` uses strength 10 as mentio
|
|
|
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))
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./BCryptPasswordEncoderUsage[tag=bcryptPasswordEncoder,indent=0]
|
|
|
|
|
|
[[authentication-password-storage-argon2]]
|
|
|
== Argon2PasswordEncoder
|
|
|
@@ -370,28 +231,7 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
|
|
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))
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./Argon2PasswordEncoderUsage[tag=argon2PasswordEncoder,indent=0]
|
|
|
|
|
|
[[authentication-password-storage-pbkdf2]]
|
|
|
== Pbkdf2PasswordEncoder
|
|
|
@@ -402,28 +242,7 @@ Like other adaptive one-way functions, it should be tuned to take about 1 second
|
|
|
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))
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./Pbkdf2PasswordEncoderUsage[tag=pbkdf2PasswordEncoder,indent=0]
|
|
|
|
|
|
[[authentication-password-storage-scrypt]]
|
|
|
== SCryptPasswordEncoder
|
|
|
@@ -433,28 +252,7 @@ To defeat password cracking on custom hardware, scrypt is a deliberately slow al
|
|
|
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))
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./SCryptPasswordEncoderUsage[tag=sCryptPasswordEncoder,indent=0]
|
|
|
|
|
|
[[authentication-password-storage-other]]
|
|
|
== Other ``PasswordEncoder``s
|
|
|
@@ -606,86 +404,4 @@ However, just a 401 or the redirect is not so useful in that case, it will cause
|
|
|
In such cases, you can handle the `CompromisedPasswordException` via the `AuthenticationFailureHandler` to perform your desired logic, like redirecting the user-agent to `/reset-password`, for example:
|
|
|
|
|
|
.Using CompromisedPasswordChecker
|
|
|
-[tabs]
|
|
|
-======
|
|
|
-Java::
|
|
|
-+
|
|
|
-[source,java,role="primary"]
|
|
|
-----
|
|
|
-@Bean
|
|
|
-public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
|
- http
|
|
|
- .authorizeHttpRequests(authorize -> authorize
|
|
|
- .anyRequest().authenticated()
|
|
|
- )
|
|
|
- .formLogin((login) -> login
|
|
|
- .failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
|
|
|
- );
|
|
|
- return http.build();
|
|
|
-}
|
|
|
-
|
|
|
-@Bean
|
|
|
-public CompromisedPasswordChecker compromisedPasswordChecker() {
|
|
|
- return new HaveIBeenPwnedRestApiPasswordChecker();
|
|
|
-}
|
|
|
-
|
|
|
-static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
|
|
|
-
|
|
|
- private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
|
|
|
- "/login?error");
|
|
|
-
|
|
|
- private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
|
|
|
-
|
|
|
- @Override
|
|
|
- public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
|
|
|
- AuthenticationException exception) throws IOException, ServletException {
|
|
|
- if (exception instanceof CompromisedPasswordException) {
|
|
|
- this.redirectStrategy.sendRedirect(request, response, "/reset-password");
|
|
|
- return;
|
|
|
- }
|
|
|
- this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
|
|
|
- }
|
|
|
-
|
|
|
-}
|
|
|
-----
|
|
|
-
|
|
|
-Kotlin::
|
|
|
-+
|
|
|
-[source,kotlin,role="secondary"]
|
|
|
-----
|
|
|
-@Bean
|
|
|
-open fun filterChain(http:HttpSecurity): SecurityFilterChain {
|
|
|
- http {
|
|
|
- authorizeHttpRequests {
|
|
|
- authorize(anyRequest, authenticated)
|
|
|
- }
|
|
|
- formLogin {
|
|
|
- failureHandler = CompromisedPasswordAuthenticationFailureHandler()
|
|
|
- }
|
|
|
- }
|
|
|
- return http.build()
|
|
|
-}
|
|
|
-
|
|
|
-@Bean
|
|
|
-open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
|
|
|
- return HaveIBeenPwnedRestApiPasswordChecker()
|
|
|
-}
|
|
|
-
|
|
|
-class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
|
|
|
- private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
|
|
|
- private val redirectStrategy = DefaultRedirectStrategy()
|
|
|
-
|
|
|
- override fun onAuthenticationFailure(
|
|
|
- request: HttpServletRequest,
|
|
|
- response: HttpServletResponse,
|
|
|
- exception: AuthenticationException
|
|
|
- ) {
|
|
|
- if (exception is CompromisedPasswordException) {
|
|
|
- redirectStrategy.sendRedirect(request, response, "/reset-password")
|
|
|
- return
|
|
|
- }
|
|
|
- defaultFailureHandler.onAuthenticationFailure(request, response, exception)
|
|
|
- }
|
|
|
-}
|
|
|
-----
|
|
|
-======
|
|
|
+include-code::./CompromisedPasswordCheckerUsage[tag=configuration,indent=0]
|