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
- Use the detached-payload overloads: parser.parseSignedContent(detachedPayloadBytes) or parseSignedClaims(detachedPayloadBytes).
- Ensure the exact raw (unencoded) payload bytes that were signed are supplied — any difference fails verification.
- If you don't actually use unencoded payloads, have the issuer stop setting b64:false/crit:["b64"].
- 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
- Always use parseSignedContent/parseSignedClaims overloads that take the payload when tokens may be detached
- Pass the exact raw bytes that were signed; don't re-encode them
- Ensure the payload is fetched before invoking the parser
- Reserve plain parse() for standard (non-detached) tokens
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
- Unexpected content JWS.
- Unexpected Claims JWS.
- Both 'signWith' and 'encryptWith' cannot be specified. Choos
- 'b64' Unencoded payload option has been specified, but paylo
- Unsupported signature algorithm '${alg}': ${e.getMessage()}
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/616d34cbb1e28585.
Report an issue: GitHub.