ソースを参照

Refactor Password4jPasswordEncoder to use AlgorithmFinder for algorithm selection and enhance documentation
Closes gh-17706

Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>

Add Password4jPasswordEncoder for enhanced password hashing support

Signed-off-by: M.Bozorgmehr <m.bozorgmehr@emofid.com>
Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>

Add Password4jPasswordEncoder for enhanced password hashing support

Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>
Signed-off-by: Mehrdad <mehrdad.bozorgmehr@gmail.com>
Signed-off-by: M.Bozorgmehr <mehrdad.bozorgmehr@gmail.com>

M.Bozorgmehr 3 週間 前
コミット
bd593a63d0

+ 1 - 0
crypto/spring-security-crypto.gradle

@@ -8,6 +8,7 @@ dependencies {
 	management platform(project(":spring-security-dependencies"))
 	optional 'org.springframework:spring-core'
 	optional 'org.bouncycastle:bcpkix-jdk18on'
+	optional 'com.password4j:password4j'
 
 	testImplementation "org.assertj:assertj-core"
 	testImplementation "org.junit.jupiter:junit-jupiter-api"

+ 13 - 0
crypto/src/main/java/org/springframework/security/crypto/factory/PasswordEncoderFactories.java

@@ -24,6 +24,7 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
 import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+import org.springframework.security.crypto.password4j.Password4jPasswordEncoder;
 import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
 
 /**
@@ -65,6 +66,10 @@ public final class PasswordEncoderFactories {
 	 * <li>argon2 - {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_2()}</li>
 	 * <li>argon2@SpringSecurity_v5_8 -
 	 * {@link Argon2PasswordEncoder#defaultsForSpringSecurity_v5_8()}</li>
+	 * <li>password4j-bcrypt - {@link Password4jPasswordEncoder} with BCrypt</li>
+	 * <li>password4j-scrypt - {@link Password4jPasswordEncoder} with SCrypt</li>
+	 * <li>password4j-argon2 - {@link Password4jPasswordEncoder} with Argon2</li>
+	 * <li>password4j-pbkdf2 - {@link Password4jPasswordEncoder} with PBKDF2</li>
 	 * </ul>
 	 * @return the {@link PasswordEncoder} to use
 	 */
@@ -87,6 +92,14 @@ public final class PasswordEncoderFactories {
 		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
 		encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
 		encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
+
+		// Password4j implementations
+		encoders.put("password4j-bcrypt", Password4jPasswordEncoder.bcrypt(10));
+		encoders.put("password4j-scrypt", Password4jPasswordEncoder.scrypt(16384, 8, 1, 32));
+		encoders.put("password4j-argon2", Password4jPasswordEncoder.argon2(65536, 3, 4, 32,
+			com.password4j.types.Argon2.ID));
+		encoders.put("password4j-pbkdf2", Password4jPasswordEncoder.pbkdf2(310000, 32));
+
 		return new DelegatingPasswordEncoder(encodingId, encoders);
 	}
 

+ 249 - 0
crypto/src/main/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoder.java

@@ -0,0 +1,249 @@
+/*
+ * 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.crypto.password4j;
+
+import com.password4j.*;
+import com.password4j.types.Argon2;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.crypto.password.AbstractValidatingPasswordEncoder;
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of {@link org.springframework.security.crypto.password.PasswordEncoder} that uses the Password4j library.
+ * This encoder supports multiple password hashing algorithms including BCrypt, SCrypt, Argon2, and PBKDF2.
+ *
+ * <p>The encoder determines the algorithm used based on the algorithm type specified during construction.
+ * For verification, it can automatically detect the algorithm used in existing hashes.</p>
+ *
+ * <p>This implementation is thread-safe and can be shared across multiple threads.</p>
+ *
+ * @author Mehrdad Bozorgmehr
+ * @since 6.5
+ */
+public class Password4jPasswordEncoder extends AbstractValidatingPasswordEncoder {
+
+	private final Log logger = LogFactory.getLog(getClass());
+
+	private final HashingFunction hashingFunction;
+
+	private final Password4jAlgorithm algorithm;
+
+
+	/**
+	 * Enumeration of supported Password4j algorithms.
+	 */
+	public enum Password4jAlgorithm {
+		/**
+		 * BCrypt algorithm.
+		 */
+		BCRYPT,
+		/**
+		 * SCrypt algorithm.
+		 */
+		SCRYPT,
+		/**
+		 * Argon2 algorithm.
+		 */
+		ARGON2,
+		/**
+		 * PBKDF2 algorithm.
+		 */
+		PBKDF2,
+		/**
+		 * Compressed PBKDF2 algorithm.
+		 */
+		COMPRESSED_PBKDF2
+	}
+
+	/**
+	 * Constructs a Password4j password encoder with the default BCrypt algorithm.
+	 */
+	public Password4jPasswordEncoder() {
+		this(Password4jAlgorithm.BCRYPT);
+	}
+
+	/**
+	 * Constructs a Password4j password encoder with the specified algorithm using default parameters.
+	 *
+	 * @param algorithm the password hashing algorithm to use
+	 */
+	public Password4jPasswordEncoder(Password4jAlgorithm algorithm) {
+		Assert.notNull(algorithm, "algorithm cannot be null");
+		this.algorithm = algorithm;
+		this.hashingFunction = createDefaultHashingFunction(algorithm);
+	}
+
+	/**
+	 * Constructs a Password4j password encoder with a custom hashing function.
+	 *
+	 * @param hashingFunction the custom hashing function to use
+	 * @param algorithm       the password hashing algorithm type
+	 */
+	public Password4jPasswordEncoder(HashingFunction hashingFunction, Password4jAlgorithm algorithm) {
+		Assert.notNull(hashingFunction, "hashingFunction cannot be null");
+		Assert.notNull(algorithm, "algorithm cannot be null");
+		this.hashingFunction = hashingFunction;
+		this.algorithm = algorithm;
+	}
+
+	/**
+	 * Creates a Password4j password encoder with BCrypt algorithm and specified rounds.
+	 *
+	 * @param rounds the number of rounds (cost factor) for BCrypt
+	 * @return a new Password4j password encoder
+	 */
+	public static Password4jPasswordEncoder bcrypt(int rounds) {
+		return new Password4jPasswordEncoder(BcryptFunction.getInstance(rounds), Password4jAlgorithm.BCRYPT);
+	}
+
+	/**
+	 * Creates a Password4j password encoder with SCrypt algorithm and specified parameters.
+	 *
+	 * @param workFactor       the work factor (N parameter)
+	 * @param resources        the resources (r parameter)
+	 * @param parallelization  the parallelization (p parameter)
+	 * @param derivedKeyLength the derived key length
+	 * @return a new Password4j password encoder
+	 */
+	public static Password4jPasswordEncoder scrypt(int workFactor, int resources, int parallelization, int derivedKeyLength) {
+		return new Password4jPasswordEncoder(
+				ScryptFunction.getInstance(workFactor, resources, parallelization, derivedKeyLength),
+				Password4jAlgorithm.SCRYPT
+		);
+	}
+
+	/**
+	 * Creates a Password4j password encoder with Argon2 algorithm and specified parameters.
+	 *
+	 * @param memory       the memory cost
+	 * @param iterations   the number of iterations
+	 * @param parallelism  the parallelism
+	 * @param outputLength the output length
+	 * @param type         the Argon2 type
+	 * @return a new Password4j password encoder
+	 */
+	public static Password4jPasswordEncoder argon2(int memory, int iterations, int parallelism, int outputLength, Argon2 type) {
+		return new Password4jPasswordEncoder(
+				Argon2Function.getInstance(memory, iterations, parallelism, outputLength, type),
+				Password4jAlgorithm.ARGON2
+		);
+	}
+
+	/**
+	 * Creates a Password4j password encoder with PBKDF2 algorithm and specified parameters.
+	 *
+	 * @param iterations       the number of iterations
+	 * @param derivedKeyLength the derived key length
+	 * @return a new Password4j password encoder
+	 */
+	public static Password4jPasswordEncoder pbkdf2(int iterations, int derivedKeyLength) {
+		return new Password4jPasswordEncoder(
+				CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
+				Password4jAlgorithm.PBKDF2
+		);
+	}
+
+	/**
+	 * Creates a Password4j password encoder with compressed PBKDF2 algorithm.
+	 *
+	 * @param iterations       the number of iterations
+	 * @param derivedKeyLength the derived key length
+	 * @return a new Password4j password encoder
+	 */
+	public static Password4jPasswordEncoder compressedPbkdf2(int iterations, int derivedKeyLength) {
+		return new Password4jPasswordEncoder(
+				CompressedPBKDF2Function.getInstance("SHA256", iterations, derivedKeyLength),
+				Password4jAlgorithm.COMPRESSED_PBKDF2
+		);
+	}
+
+	/**
+	 * Creates a Password4j password encoder with default settings for Spring Security v5.8+.
+	 * This uses BCrypt with 10 rounds.
+	 *
+	 * @return a new Password4j password encoder with recommended defaults
+	 * @since 6.5
+	 */
+	public static Password4jPasswordEncoder defaultsForSpringSecurity() {
+		return bcrypt(10);
+	}
+
+	@Override
+	protected String encodeNonNullPassword(String rawPassword) {
+		try {
+			Hash hash = Password.hash(rawPassword).with(this.hashingFunction);
+			return hash.getResult();
+		} catch (Exception ex) {
+			throw new IllegalStateException("Failed to encode password using Password4j", ex);
+		}
+	}
+
+	@Override
+	protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
+		try {
+			// Use the specific hashing function for verification
+			return Password.check(rawPassword, encodedPassword).with(this.hashingFunction);
+		} catch (Exception ex) {
+			this.logger.warn("Password verification failed for encoded password: " + encodedPassword, ex);
+			return false;
+		}
+	}
+
+	@Override
+	protected boolean upgradeEncodingNonNull(String encodedPassword) {
+		// Password4j handles upgrade detection internally for most algorithms
+		// For now, we'll return false to maintain existing behavior
+		return false;
+	}
+
+	/**
+	 * Creates a default hashing function for the specified algorithm.
+	 *
+	 * @param algorithm the password hashing algorithm
+	 * @return the default hashing function
+	 */
+	private static HashingFunction createDefaultHashingFunction(Password4jAlgorithm algorithm) {
+        return switch (algorithm) {
+            case BCRYPT -> BcryptFunction.getInstance(10); // Default 10 rounds
+            case SCRYPT -> ScryptFunction.getInstance(16384, 8, 1, 32); // Default parameters
+            case ARGON2 -> Argon2Function.getInstance(65536, 3, 4, 32, Argon2.ID); // Default parameters
+            case PBKDF2 ->
+                    CompressedPBKDF2Function.getInstance("SHA256", 310000, 32); // Use compressed format for self-contained encoding
+            case COMPRESSED_PBKDF2 -> CompressedPBKDF2Function.getInstance("SHA256", 310000, 32);
+        };
+	}
+
+	/**
+	 * Gets the algorithm used by this encoder.
+	 *
+	 * @return the password hashing algorithm
+	 */
+	public Password4jAlgorithm getAlgorithm() {
+		return this.algorithm;
+	}
+
+	/**
+	 * Gets the hashing function used by this encoder.
+	 *
+	 * @return the hashing function
+	 */
+	public HashingFunction getHashingFunction() {
+		return this.hashingFunction;
+	}
+
+}

