Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
302 changes: 9 additions & 293 deletions docs/modules/ROOT/pages/features/authentication/password-storage.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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]
Loading