spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.nonceNotTwoTokens
DigestAuthenticationFilter.nonceNotTwoTokens
Error message
Nonce should have yielded two tokens but was {0} What it means
After Base64-decoding, the nonce plaintext must have the form 'expiryTime:signature' (two colon-separated tokens). validateAndDecode throws this BadCredentialsException when splitting the decoded nonce on ':' does not yield exactly two tokens, i.e. the nonce payload structure isn't what the server's entry point produced.
Solutions
- Obtain nonces only from this server's DigestAuthenticationEntryPoint via a fresh 401 challenge; never fabricate or reuse nonces from another app.
- Check for multiple digest-auth apps behind the same host and make clients target the correct origin / consume each app's own challenge.
- Verify all server instances behind the load balancer run a consistent Spring Security version and identical entry point configuration.
- Decode the nonce client-side (base64 -> 'expiry:md5sig') to sanity-check the shape before sending.
Example fix
// before (hand-made nonce)
String nonce = Base64.getEncoder().encodeToString("12345".getBytes());
// after: request a challenge and use the server-issued nonce
String nonce = extractNonce(authConn.getHeaderField("WWW-Authenticate")); // expiry:signature format Defensive patterns
Strategy: validation
Validate before calling
String plain = new String(java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)));
if (plain.split(":", -1).length != 2) {
throw new IllegalStateException("nonce payload must be '<expiry>:<signature>'; request a fresh challenge");
}
Type guard
boolean hasNonceShape(String decodedNonce) {
return decodedNonce != null && decodedNonce.split(":", -1).length == 2;
} Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().contains("should have yielded two tokens")) {
response.sendError(401, "Unrecognized nonce format; re-authenticate from the server challenge");
}
} Prevention
- Only use nonces issued by this application's DigestAuthenticationEntryPoint
- Avoid multiple digest-auth apps behind one hostname sharing clients
- Keep all server instances on the same Spring Security version
- Decode and sanity-check the nonce shape before sending
When it happens
Trigger: Base64-decoding succeeds but the plaintext contains zero, one, or 3+ colon-separated segments — e.g. a nonce fabricated by the client, a nonce issued by a different application/server sharing the Base64 shape but not the format, or a corrupted/transformed nonce that happens to be valid Base64.
Common situations: Two different services both doing digest auth behind one domain and the client replaying a nonce from the wrong service; manual testing with arbitrary Base64 strings as nonces; a load balancer pool running mismatched application versions with different nonce formats.
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
- DigestAuthenticationFilter.incorrectRealm
- DigestAuthenticationFilter.missingAuth
- DigestAuthenticationFilter.missingMandatory
- DigestAuthenticationFilter.nonceCompromised
- DigestAuthenticationFilter.nonceEncoding
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/af4139c263889826.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:394
"DigestAuthenticationFilter.incorrectRealm", new Object[] { this.realm, expectedRealm },
"Response realm name '{0}' does not match system realm name of '{1}'"));
}
// Check nonce was Base64 encoded (as sent by DigestAuthenticationEntryPoint)
final byte[] nonceBytes;
try {
nonceBytes = Base64.getDecoder().decode(this.nonce.getBytes());
}
catch (IllegalArgumentException ex) {
throw new BadCredentialsException(
DigestAuthenticationFilter.this.messages.getMessage("DigestAuthenticationFilter.nonceEncoding",
new Object[] { this.nonce }, "Nonce is not encoded in Base64; received nonce {0}"));
}
// Decode nonce from Base64 format of nonce is: base64(expirationTime + ":" +
// md5Hex(expirationTime + ":" + key))
String nonceAsPlainText = new String(nonceBytes);
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}"));View on GitHub (pinned to 96852e8860)