Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add password4j implementation of PasswordEncoder #17825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
mehrdadbozorgmehr wants to merge 1 commit into spring-projects:main
base: main
Choose a base branch
Loading
from mehrdadbozorgmehr:gh-17706
Open
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
1 change: 1 addition & 0 deletions crypto/spring-security-crypto.gradle
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
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;

/**
Expand Down Expand Up @@ -65,6 +66,10 @@ private 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
*/
Expand All @@ -87,6 +92,14 @@ public static PasswordEncoder createDelegatingPasswordEncoder() {
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));

Comment on lines +95 to +102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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));

I don't think we should be prefixing any of the algorithms with password4j because validating bcrypt work with any bcrypt implementation. Since we already have support for all of these algorithms, I'd prefer to just remove these for now.

After this ticket, we might consider adding a ticket Classpath Selection for PasswordEncoder Implementation. Before implementing it, we'd want to discuss how it would work and determine if this is something we want to do.

return new DelegatingPasswordEncoder(encodingId, encoders);
}

Expand Down
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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.*;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not use wildcard imports. You should run ./gradlew check to perform all checks (including checkstyle). You can run ./gradlew checkstyleMain checkstyleTest to just do checkstyle tests to speed things up.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @since 6.5
* @since 7.0

Since this is a new feature it would get merged with 7.0

*/
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
}

Comment on lines +47 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* 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
}

Please remove Password4jAlgorithm.

Generally, we try to avoid implementation specific APIs on our interfaces. However, behind an interface we encourage injecting the implementation directly rather than abstracting it out. This gives the users as much flexibility as possible and reduces the complexity.

/**
* 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not validate that the HashingFunction and the Password4jAlgorithm are aligned. For example, this should fail but doesn't

new Password4jPasswordEncoder(
 AlgorithmFinder.getBcryptInstance(),
 Password4jPasswordEncoder.Password4jAlgorithm.ARGON2);

As mentioned previously, I think we can remove Password4jAlgorithm all together. However, I wanted to point this out as another reason we do not want/need it.

Instead, please have a single constructor that allows injecting a non-null HashingFunction. Please be sure to document how to create instances in the javadoc. For example, defaults can be obtained from AlgorithmFinder. However, an example of a single algorithm that has been customized would be useful as well.

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);
}

Comment on lines +104 to +185
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* 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);
}

I think that all of the static factory methods can be removed. Users should be encouraged to take advantage of password4j's AlgorithmFinder API to create a HashingFunction instance. This reduces the maintenance for Spring Security and also gives users more flexibility.

@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;
}
Comment on lines +230 to +247
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* 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;
}

We generally try to avoid exposing getters as this breaks encapsulation. For example, a user may inject a Jackson ObjectMapper and that might be used internally for a while.

public class SomeClass {
 // jackson object mapper
 private ObjectMapper objectMapper;
 public SomeClass(ObjectMapper objectMapper) {
 this.objectMapper = Objects.requireNonNull(objectMapper);
 }
 public SomePojo convert(String string) {
 // ... use objectMapper for conversion
 }
 // makes refactoring hard
 public ObjectMapper getObjectMapper() {
 return this.objectMapper;
 }
}

If we expose a getObjectMapper() method, then it makes breaks encapsulation and makes refactoring more difficult. For example, if we want more flexibility we might decide to support a Converter

public class SomeClass {
 private Converter converter;
 public SomeClass(Converter converter) {
 this.converter = Objects.requireNonNull(converter);
 }
 public SomeClass(ObjectMapper objectMapper) {
 this(new MappingJackson2HttpMessageConverter(objectMapper));
 }
 public SomePojo convert(String string) {
 // ... can update to use converter because its internal
 }
 // makes refactoring hard
 public ObjectMapper getObjectMapper() {
 // Cannot be passive here because the converter replaced ObjectMapper!!
 }
}


}
View file Open in desktop
Original file line number Diff line number Diff line change
@@ -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;
Loading

AltStyle によって変換されたページ (->オリジナル) /