+ 21 - 0
crypto/src/main/java/org/springframework/security/crypto/password4j/package-info.java

@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+
+@NullMarked
+package org.springframework.security.crypto.password4j;
+
+import org.jspecify.annotations.NullMarked;

+ 596 - 0
crypto/src/test/java/org/springframework/security/crypto/password4j/Password4jPasswordEncoderTests.java

@@ -0,0 +1,596 @@
+/*
+ * 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.crypto.password4j;
+
+import com.password4j.Argon2Function;
+import com.password4j.BcryptFunction;
+import com.password4j.CompressedPBKDF2Function;
+import com.password4j.ScryptFunction;
+import com.password4j.types.Argon2;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+/**
+ * Tests for {@link Password4jPasswordEncoder}.
+ *
+ * @author Mehrdad Bozorgmehr
+ */
+class Password4jPasswordEncoderTests {
+
+    private static final String PASSWORD = "password";
+    private static final String WRONG_PASSWORD = "wrongpassword";
+    private static final String UNICODE_PASSWORD = "пароль123🔐";
+    private static final String LONG_PASSWORD = "a".repeat(1000);
+
+    // Constructor Tests
+    @Test
+    void constructorWithNullAlgorithmShouldThrowException() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> new Password4jPasswordEncoder(null))
+                .withMessage("algorithm cannot be null");
+    }
+
+    @Test
+    void constructorWithNullHashingFunctionShouldThrowException() {
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> new Password4jPasswordEncoder(null, Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT))
+                .withMessage("hashingFunction cannot be null");
+    }
+
+    @Test
+    void constructorWithNullAlgorithmAndValidHashingFunctionShouldThrowException() {
+        BcryptFunction function = BcryptFunction.getInstance(10);
+        assertThatIllegalArgumentException()
+                .isThrownBy(() -> new Password4jPasswordEncoder(function, null))
+                .withMessage("algorithm cannot be null");
+    }
+
+    @Test
+    void defaultConstructorShouldUseBCrypt() {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
+        assertThat(encoder.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
+        assertThat(encoder.getHashingFunction()).isInstanceOf(BcryptFunction.class);
+    }
+
+    // BCrypt Tests
+    @Test
+    void bcryptEncoderShouldEncodeAndVerifyPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(10);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded)
+                .isNotNull()
+                .isNotEqualTo(PASSWORD)
+                .startsWith("$2b$10$");// Password4j uses $2b$ format
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+        assertThat(encoder.matches(null, encoded)).isFalse();
+        assertThat(encoder.matches(PASSWORD, null)).isFalse();
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {4, 6, 8, 10, 12, 14})
+    void bcryptWithDifferentRoundsShouldWork(int rounds) {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(rounds);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded).startsWith("$2b$" + String.format("%02d", rounds) + "$");
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    @Test
+    void bcryptShouldProduceDifferentHashesForSamePassword() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.bcrypt(10);
+
+        String hash1 = encoder.encode(PASSWORD);
+        String hash2 = encoder.encode(PASSWORD);
+
+        assertThat(hash1).isNotEqualTo(hash2);
+        assertThat(encoder.matches(PASSWORD, hash1)).isTrue();
+        assertThat(encoder.matches(PASSWORD, hash2)).isTrue();
+    }
+
+    // SCrypt Tests
+    @Test
+    void scryptEncoderShouldEncodeAndVerifyPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.scrypt(16384, 8, 1, 32);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded).isNotNull().isNotEqualTo(PASSWORD);
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+    }
+
+    @Test
+    void scryptWithDifferentParametersShouldWork() {
+        Password4jPasswordEncoder encoder1 = Password4jPasswordEncoder.scrypt(8192, 8, 1, 32);
+        Password4jPasswordEncoder encoder2 = Password4jPasswordEncoder.scrypt(16384, 16, 2, 64);
+
+        String hash1 = encoder1.encode(PASSWORD);
+        String hash2 = encoder2.encode(PASSWORD);
+
+        assertThat(encoder1.matches(PASSWORD, hash1)).isTrue();
+        assertThat(encoder2.matches(PASSWORD, hash2)).isTrue();
+        assertThat(hash1).isNotEqualTo(hash2);
+    }
+
+    // Argon2 Tests
+    @Test
+    void argon2EncoderShouldEncodeAndVerifyPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(
+                65536, 3, 4, 32, Argon2.ID);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded)
+                .isNotNull()
+                .isNotEqualTo(PASSWORD)
+                .startsWith("$argon2id$");
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+    }
+
+    @ParameterizedTest
+    @EnumSource(Argon2.class)
+    void argon2WithDifferentTypesShouldWork(Argon2 type) {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(
+                65536, 3, 4, 32, type);
+
+        String encoded = encoder.encode(PASSWORD);
+        String expectedPrefix = switch (type) {
+            case D -> "$argon2d$";
+            case I -> "$argon2i$";
+            case ID -> "$argon2id$";
+        };
+
+        assertThat(encoded).startsWith(expectedPrefix);
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    // PBKDF2 Tests
+    @Test
+    void pbkdf2EncoderShouldEncodeAndVerifyPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.pbkdf2(100000, 32);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded)
+                .isNotNull()
+                .isNotEqualTo(PASSWORD);
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+    }
+
+    @Test
+    void compressedPbkdf2EncoderShouldEncodeAndVerifyPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.compressedPbkdf2(100000, 32);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded)
+                .isNotNull()
+                .isNotEqualTo(PASSWORD);
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+    }
+
+    @ParameterizedTest
+    @CsvSource({
+            "50000, 16",
+            "100000, 32",
+            "200000, 64",
+            "500000, 32"
+    })
+    void pbkdf2WithDifferentParametersShouldWork(int iterations, int keyLength) {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.pbkdf2(iterations, keyLength);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    // Factory Method Tests
+    @Test
+    void defaultsForSpringSecurityShouldUseBCrypt() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        assertThat(encoder.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
+        assertThat(encoder.getHashingFunction()).isInstanceOf(BcryptFunction.class);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded).startsWith("$2b$10$"); // Password4j uses $2b$ format
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    // Null and Empty Input Tests
+    @Test
+    void encodeNullPasswordShouldReturnNull() {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
+        assertThat(encoder.encode(null)).isNull();
+    }
+
+    @Test
+    void encodeEmptyPasswordShouldWork() {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
+        String encoded = encoder.encode("");
+        assertThat(encoded).isNotNull();
+        // AbstractValidatingPasswordEncoder returns false for empty raw passwords
+        assertThat(encoder.matches("", encoded)).isFalse();
+    }
+
+    @Test
+    void matchesWithNullOrEmptyParametersShouldReturnFalse() {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
+        String validHash = encoder.encode(PASSWORD);
+
+        assertThat(encoder.matches(null, validHash)).isFalse();
+        assertThat(encoder.matches("", validHash)).isFalse();
+        assertThat(encoder.matches(PASSWORD, null)).isFalse();
+        assertThat(encoder.matches(PASSWORD, "")).isFalse();
+        assertThat(encoder.matches(null, null)).isFalse();
+        assertThat(encoder.matches("", "")).isFalse();
+    }
+
+    // Password Variety Tests
+    @ParameterizedTest
+    @ValueSource(strings = {"password", "123456", "P@ssw0rd!", "a very long password with spaces and symbols !@#$%"})
+    void shouldHandleVariousPasswordFormats(String password) {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String encoded = encoder.encode(password);
+        assertThat(encoded).isNotNull();
+        assertThat(encoder.matches(password, encoded)).isTrue();
+        assertThat(encoder.matches(password + "x", encoded)).isFalse();
+    }
+
+    @Test
+    void shouldHandleUnicodePasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String encoded = encoder.encode(UNICODE_PASSWORD);
+        assertThat(encoded).isNotNull();
+        assertThat(encoder.matches(UNICODE_PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches("password", encoded)).isFalse();
+    }
+
+    @Test
+    void shouldHandleLongPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String encoded = encoder.encode(LONG_PASSWORD);
+        assertThat(encoded).isNotNull();
+        assertThat(encoder.matches(LONG_PASSWORD, encoded)).isTrue();
+    }
+
+    // Upgrade Encoding Tests
+    @Test
+    void upgradeEncodingShouldReturnFalse() {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder();
+        String encoded = encoder.encode(PASSWORD);
+
+        // For now, upgradeEncoding should return false
+        assertThat(encoder.upgradeEncoding(encoded)).isFalse();
+        assertThat(encoder.upgradeEncoding(null)).isFalse();
+        assertThat(encoder.upgradeEncoding("")).isFalse();
+    }
+
+    @ParameterizedTest
+    @EnumSource(Password4jPasswordEncoder.Password4jAlgorithm.class)
+    void upgradeEncodingShouldReturnFalseForAllAlgorithms(Password4jPasswordEncoder.Password4jAlgorithm algorithm) {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
+        String encoded = encoder.encode(PASSWORD);
+
+        assertThat(encoder.upgradeEncoding(encoded)).isFalse();
+    }
+
+    // Custom Hashing Function Tests
+    @Test
+    void shouldWorkWithCustomHashingFunction() {
+        BcryptFunction customFunction = BcryptFunction.getInstance(12);
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded).startsWith("$2b$12$"); // Password4j uses $2b$ format
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    @Test
+    void shouldWorkWithCustomScryptFunction() {
+        ScryptFunction customFunction = ScryptFunction.getInstance(32768, 16, 2, 64);
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.SCRYPT);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    @Test
+    void shouldWorkWithCustomArgon2Function() {
+        Argon2Function customFunction = Argon2Function.getInstance(131072, 4, 8, 64, Argon2.ID);
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(customFunction, Password4jPasswordEncoder.Password4jAlgorithm.ARGON2);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded).startsWith("$argon2id$");
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    // Algorithm Coverage Tests
+    @Test
+    void shouldCreateEncoderForEachAlgorithm() {
+        // Test all algorithm types can be instantiated
+        for (Password4jPasswordEncoder.Password4jAlgorithm algorithm : Password4jPasswordEncoder.Password4jAlgorithm.values()) {
+            Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
+            assertThat(encoder.getAlgorithm()).isEqualTo(algorithm);
+
+            String encoded = encoder.encode(PASSWORD);
+            assertThat(encoded).isNotNull();
+            assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        }
+    }
+
+    @ParameterizedTest
+    @EnumSource(Password4jPasswordEncoder.Password4jAlgorithm.class)
+    void allAlgorithmsShouldProduceValidHashes(Password4jPasswordEncoder.Password4jAlgorithm algorithm) {
+        Password4jPasswordEncoder encoder = new Password4jPasswordEncoder(algorithm);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoded)
+                .isNotNull()
+                .isNotEmpty()
+                .isNotEqualTo(PASSWORD);
+
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        assertThat(encoder.matches(WRONG_PASSWORD, encoded)).isFalse();
+    }
+
+    // Security Properties Tests
+    @RepeatedTest(10)
+    void samePasswordShouldProduceDifferentHashes() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String hash1 = encoder.encode(PASSWORD);
+        String hash2 = encoder.encode(PASSWORD);
+
+        // Hashes should be different (due to salt)
+        assertThat(hash1).isNotEqualTo(hash2);
+
+        // But both should verify correctly
+        assertThat(encoder.matches(PASSWORD, hash1)).isTrue();
+        assertThat(encoder.matches(PASSWORD, hash2)).isTrue();
+    }
+
+    @Test
+    void hashLengthShouldBeConsistent() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String hash1 = encoder.encode("short");
+        String hash2 = encoder.encode("this is a much longer password with many characters");
+
+        // BCrypt hashes should have consistent length
+        assertThat(hash1).hasSize(60); // BCrypt produces 60-character hashes
+        assertThat(hash2).hasSize(60);
+    }
+
+    @Test
+    void similarPasswordsShouldProduceCompletelyDifferentHashes() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        String hash1 = encoder.encode("password");
+        String hash2 = encoder.encode("password1");
+        String hash3 = encoder.encode("Password");
+
+        assertThat(hash1)
+                .isNotEqualTo(hash2)
+                .isNotEqualTo(hash3);
+        assertThat(hash2).isNotEqualTo(hash3);
+
+        // Cross-verification should fail
+        assertThat(encoder.matches("password", hash2)).isFalse();
+        assertThat(encoder.matches("password1", hash1)).isFalse();
+    }
+
+
+    // Additional Security and Robustness Tests
+    @Test
+    void shouldHandleVeryLongPasswords() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+        String veryLongPassword = "a".repeat(10000); // 10KB password
+
+        String encoded = encoder.encode(veryLongPassword);
+        assertThat(encoded).isNotNull();
+        assertThat(encoder.matches(veryLongPassword, encoded)).isTrue();
+        // Fix: BCrypt truncates passwords longer than 72 bytes, so we need to test with a meaningful difference
+        // Test with a shorter difference that's within the 72-byte limit
+        String slightlyDifferentPassword = "b" + veryLongPassword.substring(1); // Change first character
+        assertThat(encoder.matches(slightlyDifferentPassword, encoded)).isFalse();
+    }
+
+    @Test
+    void shouldHandlePasswordsWithNullBytes() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+        String passwordWithNull = "password\u0000test";
+
+        String encoded = encoder.encode(passwordWithNull);
+        assertThat(encoded).isNotNull();
+        assertThat(encoder.matches(passwordWithNull, encoded)).isTrue();
+        assertThat(encoder.matches("passwordtest", encoded)).isFalse();
+    }
+
+    @Test
+    void shouldProduceStrongRandomness() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+        java.util.Set<String> hashes = new java.util.HashSet<>();
+
+        // Generate many hashes of the same password
+        for (int i = 0; i < 100; i++) {
+            String hash = encoder.encode(PASSWORD);
+            assertThat(hashes.add(hash)).isTrue(); // Each hash should be unique
+        }
+
+        assertThat(hashes).hasSize(100);
+    }
+
+    @Test
+    void shouldResistTimingAttacks() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+        String validHash = encoder.encode(PASSWORD);
+
+        // Measure time for correct password
+        long startTime = System.nanoTime();
+        boolean result1 = encoder.matches(PASSWORD, validHash);
+        long correctTime = System.nanoTime() - startTime;
+
+        // Measure time for wrong password of same length
+        startTime = System.nanoTime();
+        boolean result2 = encoder.matches("passwore", validHash); // Same length, different content
+        long wrongTime = System.nanoTime() - startTime;
+
+        assertThat(result1).isTrue();
+        assertThat(result2).isFalse();
+
+        // Times should be relatively close (within 10x factor for timing attack resistance)
+        double ratio = Math.max(correctTime, wrongTime) / (double) Math.min(correctTime, wrongTime);
+        assertThat(ratio).isLessThan(10.0);
+    }
+
+
+    @Test
+    void scryptShouldHandleEdgeCaseParameters() {
+        // Test with minimum viable parameters
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.scrypt(2, 1, 1, 16);
+
+        String encoded = encoder.encode(PASSWORD);
+        assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+    }
+
+    @Test
+    void argon2ShouldWorkWithDifferentMemorySizes() {
+        // Test with various memory configurations
+        int[] memorySizes = {1024, 4096, 16384, 65536};
+
+        for (int memory : memorySizes) {
+            Password4jPasswordEncoder encoder = Password4jPasswordEncoder.argon2(memory, 2, 1, 32, Argon2.ID);
+            String encoded = encoder.encode(PASSWORD);
+            assertThat(encoder.matches(PASSWORD, encoded)).isTrue();
+        }
+    }
+
+    @Test
+    void pbkdf2ShouldWorkWithDifferentHashAlgorithms() {
+        // Test that the implementation handles different internal configurations
+        Password4jPasswordEncoder encoder1 = Password4jPasswordEncoder.pbkdf2(50000, 16);
+        Password4jPasswordEncoder encoder2 = Password4jPasswordEncoder.pbkdf2(100000, 32);
+        Password4jPasswordEncoder encoder3 = Password4jPasswordEncoder.pbkdf2(200000, 64);
+
+        String hash1 = encoder1.encode(PASSWORD);
+        String hash2 = encoder2.encode(PASSWORD);
+        String hash3 = encoder3.encode(PASSWORD);
+
+        assertThat(encoder1.matches(PASSWORD, hash1)).isTrue();
+        assertThat(encoder2.matches(PASSWORD, hash2)).isTrue();
+        assertThat(encoder3.matches(PASSWORD, hash3)).isTrue();
+
+        // Hashes should be different due to different parameters
+        assertThat(hash1).isNotEqualTo(hash2);
+        assertThat(hash2).isNotEqualTo(hash3);
+    }
+
+    // Cross-Algorithm Verification Tests
+    @Test
+    void differentAlgorithmsShouldNotCrossVerify() {
+        Password4jPasswordEncoder bcryptEncoder = Password4jPasswordEncoder.bcrypt(10);
+        Password4jPasswordEncoder scryptEncoder = Password4jPasswordEncoder.scrypt(16384, 8, 1, 32);
+        Password4jPasswordEncoder argon2Encoder = Password4jPasswordEncoder.argon2(65536, 3, 4, 32, Argon2.ID);
+
+        String bcryptHash = bcryptEncoder.encode(PASSWORD);
+        String scryptHash = scryptEncoder.encode(PASSWORD);
+        String argon2Hash = argon2Encoder.encode(PASSWORD);
+
+        // Each encoder should only verify its own hashes
+        assertThat(bcryptEncoder.matches(PASSWORD, bcryptHash)).isTrue();
+        assertThat(bcryptEncoder.matches(PASSWORD, scryptHash)).isFalse();
+        assertThat(bcryptEncoder.matches(PASSWORD, argon2Hash)).isFalse();
+
+        assertThat(scryptEncoder.matches(PASSWORD, scryptHash)).isTrue();
+        assertThat(scryptEncoder.matches(PASSWORD, bcryptHash)).isFalse();
+        assertThat(scryptEncoder.matches(PASSWORD, argon2Hash)).isFalse();
+
+        assertThat(argon2Encoder.matches(PASSWORD, argon2Hash)).isTrue();
+        assertThat(argon2Encoder.matches(PASSWORD, bcryptHash)).isFalse();
+        assertThat(argon2Encoder.matches(PASSWORD, scryptHash)).isFalse();
+    }
+
+
+    @Test
+    void encodingShouldCompleteInReasonableTime() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        long startTime = System.currentTimeMillis();
+        String encoded = encoder.encode(PASSWORD);
+        long duration = System.currentTimeMillis() - startTime;
+
+        assertThat(encoded).isNotNull();
+        assertThat(duration).isLessThan(5000); // Should complete within 5 seconds
+    }
+
+    // Compatibility and Integration Tests
+    @Test
+    void shouldBeCompatibleWithSpringSecurityConventions() {
+        Password4jPasswordEncoder encoder = Password4jPasswordEncoder.defaultsForSpringSecurity();
+
+        // Test common Spring Security patterns
+        assertThat(encoder.encode(null)).isNull();
+        assertThat(encoder.matches(null, "hash")).isFalse();
+        assertThat(encoder.matches("password", null)).isFalse();
+        assertThat(encoder.upgradeEncoding("anyhash")).isFalse();
+
+        // Test that it follows AbstractValidatingPasswordEncoder contract
+        assertThat(encoder.matches("", "")).isFalse();
+        assertThat(encoder.upgradeEncoding("")).isFalse();
+    }
+
+    @Test
+    void factoryMethodsShouldCreateCorrectInstances() {
+        // Verify all factory methods create properly configured instances
+        Password4jPasswordEncoder bcrypt = Password4jPasswordEncoder.bcrypt(12);
+        assertThat(bcrypt.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.BCRYPT);
+        assertThat(bcrypt.getHashingFunction()).isInstanceOf(BcryptFunction.class);
+
+        Password4jPasswordEncoder scrypt = Password4jPasswordEncoder.scrypt(32768, 8, 1, 32);
+        assertThat(scrypt.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.SCRYPT);
+        assertThat(scrypt.getHashingFunction()).isInstanceOf(ScryptFunction.class);
+
+        Password4jPasswordEncoder argon2 = Password4jPasswordEncoder.argon2(65536, 3, 4, 32, Argon2.ID);
+        assertThat(argon2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.ARGON2);
+        assertThat(argon2.getHashingFunction()).isInstanceOf(Argon2Function.class);
+
+        Password4jPasswordEncoder pbkdf2 = Password4jPasswordEncoder.pbkdf2(100000, 32);
+        assertThat(pbkdf2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.PBKDF2);
+        assertThat(pbkdf2.getHashingFunction()).isInstanceOf(CompressedPBKDF2Function.class);
+
+        Password4jPasswordEncoder compressedPbkdf2 = Password4jPasswordEncoder.compressedPbkdf2(100000, 32);
+        assertThat(compressedPbkdf2.getAlgorithm()).isEqualTo(Password4jPasswordEncoder.Password4jAlgorithm.COMPRESSED_PBKDF2);
+        assertThat(compressedPbkdf2.getHashingFunction()).isInstanceOf(CompressedPBKDF2Function.class);
+    }
+}

