apereo/cas · warning
Issuer [ ] is invalid since the expected issuer should be […
Error message
Issuer [{}] is invalid since the expected issuer should be [{}] What it means
WsFederationCredential.isValid() validates a WS-Federation token against the expected relying-party values. This warning is logged (and the credential rejected, returning false) when the token's Issuer claim does not case-insensitively match the configured expected issuer. CAS throws/flags it to prevent accepting tokens from the wrong identity provider.
Solutions
- Compare the actual Issuer in the token (see the warning's first placeholder) with the configured expected issuer and set cas.authn.wsfed identity provider metadata/relyingPartyIdentifier to the exact string.
- Check for trailing-slash or http/https scheme mismatches between the ADFS FederationMetadata.xml entityID and the CAS config.
- Verify you are pointing at the intended ADFS/IdP environment (staging vs production) and its metadata.
- If the IdP genuinely changed its identifier, update the federation trust and CAS metadata accordingly.
Example fix
// before cas.authn.wsfed[0].identity-provider-metadata.identity-provider-uri=https://sts.example.com/adfs/services/trust/ // after (must match token Issuer exactly, case-insensitive) cas.authn.wsfed[0].identity-provider-metadata.identity-provider-uri=https://sts.example.com/adfs/services/trust
Defensive patterns
Strategy: validation
Validate before calling
if (credential.getIssuer() == null || !credential.getIssuer().equalsIgnoreCase(expectedIssuer)) {
throw new IllegalStateException("Token issuer mismatch: " + credential.getIssuer() + " != " + expectedIssuer);
} Prevention
- Copy the entityID/issuer string verbatim from FederationMetadata.xml into CAS config.
- Diff staging and production IdP identifiers when switching environments.
- Watch for http vs https and trailing-slash differences in issuer URIs.
When it happens
Trigger: Calling isValid(expectedAudience, expectedIssuer, timeDrift) where this.issuer (from the WS-Fed token's Issuer element) differs case-insensitively from the expectedIssuer argument supplied by WsFederationAuthenticationService configuration (cas.authn.wsfed[...].identityProviderMetadata / relyingPartyIdentifier or the idp issuer in metadata).
Common situations: ADF Metadata URL misconfigured so the expected issuer doesn't match the IdP's actual entityID (e.g. trailing slash, hostname vs realm difference like https://sts.example.com/adfs/services/trust vs http://sts.example.com/adfs/services/trust); swapping staging/production ADFS; ADFS 3.0 vs 4.0 issuing different identifier forms.
Related errors
- Ticket is issued before the allowed drift. Issued on
- Ticket is issued after the allowed drift. Retrieved on
- Ticket is too late because it's retrieved on
- Found multiple values for id attribute
- Denied
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/35903153f35d8450.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-wsfederation/src/main/java/org/apereo/cas/support/wsfederation/authentication/principal/WsFederationCredential.java:64
private ZonedDateTime retrievedOn;
private Map<String, List<Object>> attributes;
/**
* Validates the credential.
*
* @param expectedAudience the audience that the token was issued to (CAS Server)
* @param expectedIssuer the issuer of the token (the IdP)
* @param timeDrift the amount of acceptable time drift
* @return true if the credentials are valid, otherwise false
*/
public boolean isValid(final String expectedAudience, final String expectedIssuer, final long timeDrift) {
if (!this.audience.equalsIgnoreCase(expectedAudience)) {
LOGGER.warn("Audience [{}] is invalid where the expected audience should be [{}]", this.audience, expectedAudience);
return false;
}
if (!this.issuer.equalsIgnoreCase(expectedIssuer)) {
LOGGER.warn("Issuer [{}] is invalid since the expected issuer should be [{}]", this.issuer, expectedIssuer);
return false;
}
val retrievedOnTimeDrift = getRetrievedOn().minus(timeDrift, ChronoUnit.MILLIS);
if (getIssuedOn().isBefore(retrievedOnTimeDrift)) {
LOGGER.warn("Ticket is issued before the allowed drift. Issued on [{}] while allowed drift is [{}]",
this.issuedOn, retrievedOnTimeDrift);
return false;
}
val retrievedOnTimeAfterDrift = getRetrievedOn().plus(timeDrift, ChronoUnit.MILLIS);
if (getIssuedOn().isAfter(retrievedOnTimeAfterDrift)) {
LOGGER.warn("Ticket is issued after the allowed drift. Retrieved on [{}] and issued on [{}] while allowed drift is [{}]",
getRetrievedOn(), getIssuedOn(), retrievedOnTimeAfterDrift);
return false;
}
if (getRetrievedOn().isAfter(this.notOnOrAfter)) {
LOGGER.warn("Ticket is too late because it's retrieved on [{}] which is after [{}].",
getRetrievedOn(), this.notOnOrAfter);
return false;View on GitHub (pinned to e7288fc434)