spring-projects/spring-security · error · BadCredentialsException

DigestAuthenticationFilter.nonceCompromised

DigestAuthenticationFilter.nonceCompromised

Error message

Nonce token compromised {0}

What it means

The decoded nonce's second token must equal md5Hex(expiryTime + ":" + entryPointKey) — the server's signature proving the nonce was issued by this entry point. validateAndDecode throws this BadCredentialsException when the signature check fails, indicating a forged, expired-regenerated, or foreign nonce (the message names the suspicious nonce).

Solutions

  1. Have the client request a fresh nonce from the current server's 401 challenge instead of replaying a cached one.
  2. If running multiple server nodes, give every node the same DigestAuthenticationEntryPoint key (set explicitly, not left random) so all nodes validate the same nonces.
  3. Verify no one/thing is modifying the nonce in transit (TLS, trusted proxies only).
  4. Treat repeated occurrences as a security signal: log the nonce from the message and investigate the source for forgery attempts.

Example fix

// before (per-node random key -> cross-node failures)
DigestAuthenticationEntryPoint ep = new DigestAuthenticationEntryPoint();
// after
DigestAuthenticationEntryPoint ep = new DigestAuthenticationEntryPoint();
ep.setKey("shared-stable-key-across-nodes"); // identical on every instance
Defensive patterns

Strategy: try-catch

Validate before calling

String plain = new String(java.util.Base64.getDecoder().decode(nonce.getBytes(StandardCharsets.UTF_8)));
String[] t = plain.split(":", -1);
String expectedSig = md5Hex(Long.parseLong(t[0]) + ":" + sharedEntryPointKey);
if (!MessageDigest.isEqual(expectedSig.getBytes(), t[1].getBytes())) {
    throw new IllegalStateException("nonce signature invalid; request a fresh challenge");
}

Try / catch

try {
    chain.doFilter(request, response);
} catch (BadCredentialsException e) {
    if (e.getMessage().startsWith("Nonce token compromised")) {
        securityAuditLog.warn("Possible nonce tampering/replay", e);
        response.sendError(401, "Invalid nonce; re-authenticate");
    }
}

Prevention

When it happens

Trigger: A client sends a nonce whose decoded signature doesn't match the server's current entryPointKey computation — nonces from a server with a different key, a nonce whose expiry portion was edited after issuance, or replayed nonces after the server restarted with a new random key.

Common situations: Server restarts or multiple nodes with different/mismatched DigestAuthenticationEntryPoint keys while clients cache nonces; deliberate tampering/replay attacks; environments where the key changed between deployments.

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10). Data as JSON: /api/errors/267ae78d69e0452f. Report an issue: GitHub.

Appendix: source

Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:410

			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);
		}

		boolean isNonceExpired() {
			long now = System.currentTimeMillis();
			return this.nonceExpiryTime < now;
		}

		@Nullable String getUsername() {

View on GitHub (pinned to 96852e8860)