{"record":{"id":"2b8295e08fd6d4e7","repo":"apereo/cas","slug":"proof-jwt-is-missing-iat","errorCode":null,"errorMessage":"Proof JWT is missing iat","messagePattern":"Proof JWT is missing iat","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":102,"sourceCode":"\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();\n        val issuedAt = claims.getIssueTime();\n        if (issuedAt == null) {\n            throw new IllegalArgumentException(\"Proof JWT is missing iat\");\n        }\n        val now = Instant.now(Clock.systemUTC());\n        val iat = issuedAt.toInstant();\n        if (iat.isAfter(now.plusSeconds(SECONDS_IN_FUTURE))) {\n            throw new IllegalArgumentException(\"Proof iat is in the future\");\n        }\n        if (iat.isBefore(now.minus(Duration.ofMinutes(MINUTES_IN_PAST)))) {\n            throw new IllegalArgumentException(\"Proof JWT is too old\");\n        }\n    }\n}\n","sourceCodeStart":84,"sourceCodeEnd":114,"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#L84-L114","documentation":"Thrown by OidcVerifiableCredentialJwtProofValidator.verifyFreshness when the proof JWT lacks an 'iat' (issued-at) claim. Proof freshness requires the issued-at timestamp to bound the proof's validity window and prevent replay of old proofs.","triggerScenarios":"Building the proof JWT claims set without calling issueTime(...) or setting the iat claim; a JWT library that omits iat unless explicitly requested; stripped claims after re-serialization.","commonSituations":"Minimal hand-rolled JWT builders omitting standard time claims; copied claim-set code from non-proof contexts (e.g. ID tokens) that did not set iat.","solutions":["Set the iat claim (claimsSet.issueTime(new Date())) to the current UTC time when building the proof JWT.","Regenerate the proof for each credential request so iat is current.","Use a proof-builder library that enforces standard time claims."],"exampleFix":"// before\nJWTClaimsSet claims = new JWTClaimsSet.Builder().issuer(holderId).build();\n// after\nJWTClaimsSet claims = new JWTClaimsSet.Builder().issuer(holderId)\n    .issueTime(new Date()).build();","handlingStrategy":"validation","validationCode":"if (claims.getIssueTime() == null) {\n    throw new IllegalArgumentException(\"Proof JWT must include iat\");\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always call issueTime(new Date()) when building proof claims.","Regenerate proofs per request rather than reusing templates."],"tags":["jwt","iat","freshness","replay-protection","verifiable-credentials"],"backgroundTag":"missing-required-argument","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"}