spring-projects/spring-security · error · BadCredentialsException
Kerberos validation not successful
Error message
Kerberos validation not successful
What it means
SunJaasKerberosTicketValidator.validateTicket() wraps IllegalStateException or PrivilegedActionException (from the JAAS/GSS validation action) in a BadCredentialsException. It means the Spnego/Kerberos service ticket presented by the client could not be validated against the service principal, e.g. the underlying JAAS login failed or the GSS acceptSecContext step threw.
Solutions
- Confirm the servicePrincipal matches the SPN the client used and that the keytab contains that principal's key (kvno must match).
- Decode the Authorization header correctly: strip 'Negotiate ' prefix and Base64-decode before passing the token to validateTicket.
- Set the system property sun.security.krb5.debug=true and check the wrapped cause for GSS error codes (e.g. KRB_AP_ERR_TKT_EXPIRED).
- Ensure krb5.conf exists and clocks are synchronized; expired or replayed tickets are a common cause.
- Check the wrapped exception (BadCredentialsException.getCause()) to distinguish JAAS login failures from GSS token failures.
Example fix
// before
String header = request.getHeader("Authorization"); // "Negotiate <token>"
byte[] token = header.getBytes(); // wrong: raw string
validator.validateTicket(token);
// after
String header = request.getHeader("Authorization");
String b64 = header.substring(header.indexOf(' ') + 1);
byte[] token = Base64.getDecoder().decode(b64);
validator.validateTicket(token); Defensive patterns
Strategy: validation
Validate before calling
// before validateTicket
if (token == null || token.length == 0) throw new BadCredentialsException("missing spnego token");
String auth = request.getHeader("Authorization");
if (auth == null || !auth.startsWith("Negotiate ")) throw new BadCredentialsException("no negotiate header");
byte[] token = Base64.getDecoder().decode(auth.substring("Negotiate ".length())); Try / catch
try {
return validator.validateTicket(token);
} catch (BadCredentialsException e) {
LOGGER.debug("ticket validation failed, cause: {}", e.getCause(), e);
response.setStatus(401);
response.setHeader("WWW-Authenticate", "Negotiate");
return null;
} Prevention
- Always strip the 'Negotiate ' prefix and Base64-decode before validating.
- Keep keytab KVNO in sync with the KDC after password rotations.
- Register exactly one SPN per service account to avoid duplicate-SPN ticket failures.
- Check e.getCause() to distinguish JAAS config problems from token problems.
When it happens
Trigger: Calling validateTicket(byte[] token) with a Base64-decoded Spnego/Negotiate token whose GSS validation fails: token expired/replayed, token encrypted for a different service principal, servicePrincipal/keytab not set, or the internal KerberosValidateAction throwing PrivilegedActionException; also thrown when Subject.doAs raises IllegalStateException.
Common situations: Client tickets issued by a realm the server doesn't trust, servicePrincipalName (SPN) not registered or duplicated in AD, missing krb5.conf/keytab on the server, or token mangling caused by incorrect Base64 decoding of the Authorization header.
Related errors
- GSSContext name of the context initiator is null
- Error running rest call
- Negotiate Header was invalid
- ticketValidator must be set
- Unauthenticated or no response token
AI-assisted analysis of spring-projects/spring-security@96852e8860 (2026-09-10).
Data as JSON: /api/errors/463f8a9007128d11.
Report an issue: GitHub.
Appendix: source
Thrown at kerberos/kerberos-core/src/main/java/org/springframework/security/kerberos/authentication/sun/SunJaasKerberosTicketValidator.java:97
@Override
public KerberosTicketValidation validateTicket(byte[] token) {
try {
if (this.serviceSubject == null) {
throw new IllegalStateException("serviceSubject must be initialized");
}
if (!this.multiTier) {
return Subject.doAs(this.serviceSubject, new KerberosValidateAction(token));
}
Subject subjectCopy = JaasUtil.copySubject(this.serviceSubject);
JaasSubjectHolder subjectHolder = new JaasSubjectHolder(subjectCopy);
return Subject.doAs(subjectHolder.getJaasSubject(), new KerberosMultitierValidateAction(token));
}
catch (IllegalStateException | PrivilegedActionException ex) {
throw new BadCredentialsException("Kerberos validation not successful", ex);
}
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.servicePrincipal, "servicePrincipal must be specified");
Assert.notNull(this.keyTabLocation, "keyTab must be specified");
if (this.servicePrincipal == null || this.keyTabLocation == null) {
throw new IllegalStateException("servicePrincipal and keyTabLocation must be set");
}
if (this.keyTabLocation instanceof ClassPathResource) {
this.LOG.warn(
"Your keytab is in the classpath. This file needs special protection and shouldn't be in the classpath. JAAS may also not be able to load this file from classpath.");
}
String keyTabLocationAsString = this.keyTabLocation.getURL().toExternalForm();
// We need to remove the file prefix (if there is one), as it is not supported in
// Java 7 anymore.
// As Java 6 accepts it with and without the prefix, we don't need to check forView on GitHub (pinned to 96852e8860)