jwtk/jjwt · error · UnsupportedJwtException

Protected Header crit set references unsupported header name

Error message

Protected Header crit set references unsupported header name '%s'. Application developers expecting to support a JWT extension using header '%s' in their application code must indicate it is supported by using the JwtParserBuilder.critical method. Header: %s

What it means

For each name in a protected header's 'crit' set, the parser also requires that the application has explicitly declared support via JwtParserBuilder.critical(...). RFC 7515 mandates that crit-protected extensions must be understood; jjwt enforces this by throwing UnsupportedJwtException when a crit name is not in the parser's supported-critical set, even if the header contains the parameter.

Source

Thrown at impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java:449

            Set<String> supportedCrit = this.critical;
            String b64Id = DefaultJwsHeader.B64.getId();
            if (!unencodedPayload.isEmpty() && !this.critical.contains(b64Id)) {
                // The application developer explicitly indicates they're using a B64 payload, so
                // ensure that the B64 crit header is supported, even if they forgot to configure it on the
                // parser builder:
                supportedCrit = new LinkedHashSet<>(Collections.size(this.critical) + 1);
                supportedCrit.add(DefaultJwsHeader.B64.getId());
                supportedCrit.addAll(this.critical);
            }
            // assert any values per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11:
            for (String name : crit) {
                if (!header.containsKey(name)) {
                    String msg = String.format(CRIT_MISSING_MSG, name, name, header);
                    throw new MalformedJwtException(msg);
                }
                if (!supportedCrit.contains(name)) {
                    String msg = String.format(CRIT_UNSUPPORTED_MSG, name, name, header);
                    throw new UnsupportedJwtException(msg);
                }
            }
        }

        // =============== Payload =================
        final CharSequence payloadToken = tokenized.getPayload();
        Payload payload;
        boolean integrityVerified = false; // only true after successful signature verification or AEAD decryption

        // check if b64 extension enabled:
        final boolean payloadBase64UrlEncoded = !(header instanceof JwsHeader) || ((JwsHeader) header).isPayloadEncoded();
        if (payloadBase64UrlEncoded) {
            // standard encoding, so decode it:
            byte[] data = decode(payloadToken, "payload");
            payload = new Payload(data, header.getContentType());
        } else {
            // The JWT uses the b64 extension, and we already know the parser supports that extension at this point
            // in the code execution path because of the ----- crit ----- assertions section above as well as the

View on GitHub (pinned to fb71496164)

Solutions

  1. Register the extension on the parser builder: Jwts.parser().critical().add("name").and() (and implement/verify its semantics).
  2. For unencoded-payload JWSs, ensure the parser is built to accept 'b64' as critical and use parseSignedContent(byte[] payload).
  3. Coordinate with the token issuer to confirm which crit extensions are in use and document them in all consumers.
  4. If the extension is not needed, remove it from the token's crit set at the producer.

Example fix

// before
JwtParser p = Jwts.parser().verifyWith(key).build();
p.parseSignedClaims(token); // header has crit:["b64"]

// after
JwtParser p = Jwts.parser().verifyWith(key)
    .critical().add("b64").and().build();
Defensive patterns

Strategy: validation

Validate before calling

java.util.Map<String,Object> h = parseHeaderJson(token);
java.util.Set<String> supported = java.util.Set.of("b64"); // extensions you actually implement
@SuppressWarnings("unchecked") java.util.List<String> crit = (java.util.List<String>) h.get("crit");
if (crit != null && !supported.containsAll(crit)) throw new IllegalArgumentException("unsupported crit extensions: " + crit);

Try / catch

try { return parser.parse(token); }
catch (io.jsonwebtoken.UnsupportedJwtException e) { throw new InvalidTokenException("unsupported crit extension", e); }

Prevention

When it happens

Trigger: Parsing a token whose header contains "crit":["name"] (with "name" present in the header) while the parser was built without .critical("name") — common with the 'b64' unencoded-payload extension or custom extension headers.

Common situations: Receiving tokens using the b64/unencoded-payload extension without configuring the parser; adding custom crit headers on the producer side but not registering them in every consuming service; version upgrades where new crit extensions appear from an issuer.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09). Data as JSON: /api/errors/cba5b85c7a02467c. Report an issue: GitHub.