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

  1. 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.
  2. Check for trailing-slash or http/https scheme mismatches between the ADFS FederationMetadata.xml entityID and the CAS config.
  3. Verify you are pointing at the intended ADFS/IdP environment (staging vs production) and its metadata.
  4. 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

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


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)