spring-projects/spring-security · error · IllegalArgumentException
Encoded password does not look like BCrypt: X
Error message
Encoded password does not look like BCrypt: X
What it means
upgradeEncodingNonNull re-checks an existing encoded password against the BCrypt pattern ($2a/$2y/$2b followed by rounds and a 53-char hash portion) before parsing its strength. If the stored value does not match the pattern, the library throws IllegalArgumentException, because it cannot determine the rounds of a non-BCrypt value.
Solutions
- Only call upgradeEncoding with values previously produced by BCryptPasswordEncoder (verify with a $2[aby]$ pattern first).
- Fix data migrations so the column contains full 60-char BCrypt hashes.
- Use DelegatingPasswordEncoder so non-BCrypt legacy hashes are handled by their own encoder/upgrade logic instead of this path.
Example fix
// before
if (encoder.upgradeEncoding(stored)) { ... } // throws on legacy hashes
// after
if (stored != null && stored.matches("\\$2[aby]\\$\\d{2}\\$.{53}") && encoder.upgradeEncoding(stored)) { ... } Defensive patterns
Strategy: type-guard
Validate before calling
private static final Pattern BCRYPT = Pattern.compile("\\A\\$2[aby]?\\$\\d{2}\\$[./A-Za-z0-9]{53}\\z");
boolean looksLikeBcrypt(String s) { return s != null && BCRYPT.matcher(s).matches(); } Type guard
boolean isBcryptHash(String s) { return s != null && s.startsWith("$") && BCRYPT_PATTERN.matcher(s).matches(); } Try / catch
try { needUpgrade = encoder.upgradeEncoding(stored); } catch (IllegalArgumentException e) { needUpgrade = true; // non-BCrypt legacy values always need re-encoding on next login } Prevention
- Use DelegatingPasswordEncoder so each stored format is handled by its own encoder.
- Never truncate hash columns; keep at least 60 chars for BCrypt.
- Check format before calling upgradeEncoding on data of unknown provenance.
When it happens
Trigger: Calling encoder.upgradeEncoding(storedValue) with a value that is not a BCrypt hash — e.g. a plaintext password, a SHA-256 hex digest, a null-ish/empty string, or a hash produced by another PasswordEncoder — where BCRYPT_PATTERN.matcher(...).matches() fails.
Common situations: Migrating from another encoder (DelegatingPasswordEncoder with mixed stored formats); database column truncated so the hash is cut short; storing a password with whitespace or a wrong column's data.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- Amount of performance parameters invalid
- Bad number of rounds
- Bad salt length
- Bad strength
- Cpu cost parameter must be > 1 and < 65536.
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/7dad2f2e120f9bee.
Report an issue: GitHub.
Appendix: source
Thrown at crypto/src/main/java/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java:136
private String getSalt() {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random.get());
}
@Override
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
}
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
@Override
protected boolean upgradeEncodingNonNull(String encodedPassword) {
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
}
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
/**
* Stores the default bcrypt version for use in configuration.
*
* @author Lin Feng
*/
public enum BCryptVersion {
$2A("$2a"),
$2Y("$2y"),
$2B("$2b");
View on GitHub (pinned to 96852e8860)