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

  1. 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.
  2. Check intermediary proxies/CDNs for Authorization-header rewriting and disable it.
  3. If storing the nonce client-side, use a binary-safe encoding (e.g. hex of UTF-8 bytes) and decode symmetrically before sending.
  4. 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

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


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)