spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.nonceEncoding
DigestAuthenticationFilter.nonceEncoding
Error message
Nonce is not encoded in Base64; received nonce {0} What it means
The nonce sent by the client must be the Base64 value issued by DigestAuthenticationEntryPoint. validateAndDecode tries Base64-decoding the nonce and throws this BadCredentialsException if the value is not valid Base64, meaning the client tampered with, truncated, or fabricated the nonce.
Solutions
- Pass the nonce back to the server byte-for-byte exactly as it appeared in the WWW-Authenticate challenge — no re-encoding, trimming, or URL encoding.
- Check intermediary proxies/CDNs for Authorization-header rewriting and disable it.
- If storing the nonce client-side, use a binary-safe encoding (e.g. hex of UTF-8 bytes) and decode symmetrically before sending.
- Ensure the client re-requests a fresh challenge (401 -> WWW-Authenticate) rather than reusing a stale or guessed nonce.
Example fix
// before (client re-encodes) String nonce = URLEncoder.encode(challengeNonce, "UTF-8"); // after String nonce = challengeNonce; // echo verbatim from WWW-Authenticate
Defensive patterns
Strategy: validation
Validate before calling
try {
java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
throw new IllegalStateException("nonce is not valid Base64; request a fresh WWW-Authenticate challenge");
}
Type guard
boolean isServerNonce(String nonce) {
try { java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)); return true; }
catch (IllegalArgumentException e) { return false; }
} Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().contains("Nonce is not encoded in Base64")) {
response.sendError(401, "Invalid nonce; request a new challenge");
}
} Prevention
- Echo the nonce byte-for-byte from the challenge — never URL-encode or trim it
- Store the nonce in a binary-safe form (hex/quoted) if persisting client-side
- Disable proxies' Authorization-header rewriting
- Always re-fetch a challenge instead of reusing guessed nonces
When it happens
Trigger: The nonce field in the Digest header is not valid Base64 — e.g. manually invented nonce values, URL-encoding artifacts ('+' turned into spaces, '/' or '=' stripped), truncated nonce after a proxy rewrite, or a client using a URL-safe Base64 variant the strict decoder rejects.
Common situations: Proxies/gateways re-encoding the Authorization header; frameworks decoding query/header values before the filter sees them; clients storing the nonce in a cookie/DB where padding '=' gets trimmed; hand-crafted digest clients generating their own nonces.
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.nonceCompromised
- DigestAuthenticationFilter.nonceNotNumeric
- DigestAuthenticationFilter.nonceNotTwoTokens
- An error occurred while attempting to decode the Jwt…
- Cookie token was not Base64 encoded; value was
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/8daa31c6848231b2.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:385
logger.debug(LogMessage.format("extracted nc: '%s'; cnonce: '%s'", this.nc, this.cnonce));
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"DigestAuthenticationFilter.missingAuth", new Object[] { this.section212response },
"Missing mandatory digest value; received header {0}"));
}
}
// Check realm name equals what we expected
if (!this.realm.equals(expectedRealm)) {
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"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(View on GitHub (pinned to 96852e8860)