apereo/cas · error · IllegalArgumentException

Proof JWT is too old

Error message

Proof JWT is too old

What it means

OidcVerifiableCredentialJwtProofValidator.verifyFreshness rejects a Proof JWT whose 'iat' claim is older than the allowed past window (MINUTES_IN_PAST minutes before current UTC time). CAS enforces freshness so a captured proof cannot be replayed later during credential issuance or presentation.

Solutions

  1. Regenerate the proof JWT right before submitting the request so 'iat' is current
  2. Check client clock sync (NTP) on the device producing the proof JWT
  3. Increase the server freshness window (cas.authn.oidc.vc proof past-minutes setting) if network/UX latency legitimately exceeds it

Example fix

// before
String proofJwt = buildProofJwt(); // iat set long ago
submitCredentialRequest(proofJwt);
// after
String proofJwt = buildProofJwt(Instant.now(Clock.systemUTC())); // fresh iat immediately before submission
submitCredentialRequest(proofJwt);
Defensive patterns

Strategy: validation

Validate before calling

Instant iat = issuedAt.toInstant();
Instant now = Instant.now(Clock.systemUTC());
boolean fresh = !iat.isAfter(now.plusSeconds(300)) && iat.isAfter(now.minus(Duration.ofMinutes(5)));

Prevention

When it happens

Trigger: Calling validate() on a verifiable-credential proof JWT whose 'iat' Instant is before now minus MINUTES_IN_PAST, i.e. the client built the proof more than the configured minutes ago, or the client clock is behind the server clock.

Common situations: Client devices with skewed clocks (NTP drift), proofs generated at flow start but submitted after long user interaction, or overly tight server-side freshness window configuration.

Related errors


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

        }
        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)