apereo/cas · error · IllegalArgumentException

JWT audience is invalid

Error message

JWT audience is invalid

What it means

readAudience normalizes the JWT 'aud' claim to a list of strings, accepting a single string or a list of all-strings. Anything else (non-string values, mixed list, null with wrong shape) throws IllegalArgumentException("JWT audience is invalid").

Solutions

  1. Set 'aud' in the key-binding JWT to either a single string or an array of strings
  2. Decode the JWT and inspect the aud claim's JSON type
  3. Fix the JWT-building library/config that emits the malformed audience

Example fix

// before
claims.setAudience(12345); // numeric audience
// after
claims.setAudience("https://cas.example.org/cas/oidc"); // string audience
Defensive patterns

Strategy: validation

Validate before calling

Object aud = claims.get("aud");
boolean ok = aud instanceof String || (aud instanceof List<?> l && l.stream().allMatch(String.class::isInstance));

Prevention

When it happens

Trigger: validateKeyBindingJwt reads 'aud' from the key-binding JWT and finds a claim that is neither a String nor a List whose elements are all Strings — e.g. aud as a number, object, or list containing non-strings.

Common situations: Client libraries encoding aud as an object or nested list; manually crafted key-binding JWTs with a numeric client_id; serialization frameworks turning single aud into an unexpected type.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

            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()) {
            throw new IllegalArgumentException("JWT string claim is missing or invalid");
        }
        return stringValue;
    }

    private static boolean constantTimeEquals(final String left, final String right) {
        return MessageDigest.isEqual(left.getBytes(StandardCharsets.UTF_8), right.getBytes(StandardCharsets.UTF_8));
    }

    private static ResponseEntity<Map<String, Object>> buildResponse(final HttpStatus status,
                                                                     final Map<String, Object> body) {
        return ResponseEntity.status(status)
            .cacheControl(CacheControl.noStore())

View on GitHub (pinned to e7288fc434)