spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.nonceNotNumeric
DigestAuthenticationFilter.nonceNotNumeric
Error message
Nonce token should have yielded a numeric first token, but was {0} What it means
The first token of the decoded nonce must be the expiration time as a numeric string (milliseconds). When Long.valueOf(nonceTokens[0]) fails, validateAndDecode throws this BadCredentialsException with the full nonce plaintext in the message — the nonce content is not in the format produced by DigestAuthenticationEntryPoint.
Solutions
- Clear cached/stale nonces on the client and obtain a fresh nonce from this server's WWW-Authenticate challenge.
- Ensure all server instances (load-balanced pool) run the same Spring Security version and identical DigestAuthenticationEntryPoint configuration.
- If you customized nonce generation, restore the standard format base64(expiryMillis + ":" + md5Hex(expiryMillis + ":" + key)).
- Verify no intermediary is rewriting the Authorization header between client and server.
Example fix
// before (custom nonce format) String nonce = Base64.getEncoder().encodeToString(UUID.randomUUID().toString().getBytes()); // after (server-compatible) long expiry = System.currentTimeMillis() + validityMillis; String nonce = Base64.getEncoder().encodeToString((expiry + ":" + DigestAuthUtils.md5Hex(expiry + ":" + key)).getBytes());
Defensive patterns
Strategy: validation
Validate before calling
String plain = new String(java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)));
String expiry = plain.split(":", -1)[0];
Long.parseLong(expiry); // throws NumberFormatException client-side if not numeric
Type guard
boolean nonceHasNumericExpiry(String decodedNonce) {
String first = decodedNonce.split(":", -1)[0];
try { Long.parseLong(first); return true; } catch (NumberFormatException e) { return false; }
} Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().contains("numeric first token")) {
response.sendError(401, "Nonce format from a foreign issuer; request a fresh challenge");
}
} Prevention
- Clear cached nonces after server upgrades or migrations between digest implementations
- Pin all load-balanced nodes to the same Spring Security version and entry point config
- Do not customize nonce generation away from base64(expiry:md5(expiry:key))
- Validate the decoded nonce's first token is a long before sending
When it happens
Trigger: The decoded nonce's first ':'-separated token is non-numeric, e.g. a nonce generated by a different library or application version whose payload format differs, or a client-fabricated nonce that happens to Base64-decode and split but lacks the numeric expiry prefix.
Common situations: Migrating from another digest implementation (different nonce layout) while clients replay old nonces; rolling upgrades where old and new servers issue different nonce formats; custom entry point modifications that changed the payload layout.
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.nonceEncoding
- DigestAuthenticationFilter.nonceNotTwoTokens
- DigestAuthenticationFilter.incorrectRealm
- DigestAuthenticationFilter.missingAuth
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/5e6cc1cd33bdb903.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:403
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}"));
}
}
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);
}View on GitHub (pinned to 96852e8860)