spring-projects/spring-security · error · BadCredentialsException
DigestAuthenticationFilter.incorrectRealm
DigestAuthenticationFilter.incorrectRealm
Error message
Response realm name '{0}' does not match system realm name of '{1}' What it means
validateAndDecode compares the realm sent by the client in the Digest header against the realm expected by the server (from DigestAuthenticationEntryPoint). A mismatch throws this BadCredentialsException listing both realms ({0} = client realm, {1} = expected realm), since a correct digest client echoes back the server's realm and the digest hash depends on it.
Solutions
- Read both realm values from the exception message and align them: change the client to use the server's realm, or update DigestAuthenticationEntryPoint's realmName to the expected one.
- Prefer having the client take realm/nonce dynamically from the server's WWW-Authenticate challenge instead of hardcoding.
- Grep client code/config for the old realm string and update it after any realm rename.
- If two apps share a host/path space, give each a distinct path or use the same realm intentionally.
Example fix
// before (server config)
http.exceptionHandling().authenticationEntryPoint(new DigestAuthenticationEntryPoint()); // default/wrong realm
// after
DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
entryPoint.setRealmName("MyAppRealm"); // must match the realm the clients echo back Defensive patterns
Strategy: validation
Validate before calling
String serverRealm = extractRealm(conn.getHeaderField("WWW-Authenticate"));
if (!serverRealm.equals(clientConfiguredRealm)) {
clientConfiguredRealm = serverRealm; // use the realm the server advertises
}
Try / catch
try {
chain.doFilter(request, response);
} catch (BadCredentialsException e) {
if (e.getMessage().contains("does not match system realm name")) {
// parse both realms from the message and re-authenticate with the server's realm
response.sendError(401, "Realm mismatch, restart auth with server challenge");
}
} Prevention
- Never hardcode the realm; always take it from the server's WWW-Authenticate challenge
- Keep realmName identical across environments or load it from config
- After renaming a realm, update all clients and clear cached credentials
- Use one realm per protected application and document it
When it happens
Trigger: A request's Digest header contains realm="X" while the server's entry point is configured with realm="Y" (the received and expected values are both in the message). E.g. client hardcodes a realm string, or the server realm was renamed.
Common situations: Multiple server environments (dev/prod) with different realm names but a shared client; a server-side Spring Security config change renaming the realm; hardcoded realm in scripts/mobile apps; reverse proxy fronting two apps with different realms on the same host.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- DigestAuthenticationFilter.missingAuth
- 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/75774e30e6263380.
Report an issue: GitHub.
Appendix: source
Thrown at web/src/main/java/org/springframework/security/web/authentication/www/DigestAuthenticationFilter.java:375
// 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",
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) {View on GitHub (pinned to 96852e8860)