jwtk/jjwt · error · MalformedJwtException

Unsecured JWSs (those with an alg header value of 'none') ma

Error message

Unsecured JWSs (those with an alg header value of 'none') may not use the crit header parameter per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11 ("the [crit] Header Parameter MUST be integrity protected; therefore, it MUST occur only within [a] JWS Protected Header)". Header: %s

What it means

An unsecured JWS (alg='none') may not use the 'crit' (critical) header parameter, since per RFC 7515 Section 4.1.11 crit must occur only within the JWS Protected Header, which is integrity protected — impossible without a signature. jjwt throws MalformedJwtException when an unsecured JWS header contains a 'crit' entry.

Source

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

        final boolean unsecured = Jwts.SIG.NONE.getId().equalsIgnoreCase(alg);

        final CharSequence base64UrlDigest = tokenized.getDigest();
        final boolean hasDigest = Strings.hasText(base64UrlDigest);
        if (unsecured) {
            if (tokenized instanceof TokenizedJwe) {
                throw new MalformedJwtException(JWE_NONE_MSG);
            }
            // Unsecured JWTs are disabled by default per the RFC:
            if (!this.unsecured) {
                String msg = UNSECURED_DISABLED_MSG_PREFIX + header;
                throw new UnsupportedJwtException(msg);
            }
            if (hasDigest) {
                throw new MalformedJwtException(JWS_NONE_SIG_MISMATCH_MSG);
            }
            if (header.containsKey(DefaultProtectedHeader.CRIT.getId())) {
                String msg = String.format(CRIT_UNSECURED_MSG, header);
                throw new MalformedJwtException(msg);
            }
        } else if (!hasDigest) { // something other than 'none'.  Must have a digest component:
            String fmt = tokenized instanceof TokenizedJwe ? MISSING_JWE_DIGEST_MSG_FMT : MISSING_JWS_DIGEST_MSG_FMT;
            String msg = String.format(fmt, alg);
            throw new MalformedJwtException(msg);
        }
        // ----- crit assertions -----
        if (header instanceof ProtectedHeader) {
            Set<String> crit = Collections.nullSafe(((ProtectedHeader) header).getCritical());
            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);

View on GitHub (pinned to fb71496164)

Solutions

  1. Remove the 'crit' header parameter from unsecured tokens, or sign the token with a real algorithm if crit extensions are needed.
  2. Fix the token producer so crit is only emitted on signed/encrypted JWS/JWEs.
  3. If the extension requires crit integrity protection, switch to a signing algorithm such as ES256 or RS256.
  4. Reject such tokens at ingestion if unsecured extensions are not part of your protocol.

Example fix

// before: unsecured token with crit header
header = {"alg":"none","crit":["exp"],"exp":...};

// after: drop crit or sign the token
header = {"alg":"none"}; // or sign with Jwts.SIG.ES256 and keep crit
Defensive patterns

Strategy: validation

Validate before calling

String headerJson = new String(java.util.Base64.getUrlDecoder().decode(token.split("\\.")[0]), java.nio.charset.StandardCharsets.UTF_8);
if (headerJson.contains("alg\":\"none\"") && headerJson.contains("crit")) throw new IllegalArgumentException("unsecured JWS may not use crit");

Try / catch

try { return parser.parse(token); }
catch (io.jsonwebtoken.MalformedJwtException e) { throw new InvalidTokenException("crit on unsecured JWS", e); }

Prevention

When it happens

Trigger: Parsing an alg='none' JWS (with unsecured parsing enabled) whose protected header contains a non-empty 'crit' set.

Common situations: Custom token builders that copy full header maps (including crit) between signed and unsecured tokens; extension frameworks attaching crit headers generically regardless of algorithm; hand-crafted tokens during testing.

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 jwtk/jjwt@fb71496164 (2026-09-09). Data as JSON: /api/errors/e051fb1069496e97. Report an issue: GitHub.