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

  1. Clear cached/stale nonces on the client and obtain a fresh nonce from this server's WWW-Authenticate challenge.
  2. Ensure all server instances (load-balanced pool) run the same Spring Security version and identical DigestAuthenticationEntryPoint configuration.
  3. If you customized nonce generation, restore the standard format base64(expiryMillis + ":" + md5Hex(expiryMillis + ":" + key)).
  4. 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

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


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)