spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.missingAuth
DigestAuthenticationFilter.missingAuth
Error message
Missing mandatory digest value; received header {0} What it means
Same 'missing mandatory digest value' message but for the RFC 2617 'auth' qop path: when qop=auth is declared, the header must also contain nc (nonce count) and cnonce (client nonce). validateAndDecode throws this BadCredentialsException if either is null.
Solutions
- Have the client include nc (e.g. 00000001) and a random cnonce whenever qop is present in the challenge response.
- If the client only supports RFC 2069, configure/choose a server qop that matches, or fall back to no-qop digest, ensuring both sides agree.
- Regenerate the digest response including cnonce and nc in the MD5 input (A2 includes qop, nc, cnonce for RFC 2617) so the header is self-consistent.
- Log the full header from the exception message and diff each parameter against the RFC 2617 grammar.
Example fix
// before Authorization: Digest username="u", realm="r", nonce="n", uri="/", qop="auth", response="d" // after Authorization: Digest username="u", realm="r", nonce="n", uri="/", qop="auth", nc="00000001", cnonce="0a4f113b", response="d"
Defensive patterns
Strategy: validation
Validate before calling
Map<String,String> p = parseDigestParams(header);
if ("auth".equals(p.get("qop")) && (p.get("nc") == null || p.get("cnonce") == null)) {
throw new IllegalStateException("qop=auth requires nc and cnonce in the Digest header");
}
Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().contains("Missing mandatory digest value")) {
response.sendError(401, "qop=auth requires nc and cnonce");
}
} Prevention
- Only advertise qop support your client fully implements
- Generate a random cnonce and increment nc per request whenever qop is present
- Keep client and server in the same RFC level (2617 qop vs 2069 no-qop)
- Diff your header against a working digest client's output
When it happens
Trigger: A Digest Authorization header sets qop="auth" but omits nc and/or cnonce. This happens when a client advertises a qop it doesn't fully implement, or builds the header from an incomplete parameter set.
Common situations: Clients that parse the server's qop options but never generate the client nonce/counter; header built by string concatenation missing nc/cnonce; upgraded servers now requiring qop=auth while clients were written for legacy RFC 2069 (no qop).
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- DigestAuthenticationFilter.incorrectRealm
- DigestAuthenticationFilter.missingMandatory
- DigestAuthenticationFilter.nonceNotTwoTokens
- Authentication for password change failed.
- Authentication.getCredentials() cannot be null
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/fa8e115598ed8256.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:368
logger.debug(
LogMessage.format("Extracted username: '%s'; realm: '%s'; nonce: '%s'; uri: '%s'; response: '%s'",
this.username, this.realm, this.nonce, this.uri, this.response));
}
void validateAndDecode(@Nullable String entryPointKey, @Nullable String expectedRealm)
throws BadCredentialsException {
// Check all required parameters were supplied (ie RFC 2069)
if ((this.username == null) || (this.realm == null) || (this.nonce == null) || (this.uri == null)
|| (this.response == null)) {
throw new BadCredentialsException(DigestAuthenticationFilter.this.messages.getMessage(
"DigestAuthenticationFilter.missingMandatory", new Object[] { this.section212response },
"Missing mandatory digest value; received header {0}"));
}
// Check all required parameters for an "auth" qop were supplied (ie RFC 2617)
if ("auth".equals(this.qop)) {
if ((this.nc == null) || (this.cnonce == null)) {
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",View on GitHub (pinned to 96852e8860)