apereo/cas · error · IllegalArgumentException

JWT time claim is invalid

Error message

JWT time claim is invalid

What it means

readNumericDate converts a JWT time claim (exp/nbf/iat) to an Instant via BigDecimal epoch-second parsing, including nanoseconds. A non-numeric or numerically infeasible value raises ArithmeticException, which is rethrown as IllegalArgumentException("JWT time claim is invalid").

Solutions

  1. Fix the JWT producer to emit NumericDate epoch seconds (possibly fractional) for time claims
  2. Validate the claim value on the client before signing
  3. Decode the token and inspect the raw exp/nbf/iat values with a JWT debugger

Example fix

// before
claims.setExpiration(new Date("2026-09-08")); // formatted date, not NumericDate
// after
claims.setExpiration(new Date(System.currentTimeMillis() + 3600_000L)); // epoch millis -> NumericDate
Defensive patterns

Strategy: validation

Validate before calling

Object v = claims.get("exp");
boolean ok = (v instanceof Number) || (v instanceof String s && s.matches("-?\\d+(\\.\\d+)?"));

Try / catch

try { Instant exp = controllerReadNumericDate(claims); }
catch (IllegalArgumentException e) { logger.warn("Invalid time claim format"); rejectToken(); }

Prevention

When it happens

Trigger: A time claim whose string representation cannot be converted to epoch seconds — e.g. a non-numeric string like 'now', a value exceeding long range, or a fractional part with too many digits for longValueExact/movePointRight conversion.

Common situations: Clients placing formatted dates ('2026-01-01T00:00:00Z') instead of NumericDate epoch seconds in exp/nbf/iat; keys generating oversized numeric claims; corrupted JWT payloads.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of apereo/cas@e7288fc434 (2026-09-08). Data as JSON: /api/errors/2abc821ea9944636. Report an issue: GitHub.

Appendix: source

Thrown at support/cas-server-support-oidc-vc/src/main/java/org/apereo/cas/vc/presentation/OidcVerifiableCredentialPresentationResponseEndpointController.java:451

            "Key binding JWT predates the presentation transaction");
    }

    private static @Nullable Instant readNumericDate(final Map<String, Object> claims,
                                                     final String name,
                                                     final boolean required) {
        val value = claims.get(name);
        require(value != null || !required, "JWT is missing a required time claim");
        if (value == null) {
            return null;
        }
        require(value instanceof Number, "JWT time claim is invalid");
        try {
            val numericDate = new BigDecimal(value.toString());
            val components = numericDate.divideAndRemainder(BigDecimal.ONE);
            return Instant.ofEpochSecond(components[0].longValueExact(),
                components[1].movePointRight(9).longValueExact());
        } catch (final ArithmeticException exception) {
            throw new IllegalArgumentException("JWT time claim is invalid", exception);
        }
    }

    private static List<String> readAudience(final Map<String, Object> claims) {
        val audience = claims.get("aud");
        if (audience instanceof final String value) {
            return List.of(value);
        }
        if (audience instanceof final List<?> values
            && values.stream().allMatch(String.class::isInstance)) {
            return values.stream().map(String.class::cast).toList();
        }
        throw new IllegalArgumentException("JWT audience is invalid");
    }

    private static String requiredStringClaim(final Map<String, Object> claims, final String name) {
        val value = claims.get(name);
        if (!(value instanceof final String stringValue) || stringValue.isBlank()) {

View on GitHub (pinned to e7288fc434)