+ 160 - 0
crypto/src/test/java/org/springframework/security/crypto/password4j/PasswordCompatibilityTests.java

@@ -0,0 +1,160 @@
+/*
+ * 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.crypto.password4j;
+
+import com.password4j.AlgorithmFinder;
+import com.password4j.Argon2Function;
+import com.password4j.BcryptFunction;
+import com.password4j.CompressedPBKDF2Function;
+import com.password4j.ScryptFunction;
+import com.password4j.types.Argon2;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
+import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests compatibility between existing Spring Security password encoders and
+ * {@link Password4jPasswordEncoder}.
+ *
+ * @author Mehrdad Bozorgmehr
+ */
+class PasswordCompatibilityTests {
+
+	private static final String PASSWORD = "password";
+
+	// BCrypt Compatibility Tests
+	@Test
+	void bcryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
+		BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10));
+
+		String encodedBySpring = springEncoder.encode(PASSWORD);
+		boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
+
+		assertThat(matchedByPassword4j).isTrue();
+	}
+
+	@Test
+	void bcryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
+		BCryptPasswordEncoder springEncoder = new BCryptPasswordEncoder(10);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10));
+
+		String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
+		boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
+
+		assertThat(matchedBySpring).isTrue();
+	}
+
+	// SCrypt Compatibility Tests
+	@Test
+	void scryptEncodedWithSpringSecurityShouldMatchWithPassword4j() {
+		SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
+				ScryptFunction.getInstance(16384, 8, 1, 32));
+
+		String encodedBySpring = springEncoder.encode(PASSWORD);
+		boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
+
+		assertThat(matchedByPassword4j).isTrue();
+	}
+
+	@Test
+	void scryptEncodedWithPassword4jShouldMatchWithSpringSecurity() {
+		SCryptPasswordEncoder springEncoder = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
+				ScryptFunction.getInstance(16384, 8, 1, 32));
+
+		String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
+		boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
+
+		assertThat(matchedBySpring).isTrue();
+	}
+
+	// Argon2 Compatibility Tests
+	@Test
+	void argon2EncodedWithSpringSecurityShouldMatchWithPassword4j() {
+		Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 65536, 3);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
+				Argon2Function.getInstance(65536, 3, 1, 32, Argon2.ID));
+
+		String encodedBySpring = springEncoder.encode(PASSWORD);
+		boolean matchedByPassword4j = password4jEncoder.matches(PASSWORD, encodedBySpring);
+
+		assertThat(matchedByPassword4j).isTrue();
+	}
+
+	@Test
+	void argon2EncodedWithPassword4jShouldMatchWithSpringSecurity() {
+		Argon2PasswordEncoder springEncoder = new Argon2PasswordEncoder(16, 32, 1, 65536, 3);
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
+				Argon2Function.getInstance(65536, 3, 1, 32, Argon2.ID));
+
+		String encodedByPassword4j = password4jEncoder.encode(PASSWORD);
+		boolean matchedBySpring = springEncoder.matches(PASSWORD, encodedByPassword4j);
+
+		assertThat(matchedBySpring).isTrue();
+	}
+
+	// PBKDF2 Compatibility Tests - Note: Different format implementations
+	@Test
+	void pbkdf2BasicFunctionalityTest() {
+		// Test that both encoders work independently with their own formats
+		// Spring Security PBKDF2
+		Pbkdf2PasswordEncoder springEncoder = new Pbkdf2PasswordEncoder("", 16, 100000,
+				Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA256);
+		String springEncoded = springEncoder.encode(PASSWORD);
+		assertThat(springEncoder.matches(PASSWORD, springEncoded)).isTrue();
+
+		// Password4j PBKDF2
+		Password4jPasswordEncoder password4jEncoder = new Password4jPasswordEncoder(
+				CompressedPBKDF2Function.getInstance("SHA256", 100000, 32));
+		String password4jEncoded = password4jEncoder.encode(PASSWORD);
+		assertThat(password4jEncoder.matches(PASSWORD, password4jEncoded)).isTrue();
+
+		// Note: These encoders use different hash formats and are not cross-compatible
+		// This is expected behavior due to different implementation standards
+	}
+
+	// Cross-Algorithm Tests (should fail)
+	@Test
+	void bcryptEncodedPasswordShouldNotMatchArgon2Encoder() {
+		Password4jPasswordEncoder bcryptEncoder = new Password4jPasswordEncoder(BcryptFunction.getInstance(10));
+		Password4jPasswordEncoder argon2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance());
+
+		String bcryptEncoded = bcryptEncoder.encode(PASSWORD);
+		boolean matchedByArgon2 = argon2Encoder.matches(PASSWORD, bcryptEncoded);
+
+		assertThat(matchedByArgon2).isFalse();
+	}
+
+	@Test
+	void argon2EncodedPasswordShouldNotMatchScryptEncoder() {
+		Password4jPasswordEncoder argon2Encoder = new Password4jPasswordEncoder(AlgorithmFinder.getArgon2Instance());
+		Password4jPasswordEncoder scryptEncoder = new Password4jPasswordEncoder(AlgorithmFinder.getScryptInstance());
+
+		String argon2Encoded = argon2Encoder.encode(PASSWORD);
+		boolean matchedByScrypt = scryptEncoder.matches(PASSWORD, argon2Encoded);
+
+		assertThat(matchedByScrypt).isFalse();
+	}
+
+}

+ 1 - 1
dependencies/spring-security-dependencies.gradle

@@ -78,6 +78,6 @@ dependencies {
 		api libs.org.apache.maven.resolver.maven.resolver.transport.http
 		api libs.org.apache.maven.maven.resolver.provider
 		api libs.org.instancio.instancio.junit
+		api libs.com.password4j.password4j
 	}
 }
-