{"record":{"id":"0df5709d6c2c18dc","repo":"apereo/cas","slug":"proof-audience-does-not-match-credential-issuer","errorCode":null,"errorMessage":"Proof audience does not match credential issuer","messagePattern":"Proof audience does not match credential issuer","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"error","filePath":"support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/oidc/vc/issuer/proof/OidcVerifiableCredentialJwtProofValidator.java","lineNumber":81,"sourceCode":"    }\n\n    protected void verifySignature(final SignedJWT signedJwt, final JWK holderJwk) throws Exception {\n        JWSVerifier verifier = null;\n        if (holderJwk instanceof final RSAKey rsaKey) {\n            verifier = new RSASSAVerifier(rsaKey);\n        } else if (holderJwk instanceof final ECKey ecKey) {\n            verifier = new ECDSAVerifier(ecKey);\n        }\n        if (verifier == null || !signedJwt.verify(verifier)) {\n            throw new IllegalArgumentException(\"Proof JWT signature validation failed\");\n        }\n    }\n\n    protected void verifyAudience(final SignedJWT signedJwt) throws ParseException {\n        val audiences = signedJwt.getJWTClaimsSet().getAudience();\n        val credentialIssuer = casProperties.getAuthn().getOidc().getCore().getIssuer();\n        if (audiences == null || !audiences.contains(credentialIssuer)) {\n            throw new IllegalArgumentException(\"Proof audience does not match credential issuer\");\n        }\n    }\n\n    protected void verifyAlgorithm(final SignedJWT signedJwt, final JWK holderJwk) {\n        val alg = signedJwt.getHeader().getAlgorithm();\n        if (alg == null || Algorithm.NONE.equals(alg)) {\n            throw new IllegalArgumentException(\"Proof JWT algorithm is invalid\");\n        }\n        if (holderJwk instanceof RSAKey && !JWSAlgorithm.Family.RSA.contains(alg)) {\n            throw new IllegalArgumentException(\"Proof JWT algorithm does not match RSA holder key\");\n        }\n        if (holderJwk instanceof ECKey && !JWSAlgorithm.Family.EC.contains(alg)) {\n            throw new IllegalArgumentException(\"Proof JWT algorithm does not match EC holder key\");\n        }\n    }\n\n    protected void verifyFreshness(final SignedJWT signedJwt) throws ParseException {\n        val claims = signedJwt.getJWTClaimsSet();","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/apereo/cas/blob/e7288fc434b4f4505b8452e1a57e8fb3111bb863/support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/oidc/vc/issuer/proof/OidcVerifiableCredentialJwtProofValidator.java#L63-L99","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n.claim(\"aud\", \"https://cas.example.org/oidc/token\")\n// after\n.claim(\"aud\", \"https://cas.example.org/oidc\") // must equal the credential issuer","handlingStrategy":"validation","validationCode":"List<String> aud = claims.getAudience();\nif (aud == null || !aud.contains(credentialIssuer)) {\n    throw new IllegalArgumentException(\"aud must equal the credential issuer\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    validator.validate(signedJwt, holderJwk);\n} catch (IllegalArgumentException e) {\n    // rebuild proof with aud = issuer from the discovery document\n}","preventionTips":["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."],"tags":["jwt","audience","oidc","verifiable-credentials","config-mismatch"],"backgroundTag":"invalid-argument-value","analyzedSha":"e7288fc434b4f4505b8452e1a57e8fb3111bb863","analyzedAt":"2026-09-08T15:39:16.015Z","contentChangedAt":"2026-09-08T15:39:16.015Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}