spring-projects/spring-security · warning
Malformed password hash
Error message
Malformed password hash
What it means
Argon2PasswordEncoder.matchesNonNull logs this warning when Argon2EncodingUtils.decode(encodedPassword) throws IllegalArgumentException, i.e. the stored encoded password is not a valid modular-crypt-format Argon2 hash. The encoder cannot extract the salt/parameters from the string, so it returns false instead of throwing — the match simply fails.
Solutions
- Verify the stored hash starts with $argon2id$ (or the variant you configured) and is complete/untruncated.
- Check the DB column length; Argon2 hashes are ~97+ chars, so widen the column if the hash was cut off.
- If legacy hashes coexist, use DelegatingPasswordEncoder (PasswordEncoderFactories.createDelegatingPasswordEncoder) so each hash is decoded by the right encoder.
- Re-encode the affected accounts' passwords (e.g. via a password-reset flow) so they are stored in Argon2 format.
Example fix
// before
boolean ok = new Argon2PasswordEncoder(16, 32, 1, 16384, 2).matches(raw, legacyBcryptHash);
// after
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
boolean ok = encoder.matches(raw, "{bcrypt}" + legacyBcryptHash); Defensive patterns
Strategy: validation
Validate before calling
boolean validArgon2Hash = encoded != null && encoded.matches("\\$argon2(id|i|d)\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$[A-Za-z0-9+/]+\\$[A-Za-z0-9+/]+");
if (!validArgon2Hash) { /* treat as no-match / migrate account */ } Try / catch
matches() never throws for malformed hashes (it returns false and logs); treat a persistent false as a signal to inspect/re-encode the stored hash rather than catching an exception.
Prevention
- Store hashes only from passwordEncoder.encode(); never insert plaintext or other formats into the password column.
- Size DB columns to at least 120 chars to avoid truncated Argon2 hashes.
- Use DelegatingPasswordEncoder when multiple hash formats coexist.
- Watch for this log warning in monitoring — repeated occurrences mean data, not user error.
When it happens
Trigger: Calling passwordEncoder.matches(rawPassword, encodedPassword) where encodedPassword was not produced by Argon2PasswordEncoder.encode (e.g. it is a plaintext value, a BCrypt/MD5 hash, a truncated hash, or a hash produced with a different variant like $argon2i instead of $argon2id).
Common situations: Migrating a legacy user database whose password column holds non-Argon2 hashes; a column truncated by a too-short VARCHAR; users whose password was stored plaintext; switching encoders without a DelegatingPasswordEncoder.
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.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Encoded password does not look like BCrypt
- AccountStatusUserDetailsChecker.disabled
- AccountStatusUserDetailsChecker.expired
- Amount of performance parameters invalid
- An Authentication object was not found in the…
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/8d05ffc885502ab7.
Report an issue: GitHub.
Appendix: source
Thrown at crypto/src/main/java/org/springframework/security/crypto/argon2/Argon2PasswordEncoder.java:136
.withParallelism(this.parallelism)
.withMemoryAsKB(this.memory)
.withIterations(this.iterations)
.build();
// @formatter:on
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(params);
generator.generateBytes(rawPassword.toString().toCharArray(), hash);
return Argon2EncodingUtils.encode(hash, params);
}
@Override
protected boolean matchesNonNull(String rawPassword, String encodedPassword) {
Argon2EncodingUtils.Argon2Hash decoded;
try {
decoded = Argon2EncodingUtils.decode(encodedPassword);
}
catch (IllegalArgumentException ex) {
this.logger.warn("Malformed password hash", ex);
return false;
}
byte[] hashBytes = new byte[decoded.getHash().length];
Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(decoded.getParameters());
generator.generateBytes(rawPassword.toString().toCharArray(), hashBytes);
return constantTimeArrayEquals(decoded.getHash(), hashBytes);
}
@Override
protected boolean upgradeEncodingNonNull(String encodedPassword) {
Argon2Parameters parameters = Argon2EncodingUtils.decode(encodedPassword).getParameters();
return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
}
private static boolean constantTimeArrayEquals(byte[] expected, byte[] actual) {
if (expected.length != actual.length) {
return false;View on GitHub (pinned to 96852e8860)