spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.nonceCompromised
DigestAuthenticationFilter.nonceCompromised
Error message
Nonce token compromised {0} What it means
The decoded nonce's second token must equal md5Hex(expiryTime + ":" + entryPointKey) — the server's signature proving the nonce was issued by this entry point. validateAndDecode throws this BadCredentialsException when the signature check fails, indicating a forged, expired-regenerated, or foreign nonce (the message names the suspicious nonce).
Solutions
- Have the client request a fresh nonce from the current server's 401 challenge instead of replaying a cached one.
- If running multiple server nodes, give every node the same DigestAuthenticationEntryPoint key (set explicitly, not left random) so all nodes validate the same nonces.
- Verify no one/thing is modifying the nonce in transit (TLS, trusted proxies only).
- Treat repeated occurrences as a security signal: log the nonce from the message and investigate the source for forgery attempts.
Example fix
// before (per-node random key -> cross-node failures)
DigestAuthenticationEntryPoint ep = new DigestAuthenticationEntryPoint();
// after
DigestAuthenticationEntryPoint ep = new DigestAuthenticationEntryPoint();
ep.setKey("shared-stable-key-across-nodes"); // identical on every instance Defensive patterns
Strategy: try-catch
Validate before calling
String plain = new String(java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)));
String[] t = plain.split(":", -1);
String expectedSig = md5Hex(Long.parseLong(t[0]) + ":" + sharedEntryPointKey);
if (!MessageDigest.isEqual(expectedSig.getBytes(), t[1].getBytes())) {
throw new IllegalStateException("nonce signature invalid; request a fresh challenge");
}
Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().startsWith("Nonce token compromised")) {
securityAuditLog.warn("Possible nonce tampering/replay", e);
response.sendError(401, "Invalid nonce; re-authenticate");
}
} Prevention
- Set an explicit, identical DigestAuthenticationEntryPoint key on every server node
- Re-authenticate from a fresh challenge rather than replaying cached nonces after restarts
- Always serve and accept Authorization headers only over TLS
- Monitor 'Nonce token compromised' occurrences as a potential attack signal
When it happens
Trigger: A client sends a nonce whose decoded signature doesn't match the server's current entryPointKey computation — nonces from a server with a different key, a nonce whose expiry portion was edited after issuance, or replayed nonces after the server restarted with a new random key.
Common situations: Server restarts or multiple nodes with different/mismatched DigestAuthenticationEntryPoint keys while clients cache nonces; deliberate tampering/replay attacks; environments where the key changed between deployments.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- DigestAuthenticationFilter.nonceEncoding
- DigestAuthenticationFilter.nonceNotNumeric
- DigestAuthenticationFilter.nonceNotTwoTokens
- Cookie contained signature
- DigestAuthenticationFilter.incorrectRealm
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/267ae78d69e0452f.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:410
String[] nonceTokens = StringUtils.delimitedListToStringArray(nonceAsPlainText, ":");
if (nonceTokens.length != 2) {
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"DigestAuthenticationFilter.nonceNotTwoTokens", new Object[] { nonceAsPlainText },
"Nonce should have yielded two tokens but was {0}"));
}
// Extract expiry time from nonce
try {
this.nonceExpiryTime = Long.valueOf(nonceTokens[0]);
}
catch (NumberFormatException nfe) {
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"DigestAuthenticationFilter.nonceNotNumeric", new Object[] { nonceAsPlainText },
"Nonce token should have yielded a numeric first token, but was {0}"));
}
// Check signature of nonce matches this expiry time
String expectedNonceSignature = DigestAuthUtils.md5Hex(this.nonceExpiryTime + ":" + entryPointKey);
if (!Utf8.isEqual(expectedNonceSignature, nonceTokens[1])) {
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"DigestAuthenticationFilter.nonceCompromised", new Object[] { nonceAsPlainText },
"Nonce token compromised {0}"));
}
}
String calculateServerDigest(@Nullable String password, String httpMethod) {
// Compute the expected response-digest (will be in hex form). Don't catch
// IllegalArgumentException (already checked validity)
return DigestAuthUtils.generateDigest(DigestAuthenticationFilter.this.passwordAlreadyEncoded, this.username,
this.realm, password, httpMethod, this.uri, this.qop, this.nonce, this.nc, this.cnonce);
}
boolean isNonceExpired() {
long now = System.currentTimeMillis();
return this.nonceExpiryTime < now;
}
@Nullable String getUsername() {View on GitHub (pinned to 96852e8860)