1
0
mirror of synced 2026-08-05 09:47:05 +00:00

Add password4j implementation of PasswordEncoder

This commit is contained in:
Rob Winch
2025-09-15 11:28:39 -05:00
committed by GitHub
30 changed files with 2515 additions and 1 deletions
@@ -463,6 +463,115 @@ There are a significant number of other `PasswordEncoder` implementations that e
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.
[[password4j]]
== Password4j-based Password Encoders
Spring Security 7.0 introduces alternative password encoder implementations based on the https://github.com/Password4j/password4j[Password4j] library.
These encoders provide additional options for popular hashing algorithms and can be used as alternatives to the existing Spring Security implementations.
The Password4j library is a Java cryptographic library that focuses on password hashing with support for multiple algorithms.
These encoders are particularly useful when you need specific algorithm configurations or want to leverage Password4j's optimizations.
All Password4j-based encoders are thread-safe and can be shared across multiple threads.
[[password4j-argon2]]
=== Argon2Password4jPasswordEncoder
The `Argon2Password4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Argon2[Argon2] algorithm via the Password4j library to hash passwords.
This provides an alternative to Spring Security's built-in `Argon2PasswordEncoder` with different configuration options and potential performance characteristics.
Argon2 is the winner of the https://en.wikipedia.org/wiki/Password_Hashing_Competition[Password Hashing Competition] and is recommended for new applications.
This implementation leverages Password4j's Argon2 support which properly includes the salt in the output hash.
Create an encoder with default settings:
.Argon2Password4jPasswordEncoder
include-code::./Argon2UsageTests[tag=default-params,indent=0]
Create an encoder with custom Argon2 parameters:
.Argon2Password4jPasswordEncoder Custom
include-code::./Argon2UsageTests[tag=custom-params,indent=0]
[[password4j-bcrypt]]
=== BcryptPassword4jPasswordEncoder
The `BcryptPassword4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Bcrypt[BCrypt] algorithm via the Password4j library to hash passwords.
This provides an alternative to Spring Security's built-in `BCryptPasswordEncoder` with Password4j's implementation characteristics.
BCrypt is a well-established password hashing algorithm that includes built-in salt generation and is resistant to rainbow table attacks.
This implementation leverages Password4j's BCrypt support which properly includes the salt in the output hash.
Create an encoder with default settings:
.BcryptPassword4jPasswordEncoder
include-code::./BcryptUsageTests[tag=default-params,indent=0]
Create an encoder with custom bcrypt parameters:
.BcryptPassword4jPasswordEncoder Custom
include-code::./BcryptUsageTests[tag=custom-params,indent=0]
[[password4j-scrypt]]
=== ScryptPassword4jPasswordEncoder
The `ScryptPassword4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/Scrypt[SCrypt] algorithm via the Password4j library to hash passwords.
This provides an alternative to Spring Security's built-in `SCryptPasswordEncoder` with Password4j's implementation characteristics.
SCrypt is a memory-hard password hashing algorithm designed to be resistant to hardware brute-force attacks.
This implementation leverages Password4j's SCrypt support which properly includes the salt in the output hash.
Create an encoder with default settings:
.ScryptPassword4jPasswordEncoder
include-code::./ScryptUsageTests[tag=default-params,indent=0]
Create an encoder with custom scrypt parameters:
.ScryptPassword4jPasswordEncoder Custom
include-code::./ScryptUsageTests[tag=custom-params,indent=0]
[[password4j-pbkdf2]]
=== Pbkdf2Password4jPasswordEncoder
The `Pbkdf2Password4jPasswordEncoder` implementation uses the https://en.wikipedia.org/wiki/PBKDF2[PBKDF2] algorithm via the Password4j library to hash passwords.
This provides an alternative to Spring Security's built-in `Pbkdf2PasswordEncoder` with explicit salt management.
PBKDF2 is a key derivation function designed to be computationally expensive to thwart dictionary and brute force attacks.
This implementation handles salt management explicitly since Password4j's PBKDF2 implementation does not include the salt in the output hash.
The encoded password format is: `+{salt}:{hash}+` where both salt and hash are Base64 encoded.
Create an encoder with default settings:
.Pbkdf2Password4jPasswordEncoder
include-code::./Pbkdf2UsageTests[tag=default-params,indent=0]
Create an encoder with custom PBKDF2 parameters:
.Pbkdf2Password4jPasswordEncoder Custom
include-code::./Pbkdf2UsageTests[tag=custom-params,indent=0]
[[password4j-ballooning]]
=== BalloonHashingPassword4jPasswordEncoder
The `BalloonHashingPassword4jPasswordEncoder` implementation uses the Balloon hashing algorithm via the Password4j library to hash passwords.
Balloon hashing is a memory-hard password hashing algorithm designed to be resistant to both time-memory trade-off attacks and side-channel attacks.
This implementation handles salt management explicitly since Password4j's Balloon hashing implementation does not include the salt in the output hash.
The encoded password format is: `+{salt}:{hash}+` where both salt and hash are Base64 encoded.
Create an encoder with default settings:
.BalloonHashingPassword4jPasswordEncoder
include-code::./BallooningHashingUsageTests[tag=default-params,indent=0]
Create an encoder with custom parameters:
.BalloonHashingPassword4jPasswordEncoder Custom
include-code::./BallooningHashingUsageTests[tag=custom-params,indent=0]
[[authentication-password-storage-configuration]]
== Password Storage Configuration
+9
View File
@@ -35,6 +35,15 @@ Java::
http.csrf((csrf) -> csrf.spa());
----
== Crypto
* Added Password4j-based password encoders providing alternative implementations for popular hashing algorithms:
** `Argon2Password4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-argon2[Argon2]
** `BcryptPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-bcrypt[BCrypt]
** `ScryptPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-scrypt[SCrypt]
** `Pbkdf2Password4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-pbkdf2[PBKDF2]
** `BalloonHashingPassword4jPasswordEncoder` - xref:features/authentication/password-storage.adoc#password4j-balloon[Balloon Hashing]
== Data
* Added support to Authorized objects for Spring Data types
+1
View File
@@ -39,6 +39,7 @@ dependencies {
testImplementation project(':spring-security-test')
testImplementation project(':spring-security-oauth2-client')
testImplementation 'com.squareup.okhttp3:mockwebserver'
testImplementation libs.com.password4j.password4j
testImplementation 'com.unboundid:unboundid-ldapsdk'
testImplementation libs.webauthn4j.core
testImplementation 'org.jetbrains.kotlin:kotlin-reflect'
@@ -0,0 +1,53 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.features.authentication.password4jargon2;
import com.password4j.Argon2Function;
import com.password4j.types.Argon2;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class Argon2UsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
}
@Test
void customParameters() {
// tag::custom-params[]
Argon2Function argon2Fn = Argon2Function.getInstance(65536, 3, 4, 32,
Argon2.ID);
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder(argon2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::custom-params[]
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.features.authentication.password4jballooning;
import com.password4j.BalloonHashingFunction;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.BalloonHashingPassword4jPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class BallooningHashingUsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
}
@Test
void customParameters() {
// tag::custom-params[]
BalloonHashingFunction ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3);
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::custom-params[]
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.features.authentication.password4jbcrypt;
import com.password4j.BcryptFunction;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class BcryptUsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
}
@Test
void customParameters() {
// tag::custom-params[]
BcryptFunction bcryptFn = BcryptFunction.getInstance(12);
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(bcryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::custom-params[]
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.features.authentication.password4jpbkdf2;
import com.password4j.PBKDF2Function;
import com.password4j.types.Hmac;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.Pbkdf2Password4jPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class Pbkdf2UsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
}
@Test
void customParameters() {
// tag::custom-params[]
PBKDF2Function pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256);
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(pbkdf2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::custom-params[]
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.docs.features.authentication.password4jscrypt;
import com.password4j.ScryptFunction;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password4j.ScryptPassword4jPasswordEncoder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Rob Winch
*/
public class ScryptUsageTests {
@Test
void defaultParams() {
// tag::default-params[]
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::default-params[]
}
@Test
void customParameters() {
// tag::custom-params[]
ScryptFunction scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32);
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(scryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
// end::custom-params[]
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.features.authentication.password4jargon2
import com.password4j.Argon2Function
import com.password4j.types.Argon2
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.Argon2Password4jPasswordEncoder
/**
* @author Rob Winch
*/
class Argon2UsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
}
@Test
fun customParameters() {
// tag::custom-params[]
val argon2Fn = Argon2Function.getInstance(
65536, 3, 4, 32,
Argon2.ID
)
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder(argon2Fn)
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
// end::custom-params[]
}
}
@@ -0,0 +1,47 @@
/*
* Copyright 2004-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.kt.docs.features.authentication.password4jballooning
import com.password4j.BalloonHashingFunction
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.BalloonHashingPassword4jPasswordEncoder
/**
* @author Rob Winch
*/
class BallooningHashingUsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
}
@Test
fun customParameters() {
// tag::custom-params[]
val ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3)
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::custom-params[]
}
}
@@ -0,0 +1,32 @@
package org.springframework.security.kt.docs.features.authentication.password4jbcrypt
import com.password4j.BcryptFunction
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.BcryptPassword4jPasswordEncoder
/**
* @author Rob Winch
*/
class BcryptUsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = BCryptPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
}
@Test
fun customParameters() {
// tag::custom-params[]
val bcryptFunction = BcryptFunction.getInstance(12)
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder(bcryptFunction)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::custom-params[]
}
}
@@ -0,0 +1,32 @@
package org.springframework.security.kt.docs.features.authentication.password4jpbkdf2
import com.password4j.PBKDF2Function
import com.password4j.types.Hmac
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.Pbkdf2Password4jPasswordEncoder
/**
* @author Rob Winch
*/
class Pbkdf2UsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
}
@Test
fun customParameters() {
// tag::custom-params[]
val pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256)
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder(pbkdf2Fn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::custom-params[]
}
}
@@ -0,0 +1,31 @@
package org.springframework.security.kt.docs.features.authentication.password4jscrypt
import com.password4j.ScryptFunction
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.crypto.password4j.ScryptPassword4jPasswordEncoder
/**
* @author Rob Winch
*/
class ScryptUsageTests {
@Test
fun defaultParams() {
// tag::default-params[]
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::default-params[]
}
@Test
fun customParameters() {
// tag::custom-params[]
val scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32)
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder(scryptFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
// end::custom-params[]
}
}