JWT Malformed: Every Cause and How to Fix It
JsonWebTokenError: jwt malformed is a different failure from a bad signature — it means the string you handed to the library isn't shaped like a JWT at all, so nothing ever gets as far as checking a secret. That distinction matters because the fix is never "try a different secret" — it's almost always upstream of verification entirely: how the token got copied, stored, or passed along before it ever reached your code. This walkthrough covers five real causes and why each one actually happens, four of which throw that exact error in Node's jsonwebtoken; the fifth (a leftover Bearer prefix) throws a close sibling error for that specific library, and is included because it's the case people most often land on this page while debugging. Each has a live, prefilled JWT Verifier example so you can watch it happen instead of reading a description of it.
header.payload.signature. Most JWT libraries check that shape before anything cryptographic happens, and throw a "malformed" error the instant it's off: two segments, four segments, no dots at all, or a segment that isn't valid base64url. Fix the shape of the string you're passing in, not the secret.
Quick reference
All five results below were computed against the exact decode logic JWT Verifier runs client-side, then verified live in the tool before publishing.
| Symptom | Most likely cause | Fix |
|---|---|---|
| Fails immediately, no secret prompt even matters | Signature segment missing or truncated — only header.payload survived | Make sure whatever stored or logged the token kept all three segments |
| Fails immediately after a value got concatenated with something else | An extra .-joined value turned three segments into four | Use a delimiter other than . when combining a token with anything else |
| Fails no matter what, and the string looks nothing like a JWT | The wrong value was read — an API key, session ID, or a stray "undefined" string | Confirm you're reading the actual token field, not a neighboring one |
| Fails after passing through a cookie, gateway, or middleware layer | The whole token got base64-encoded a second time somewhere in the pipeline | Decode it back to the raw token before it reaches the verifier |
Header decodes to null but the payload looks fine | The literal string Bearer was never stripped from the Authorization header | Strip the scheme prefix before passing the value to any JWT function |
1. The signature segment is missing or got truncated
Somewhere between issuing a token and using it, only two of the three segments survive — a proxy or log line that truncates long strings, a copy-paste that stopped one character short, or someone manually stripping the signature under the mistaken idea that it's not needed just to read the claims. It is: decoding still requires all three segments to be present, even if verification against a secret happens later or never.
const token = fullToken.split('.').slice(0, 2).join('.'); // signature dropped
jwt.decode(token);
// null — jsonwebtoken silently returns null rather than throwing here,
// but jwt.verify(token, secret) on the same string throws:
// JsonWebTokenError: jwt malformed
The header and payload segments are individually intact and would decode fine on their own — the failure is purely about segment count. If you only need to read claims without checking authenticity, decoding the two remaining segments by hand will work; but any real verification path needs the signature segment back.
See it fail live
The signature segment has been dropped, leaving only header.payload:
2. A stray dot turned three segments into four
The JWT spec uses . as its only delimiter, which means concatenating a token with anything else using the same character silently breaks it. This happens most often in a cookie value or log line built by hand: `${token}.${csrfToken}`, or a token and a request ID joined for a single audit-log field. The result splits into four segments instead of three, and every JWT library treats that exactly the same as having too few.
const cookieValue = `${token}.${csrfToken}`; // '.' used as an ad-hoc separator
// later:
jwt.verify(cookieValue.split('.').slice(0, 3).join('.'), secret); // wrong slice, wrong bytes
// or, read back whole:
jwt.verify(cookieValue, secret);
// JsonWebTokenError: jwt malformed
Pick a delimiter the JWT format doesn't already use — a colon, a pipe, or just two separate cookies — instead of trusting that a dot found by splitting the string belongs to the token and not to whatever got joined onto it.
See it fail live
A valid token with an extra .a1b2c3d4 chunk appended, as if joined with a second value:
3. The wrong value was passed in entirely
Not every string that reaches a JWT function is actually a JWT. A common bug: reading a different key than intended from storage — an API key, a session ID, or a stray "undefined" or "null" string produced by an earlier bug that never got caught. None of these contain the dots a JWT needs, so they fail the same segment check as every other case here, just with a much more surprising root cause once you go looking for it.
// auth_token doesn't exist — the real key is 'authToken' — so this is
// actual null, not the string "null", and String() below is what turns
// it into three literal characters before it ever reaches jwt.verify()
const token = String(localStorage.getItem('auth_token'));
jwt.verify(token, secret);
// JsonWebTokenError: jwt malformed — token is the 4-character string "null"
// Passing the real null/undefined straight through, with no String() in
// the way, fails differently — worth telling apart from the case above:
jwt.verify(localStorage.getItem('auth_token'), secret);
// JsonWebTokenError: jwt must be provided
If the "token" you're debugging doesn't even superficially look like three chunks of random-looking characters separated by dots, stop debugging the JWT library and go check what actually got read or passed in — and note which of the two errors above you got, since it tells you whether a stray String(), template literal, or JSON round-trip turned a missing value into literal text before it reached jwt.verify().
See it fail live
A plain API-style string with no dots at all, standing in for a token by mistake:
Open this example in JWT Verifier — one segment, no decode possible, because there's nothing JWT-shaped here to decode.4. The whole token got base64-encoded a second time
Some cookie jars, session middlewares, and API gateways base64-encode whatever value they're given before storing or forwarding it — a reasonable thing to do for an opaque string, except a JWT is already encoded. If a layer in the pipeline re-encodes the entire token and a later layer forgets to decode it back before handing it to your verification code, you get a single long base64 blob with none of the original dots left — because standard base64 output never happens to reproduce the specific two dot characters from the input at the right positions.
// A gateway wraps the token before setting the cookie
res.cookie('session', Buffer.from(token).toString('base64'));
// Your code reads it back without reversing that step
const raw = req.cookies.session;
jwt.verify(raw, secret);
// JsonWebTokenError: jwt malformed
The fix is to find wherever the extra encoding step was added and either remove it or decode it back to the raw token before verification — not to assume the token itself is corrupt.
See it fail live
A real, valid token re-encoded as base64 in its entirety — the dots are gone because base64 doesn't preserve them:
Open this example in JWT Verifier — decode it yourself first with a base64 tool and you'll see the original three-segment token underneath.5. A leftover "Bearer " prefix
The Authorization HTTP header carries a scheme prefix — Bearer <token> — and the space after "Bearer" is part of the header value, not part of the token. Passing req.headers.authorization straight into a JWT function without stripping that prefix is one of the most common integration bugs in Node APIs. It's worth checking for on its own because it doesn't fail the same way as the four cases above: the segment count still comes out to three (a space isn't a dot), so it slips past the up-front shape check entirely — the failure only shows up once the library tries to actually decode the corrupted first segment.
const token = req.headers.authorization; // "Bearer eyJhbGci..."
jwt.verify(token, secret);
// JsonWebTokenError: invalid token
// (a different error from "jwt malformed" specifically, because this one
// gets past the 3-segment check before decoding fails — worth recognizing
// as a sibling of this whole error class rather than a shape mismatch)
// Fix:
const token = req.headers.authorization.replace(/^Bearer\s+/, '');
See it fail live
The full Authorization header value, scheme prefix and all, pasted in as if it were the token itself:
.env file is a common first guess, but base64url decoding in browsers and in Node's own atob/Buffer.from is forgiving of ASCII whitespace and strips it before decoding. It's worth ruling out quickly, but don't stop there if the token still fails once whitespace is trimmed — one of the five causes above is far more likely to be the actual bug.
Debugging checklist
- Count the segments first — split on
.and confirm you get exactly three. Anything else is a shape problem, not a cryptography problem. - If the count is three but decoding still fails, check for a leftover
Bearerprefix or other scheme text before the token starts. - If the string has no dots at all, confirm you're actually reading the token field you think you are, not a neighboring key or a stringified
null/undefined. - If the token passed through a cookie, gateway, or session store, check whether that layer re-encodes values — and whether the decode step on the way back out was skipped.
- Trim whitespace as a quick sanity check, but don't assume it's the cause just because it's the easiest thing to try.
🔐 Paste your token and see exactly where it breaks
JWT Verifier decodes the header and claims, checks exp/nbf, and verifies HS256 signatures entirely in your browser using the Web Crypto API — nothing is uploaded, and nothing is stored beyond your own device.