apereo/cas · error · IllegalArgumentException
Proof audience does not match credential issuer
Error message
Proof audience does not match credential issuer
What it means
Thrown by OidcVerifiableCredentialJwtProofValidator.verifyAudience when the proof JWT's 'aud' claim is missing or does not contain the configured CAS OIDC credential issuer URL (casProperties.getAuthn().getOidc().getCore().getIssuer()). Per OIDC4VCI, the proof must be audience-restricted to the credential issuer to prevent proof reuse against other issuers.
Solutions
- Set the proof JWT's aud claim to exactly the CAS OIDC issuer identifier (cas.authn.oidc.core.issuer).
- Compare the aud value character-by-character with the configured issuer, including scheme, host, and trailing slash.
- Update client proof code after any change to the CAS issuer configuration or public base URL.
- Check casProperties.getAuthn().getOidc().getCore().getIssuer() (via the discovery document 'issuer') to learn the expected value.
Example fix
// before
.claim("aud", "https://cas.example.org/oidc/token")
// after
.claim("aud", "https://cas.example.org/oidc") // must equal the credential issuer Defensive patterns
Strategy: validation
Validate before calling
List<String> aud = claims.getAudience();
if (aud == null || !aud.contains(credentialIssuer)) {
throw new IllegalArgumentException("aud must equal the credential issuer");
} Try / catch
try {
validator.validate(signedJwt, holderJwk);
} catch (IllegalArgumentException e) {
// rebuild proof with aud = issuer from the discovery document
} Prevention
- Read the issuer value from the server's discovery document instead of hardcoding it.
- Update client configuration whenever the CAS issuer/base URL changes.
- Compare issuer strings exactly, including scheme and trailing slash.
When it happens
Trigger: Proof JWT built without an aud claim; aud set to a different value (token endpoint, web origin, wrong environment URL) instead of the issuer identifier; CAS issuer property changed (e.g. moved from http to https or a new host) while clients still target the old value.
Common situations: Developers copy proof-building code that sets aud to the token endpoint (as OIDC4VCI pre-draft or other specs require); issuer base URL mismatch between environments (dev vs prod); trailing-slash differences between the configured issuer and the aud value.
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
- Proof nonce is invalid or missing
- Proof JWT signature validation failed
- Proof JWT algorithm is invalid
- Unable to verify JWT assertion with any of the configured…
- Missing required principal attribute for claim
AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08).
Data as JSON: /api/errors/0df5709d6c2c18dc.
Report an issue: GitHub.
Appendix: source
Thrown at support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/oidc/vc/issuer/proof/OidcVerifiableCredentialJwtProofValidator.java:81
}
protected void verifySignature(final SignedJWT signedJwt, final JWK holderJwk) throws Exception {
JWSVerifier verifier = null;
if (holderJwk instanceof final RSAKey rsaKey) {
verifier = new RSASSAVerifier(rsaKey);
} else if (holderJwk instanceof final ECKey ecKey) {
verifier = new ECDSAVerifier(ecKey);
}
if (verifier == null || !signedJwt.verify(verifier)) {
throw new IllegalArgumentException("Proof JWT signature validation failed");
}
}
protected void verifyAudience(final SignedJWT signedJwt) throws ParseException {
val audiences = signedJwt.getJWTClaimsSet().getAudience();
val credentialIssuer = casProperties.getAuthn().getOidc().getCore().getIssuer();
if (audiences == null || !audiences.contains(credentialIssuer)) {
throw new IllegalArgumentException("Proof audience does not match credential issuer");
}
}
protected void verifyAlgorithm(final SignedJWT signedJwt, final JWK holderJwk) {
val alg = signedJwt.getHeader().getAlgorithm();
if (alg == null || Algorithm.NONE.equals(alg)) {
throw new IllegalArgumentException("Proof JWT algorithm is invalid");
}
if (holderJwk instanceof RSAKey && !JWSAlgorithm.Family.RSA.contains(alg)) {
throw new IllegalArgumentException("Proof JWT algorithm does not match RSA holder key");
}
if (holderJwk instanceof ECKey && !JWSAlgorithm.Family.EC.contains(alg)) {
throw new IllegalArgumentException("Proof JWT algorithm does not match EC holder key");
}
}
protected void verifyFreshness(final SignedJWT signedJwt) throws ParseException {
val claims = signedJwt.getJWTClaimsSet();View on GitHub (pinned to e7288fc434)