jwtk/jjwt · error · SignatureException

Unable to verify JWS signature: the parser has encountered a

Error message

Unable to verify JWS signature: the parser has encountered an Unencoded Payload JWS with detached payload, but the detached payload value required for signature verification has not been provided. If you expect to receive and parse Unencoded Payload JWSs in your application, the overloaded JwtParser.parseSignedContent or JwtParser.parseSignedClaims methods that accept a byte[] or InputStream must be used for these kinds of JWSs. Header: %s

What it means

For Unencoded Payload JWSs (b64=false, 'b64' critical) with a detached payload, the compact string contains no payload segment, so the caller must supply the raw payload bytes for signature verification. When unencodedPayload is empty during parse, jjwt throws SignatureException instructing the caller to use the parseSignedContent(byte[]) / parseSignedClaims(byte[]) overloads.

Source

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

        // 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
            // (JwsHeader).isPayloadEncoded() check
            if (Strings.hasText(payloadToken)) {
                // we need to verify what was in the token, otherwise it'd be a security issue if we ignored it
                // and assumed the (likely safe) unencodedPayload value instead:
                payload = new Payload(payloadToken, header.getContentType());
            } else {
                //no payload token (a detached payload), so we need to ensure that they've specified the payload value:
                if (unencodedPayload.isEmpty()) {
                    String msg = String.format(B64_MISSING_PAYLOAD, header);
                    throw new SignatureException(msg);
                }
                // otherwise, use the specified payload:
                payload = unencodedPayload;
            }
        }

        if (tokenized instanceof TokenizedJwe && payload.isEmpty()) {
            // Only JWS payload can be empty per https://github.com/jwtk/jjwt/pull/540
            String msg = "Compact JWE strings MUST always contain a payload (ciphertext).";
            throw new MalformedJwtException(msg);
        }

        byte[] iv = null;
        byte[] digest = null; // either JWE AEAD tag or JWS signature after Base64Url-decoding
        if (tokenized instanceof TokenizedJwe) {

            TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized;
            JweHeader jweHeader = Assert.stateIsInstance(JweHeader.class, header, "Not a JweHeader. ");

View on GitHub (pinned to fb71496164)

Solutions

  1. Use the detached-payload overloads: parser.parseSignedContent(detachedPayloadBytes) or parseSignedClaims(detachedPayloadBytes).
  2. Ensure the exact raw (unencoded) payload bytes that were signed are supplied — any difference fails verification.
  3. If you don't actually use unencoded payloads, have the issuer stop setting b64:false/crit:["b64"].
  4. Check ordering bugs where the payload is fetched asynchronously but parse is called before it arrives.

Example fix

// before
Jws<Claims> jws = Jwts.parser().verifyWith(key)
    .critical().add("b64").and().build().parse(token);

// after: supply the detached payload
byte[] payload = ...; // exact raw payload that was signed
Jws<Claims> jws = Jwts.parser().verifyWith(key)
    .critical().add("b64").and().build()
    .parseSignedClaims(payload, token);
Defensive patterns

Strategy: validation

Validate before calling

if (headerB64FalseDetachedPayload && (unencodedPayload == null || unencodedPayload.length == 0)) throw new IllegalStateException("detached payload bytes required: use parseSignedContent(payload, token)");

Try / catch

try { return parser.parseSignedContent(detachedPayload, token); }
catch (io.jsonwebtoken.SignatureException e) { throw new InvalidTokenException("missing detached payload for unencoded-payload JWS", e); }

Prevention

When it happens

Trigger: Calling parse() (no payload argument) on a JWS with header {"b64":false,"crit":["b64"]} and an empty payload segment; or calling an overload passing null/empty byte[] for the detached payload.

Common situations: Detached-payload flows (e.g. OAuth DPoP-like or large-payload signing) where the application received the JWS but the payload separately and forgot to pass it; calling plain parse() out of habit instead of parseSignedContent(payload).

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


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