apereo/cas · error · IllegalArgumentException

Proof JWT is missing iat

Error message

Proof JWT is missing iat

What it means

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.

Solutions

  1. Set the iat claim (claimsSet.issueTime(new Date())) to the current UTC time when building the proof JWT.
  2. Regenerate the proof for each credential request so iat is current.
  3. Use a proof-builder library that enforces standard time claims.

Example fix

// before
JWTClaimsSet claims = new JWTClaimsSet.Builder().issuer(holderId).build();
// after
JWTClaimsSet claims = new JWTClaimsSet.Builder().issuer(holderId)
    .issueTime(new Date()).build();
Defensive patterns

Strategy: validation

Validate before calling

if (claims.getIssueTime() == null) {
    throw new IllegalArgumentException("Proof JWT must include iat");
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/2b8295e08fd6d4e7. 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:102

    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();
        val issuedAt = claims.getIssueTime();
        if (issuedAt == null) {
            throw new IllegalArgumentException("Proof JWT is missing iat");
        }
        val now = Instant.now(Clock.systemUTC());
        val iat = issuedAt.toInstant();
        if (iat.isAfter(now.plusSeconds(SECONDS_IN_FUTURE))) {
            throw new IllegalArgumentException("Proof iat is in the future");
        }
        if (iat.isBefore(now.minus(Duration.ofMinutes(MINUTES_IN_PAST)))) {
            throw new IllegalArgumentException("Proof JWT is too old");
        }
    }
}

View on GitHub (pinned to e7288fc434)