spring-projects/spring-security · error · BadCredentialsException

<ticket validation failure message>

Error message

<ticket validation failure message>

What it means

CasAuthenticationProvider.validateAuthentication (via authenticateNow) catches a TicketValidationException from the CAS service ticket validator and rethrows it as a BadCredentialsException whose message is the original exception's message. The thrown message is whatever the ticket validator reported, e.g. 'Ticket ST-... not recognized'. Spring Security does this so CAS validation failures map onto the standard bad-credentials authentication failure flow (including event publishing and failure handling).

Source

Thrown at cas/src/main/java/org/springframework/security/cas/authentication/CasAuthenticationProvider.java:156

		return result;
	}

	private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
		try {
			Object credentials = authentication.getCredentials();
			if (credentials == null) {
				throw new BadCredentialsException("Authentication.getCredentials() cannot be null");
			}
			Assertion assertion = this.ticketValidator.validate(credentials.toString(), getServiceUrl(authentication));
			UserDetails userDetails = loadUserByAssertion(assertion);
			this.userDetailsChecker.check(userDetails);
			Collection<GrantedAuthority> authorities = new ArrayList<>(
					this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()));
			authorities.add(FactorGrantedAuthority.fromAuthority(AUTHORITY));
			return new CasAuthenticationToken(this.key, userDetails, credentials, authorities, userDetails, assertion);
		}
		catch (TicketValidationException ex) {
			throw new BadCredentialsException(ex.getMessage(), ex);
		}
	}

	/**
	 * Gets the serviceUrl. If the {@link Authentication#getDetails()} is an instance of
	 * {@link ServiceAuthenticationDetails}, then
	 * {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise, the
	 * {@link ServiceProperties#getService()} is used.
	 * @param authentication
	 * @return
	 */
	private @Nullable String getServiceUrl(Authentication authentication) {
		String serviceUrl;
		if (authentication.getDetails() instanceof ServiceAuthenticationDetails) {
			return ((ServiceAuthenticationDetails) authentication.getDetails()).getServiceUrl();
		}
		Assert.state(this.serviceProperties != null,
				"serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");

View on GitHub (pinned to 96852e8860)

Solutions

  1. Read the wrapped cause (ex.getCause(), the original TicketValidationException) to see the exact validation failure reported by the CAS server.
  2. Verify the service URL used to request the ticket exactly matches the service parameter sent to /serviceValidate (use ServiceProperties and ServiceAuthenticationDetails consistently).
  3. Check clock synchronization (NTP) between the application and the CAS server, since expired/not-yet-valid tickets commonly stem from skew.
  4. Ensure the ticket is only validated once and not replayed on redirects or AJAX retries.
  5. Confirm network connectivity and TLS trust between the app and the CAS server's validation endpoint.

Example fix

// before: opaque message only
try { auth = provider.authenticate(token); } catch (BadCredentialsException e) { log.error(e.getMessage()); }
// after: surface the CAS-side cause
try { auth = provider.authenticate(token); }
catch (BadCredentialsException e) {
    Throwable cause = e.getCause(); // TicketValidationException with CAS server detail
    log.error("CAS ticket validation failed: {}", cause != null ? cause.getMessage() : e.getMessage());
}
Defensive patterns

Strategy: try-catch

Validate before calling

// verify service URL matches ServiceProperties before authenticating
assert serviceProperties.getService().equals(request.getRequestURL().toString());

Try / catch

try {
    auth = provider.authenticate(token);
} catch (BadCredentialsException e) {
    Throwable cause = e.getCause();
    log.warn("CAS ticket rejected: {}", cause != null ? cause.getMessage() : e.getMessage());
    // return 401 and redirect to CAS for a fresh ticket
}

Prevention

When it happens

Trigger: A user authenticates via CAS and CasAuthenticationProvider.authenticate -> authenticateNow calls the TicketValidator; the validator rejects the presented service ticket (expired, already used, service URL mismatch, or CAS server unreachable/rejecting).

Common situations: Ticket replay after browser refresh, clock skew between app server and CAS server, service/destination URL mismatch between the service ticket request and the validation request, misconfigured ticketValidator or serviceProperties, expired tickets, CAS server outage.

Related errors


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