JWT Malformed Error: Why It Happens and How to Fix It Correctly

Quick answer

💡A JWT malformed error means the token string does not conform to the three-part base64url.base64url.base64url format, or its header or payload do not decode to valid JSON. The most common causes are a Bearer prefix left in the string, whitespace or line breaks injected during copy-paste, or URL-unsafe base64 characters (+ and /) used instead of the base64url alphabet (- and _).

Error symptoms

  • JsonWebTokenError: jwt malformed (jsonwebtoken library in Node.js)
  • Not enough segments (jose or python-jose)
  • invalid_token: malformed JWT (Auth0, Okta error responses)
  • JWT verification fails in production but the token decodes fine in jwt.io
  • Token works in Postman but fails in the Authorization header from the browser
  • DecodeError raised when calling jwt.decode() in PyJWT

Common causes

  • Passing the full Authorization header value including 'Bearer ' to the JWT library
  • Whitespace, newlines, or line breaks introduced during copy-paste or storage
  • Standard base64 padding characters (=) or non-URL-safe characters (+ and /) in the token
  • Only two segments instead of three (header.payload without a signature)
  • Header or payload segment that does not decode to valid JSON
  • Token stored in a cookie that was URL-encoded, changing + to %2B and breaking parsing

When it happens

  • Extracting the token from an Authorization header without stripping the Bearer prefix
  • Storing a JWT in localStorage and retrieving it with extra whitespace
  • Receiving a token from an OAuth provider that uses standard base64 instead of base64url
  • Passing a JWT through a URL parameter without proper encoding handling
  • Migrating from one JWT library to another with different parsing strictness

The three-part JWT structure: header, payload, and signature

A JSON Web Token is a compact, URL-safe string made of exactly three parts separated by periods. The first part is the header, a base64url-encoded JSON object that identifies the algorithm used to sign the token and the token type. A typical header decodes to something like {"alg": "HS256", "typ": "JWT"}. The second part is the payload, a base64url-encoded JSON object containing claims — statements about the subject such as the user's ID, roles, and the time the token expires. The third part is the signature, which is the cryptographic proof that the header and payload were produced by a party with the signing key and have not been modified.

The signature is computed differently depending on the algorithm. For HS256 (HMAC-SHA256), the signature is HMAC-SHA256(base64url(header) + '.' + base64url(payload), secret). For RS256 (RSA-SHA256), it is an RSA signature over the same input using a 2048-bit or larger private key. The corresponding public key is used to verify. For ES256 (ECDSA with P-256 and SHA-256), the signature uses an elliptic curve private key. In every case, the signature is over the exact ASCII bytes of the encoded header and payload, separated by a period — not over the decoded JSON objects.

This three-part structure is the foundation of every JWT malformed error. Any string that does not contain exactly two period characters is definitionally malformed. A JWT with only one period has no signature segment and a JWT with three or more periods cannot be parsed as a standard token. JWT libraries count the segments before attempting any base64 decoding, which is why the error "not enough segments" or "too many segments" appears before any cryptographic operation.

The base64url encoding used for each part differs from standard base64 in three ways: + is replaced with -, / is replaced with _, and the trailing padding character = is omitted. This makes the token safe to use directly in URL query parameters and HTTP headers without percent-encoding. Standard base64 characters like + and / are not illegal in HTTP headers, but they can cause problems in URL contexts, cookie values, and some JWT parsers that enforce strict base64url compliance.

Understanding this structure makes most malformed JWT errors immediately diagnosable. Split the token on periods and count the parts: you should get exactly three non-empty strings. Base64url-decode each of the first two parts and attempt to JSON-parse them. If either fails, the malformation is in that segment. If both decode and parse correctly, the malformation is likely in how the token is being passed to the library — a Bearer prefix, whitespace, or encoding mismatch.

A JWT that decodes is not a JWT that is valid

jwt.io and browser atob() will decode any base64url segment regardless of whether the signature is valid or the claims are trustworthy. Always verify the signature with a JWT library before trusting any claim in the payload. Decoding and verifying are separate operations.

What triggers a malformed JWT error in different libraries

Different JWT libraries apply different levels of strictness, which explains why a token that is accepted by one tool may be rejected by another. Understanding the specific behavior of the library you are using is as important as understanding the JWT standard itself.

The popular Node.js library jsonwebtoken throws JsonWebTokenError: jwt malformed for any token that cannot be split into three parts or whose parts cannot be base64url-decoded and JSON-parsed. It is strict about the segment count and will reject tokens with the Bearer prefix, leading whitespace, or trailing newlines. The jose library (also Node.js, and used in many frameworks including Next-Auth) applies similar strictness but produces different error messages like JWSInvalid or CompactDecodeError, which can be confusing when debugging because the word "malformed" does not appear.

In Python, PyJWT raises a DecodeError for structural problems with the token string. python-jose raises JWTError or JOSEError. Both libraries are strict about the three-segment structure and the base64url alphabet. A token with a trailing = padding character that a lenient decoder might silently accept will cause a strict library to raise an error. Auth0's authentication middleware wraps these library errors and returns them as HTTP 401 responses with error: invalid_token and a message that sometimes includes the underlying library's error text.

The strictness gap between jwt.io and production JWT libraries is a significant source of confusion. The jwt.io debugger is deliberately permissive: it accepts tokens with Bearer prefixes, strips whitespace, and tolerates minor encoding variations. A developer pastes a token into jwt.io, sees the payload decode correctly, concludes the token is valid, and then cannot understand why the production library rejects it. The solution is to test with the actual library in the actual runtime, not with a browser-based debugging tool.

One particularly confusing case is when a JWT is transported in a URL query parameter and the token contains + characters from standard base64 output. URL parsers interpret + as a space character in query strings, so a token retrieved from req.query.token in Express may have its + characters silently converted to spaces. The token then fails base64url decoding because spaces are not in the base64url alphabet. Always use encodeURIComponent() when placing a JWT in a URL and decodeURIComponent() when reading it back, even though the base64url encoding technically should not require this for well-formed tokens.

Example 1

Extract the token from the Authorization header correctly before passing it to the JWT library.

Stripping the Bearer prefix before JWT verification

❌ Wrong

const jwt = require('jsonwebtoken');

app.get('/api/me', (req, res) => {
  // Wrong: passes 'Bearer eyJhbGci...' including the scheme prefix
  const token = req.headers.authorization;

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    res.json({ userId: payload.sub });
  } catch (err) {
    // Throws: JsonWebTokenError: jwt malformed
    res.status(401).json({ error: err.message });
  }
});

✅ Fixed

const jwt = require('jsonwebtoken');

app.get('/api/me', (req, res) => {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing or invalid Authorization header' });
  }

  // Extract token after 'Bearer '
  const token = authHeader.slice(7).trim();

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'],  // Explicit algorithm — never rely on default
      issuer: 'https://api.example.com',
      audience: 'web-client'
    });
    res.json({ userId: payload.sub });
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
});

The broken version passes the full Authorization header value including the 'Bearer ' scheme prefix, which jwt.verify() cannot parse as a JWT. The fixed version slices off the prefix, trims whitespace, and passes an explicit algorithms allowlist plus issuer and audience constraints to prevent algorithm confusion and cross-service token reuse.

Bearer prefix, whitespace, and encoding problems in token strings

The Bearer prefix is the most common cause of a malformed JWT error in newly written code. The HTTP Authorization header for bearer token authentication is formatted as Authorization: Bearer eyJhbGci.... The value of the header includes the scheme name "Bearer" followed by a space followed by the token. When you read this header value in Express.js with req.headers.authorization, you get the full string including "Bearer ". Passing this to jwt.verify() produces a malformed error because "Bearer eyJhbG..." has no period before the first segment — the library sees the word "Bearer" as the header segment and attempts to base64-decode it, which fails.

The fix is to extract the token from the header string: const token = req.headers.authorization?.split(' ')[1]. A more defensive version checks the scheme: const [scheme, token] = header.split(' '); if (scheme.toLowerCase() !== 'bearer') return 401. Some middleware, like Passport.js or the Auth0 Express SDK, handles this extraction automatically, but many developers bypass middleware for lightweight routes and reintroduce the bug manually.

Whitespace is the second common culprit. JWTs are frequently stored in browser localStorage, copied from a terminal, pasted into API testing tools, or stored in environment variables. Each transfer introduces an opportunity for leading spaces, trailing spaces, or newline characters to be appended or prepended. A JWT stored in an environment variable with NEXT_PUBLIC_TEST_TOKEN=eyJhb... and a trailing newline will fail verification because the newline becomes part of the string. Trim the token before passing it to the library: token.trim() in JavaScript or token.strip() in Python.

Line breaks are especially problematic because they are invisible in most text editors and logging tools. A multi-line environment variable value, a JWT pasted from a formatted JSON response with line wrapping, or a token truncated by a log line limit may contain embedded newline characters. Use token.replace(/\s+/g, '') to strip all whitespace including embedded newlines, or better yet, enforce that JWT storage and retrieval functions always trim their output.

URL encoding is a third category of encoding problem. Cookies transmitted over HTTP may be URL-encoded by the browser, converting the base64url characters to percent-encoded sequences. When the server reads the cookie value, if it does not URL-decode it first, the percent-encoded characters make the token unparseable as a JWT. Always use a cookie library that handles decoding automatically, and verify the raw cookie value in browser DevTools to confirm what is actually being transmitted.

URL-safe Base64 vs standard Base64 in JWT encoding

JWT uses base64url encoding, which is a variant of standard base64 defined in RFC 4648 section 5. The two differences are character substitutions — + becomes - and / becomes _ — and the omission of trailing = padding characters. These changes make the resulting string safe to embed in URLs and HTTP headers without percent-encoding. Understanding this distinction is essential because many base64 implementations default to the standard alphabet, not the URL-safe variant, and produce tokens with characters that strict JWT parsers reject.

When decoding a JWT segment manually, you may need to re-add the stripped padding before passing it to a standard base64 decoder. JWT drops the padding, but most base64 decoders require padding to determine where the encoded data ends. The correct padding length is (4 - len(segment) % 4) % 4 equals signs. In JavaScript, this calculation produces 0, 1, or 2 equals signs, because the only impossible case for a valid base64url string is 3 padding characters. Manually implementing this calculation is error-prone; prefer a library that handles JWT decoding natively.

A subtle interoperability problem arises when one system generates a JWT with a library that uses standard base64 instead of base64url. The resulting token contains + and / characters instead of - and _. Some JWT parsers silently substitute these characters before decoding; others reject them immediately. jwt.io substitutes silently, which is why the token appears valid in the browser tool but fails in a strict production library. To confirm whether your token uses the correct alphabet, search the raw token string for + and / characters. Any occurrence indicates non-compliant base64 encoding.

In Python, the base64 module provides both base64.b64decode() for standard base64 and base64.urlsafe_b64decode() for base64url. Using b64decode() on a JWT segment will fail when the segment contains - or _ characters, because these are not in the standard alphabet. Always use urlsafe_b64decode() for JWT segments. In JavaScript, the atob() function accepts only standard base64, so you must replace - with + and _ with / before calling it: atob(segment.replace(/-/g, '+').replace(/_/g, '/')). Modern JWT libraries handle all of this internally, which is another reason to use a library rather than decoding tokens manually.

Padding also creates problems in storage. Some systems automatically strip trailing = characters from strings, or add them unconditionally to make the string length a multiple of 4. If a JWT is stored in a database column with a transformation that normalizes base64 padding, the retrieved token may have different padding than the original and fail parsing. JWT libraries tolerate missing padding because the RFC specifies that base64url omits it, but unexpected added padding may cause an error in parsers that treat the = as a literal character rather than padding.

Example 2

Correctly decode a JWT payload in both JavaScript and Python, accounting for base64url alphabet and missing padding.

Decoding a JWT segment manually with base64url handling

❌ Wrong

// JavaScript — wrong: atob() requires standard base64, not base64url
function decodePayload(token) {
  const segment = token.split('.')[1];
  // Fails if segment contains - or _ characters
  return JSON.parse(atob(segment));
}

# Python — wrong: b64decode() uses standard alphabet
import base64, json
def decode_payload(token):
    segment = token.split('.')[1]
    # Raises binascii.Error for - and _ characters
    return json.loads(base64.b64decode(segment))

✅ Fixed

// JavaScript — correct: replace base64url chars, add padding
function decodePayload(token) {
  const segment = token.split('.')[1];
  const base64 = segment
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  const padded = base64.padEnd(
    base64.length + (4 - base64.length % 4) % 4, '='
  );
  return JSON.parse(atob(padded));
}

# Python — correct: urlsafe_b64decode handles - and _ automatically
import base64, json
def decode_payload(token: str) -> dict:
    segment = token.split('.')[1]
    # urlsafe_b64decode accepts missing padding
    padded = segment + '=' * (4 - len(segment) % 4)
    return json.loads(base64.urlsafe_b64decode(padded))

Standard base64 decoders reject the - and _ characters used in base64url encoding. JavaScript's atob() requires + and / and needs explicit padding. Python's urlsafe_b64decode() handles the alphabet correctly but still needs padding added. Both fixed versions restore padding and use the correct decoder. Note that for production use, always verify the signature with a JWT library rather than decoding manually.

Always specify the expected algorithm explicitly

Pass algorithms: ['HS256'] or algorithms: ['RS256'] to jwt.verify() and jwt.decode(). Never let the token's own alg header dictate the verification algorithm. This single option prevents both the alg:none attack and algorithm confusion attacks with minimal additional code.

Validating JWT claims: exp, iat, iss, and aud

A JWT that parses without a malformed error is not necessarily valid or safe to use. Structural parsing and cryptographic signature verification are separate from claim validation. A token with a valid signature may still be expired, issued by the wrong party, or intended for a different audience. Skipping claim validation after successful signature verification is a common and serious mistake.

The exp claim (expiration time) is a Unix timestamp after which the token must not be accepted. JWT libraries typically check exp automatically and throw a TokenExpiredError if the current time is past the expiration. The iat claim (issued at) is the Unix timestamp when the token was created. Some libraries use iat to enforce maximum token age, rejecting tokens that are structurally valid but were issued too long ago. Always verify that your library's clock tolerance configuration — called clockTolerance or leeway — is set appropriately for your infrastructure's clock synchronization accuracy. A generous tolerance of 60 seconds is standard; a tolerance of several minutes introduces replay windows.

The iss claim (issuer) identifies who created the token. For an OAuth identity provider like Auth0 or Google, the issuer is the provider's URL (e.g., https://accounts.google.com). Your verification code must check that the iss in the token matches the expected issuer. Without this check, a token issued by one provider can be presented to a service that trusts a different provider, and if both use the same key format, the token may verify cryptographically while being logically invalid.

The aud claim (audience) identifies who the token is intended for. When an identity provider issues tokens for multiple services, each token specifies the intended service in the aud claim. If your service accepts tokens without verifying aud, an attacker who obtains a valid token for any of your organization's services can use it against any other service that trusts the same identity provider. This cross-service token reuse attack is prevented by strict aud validation.

Beyond these standard claims, many applications include custom claims like roles, permissions, or tenant IDs. These are not validated cryptographically — any attacker who can modify the payload without invalidating the signature (which should be impossible with a correct implementation) could change custom claims. The guarantee is that custom claims can be trusted only after the signature and standard claims have been validated. Do not use custom claims as the sole authorization mechanism without verifying the signature first.

The alg:none attack and algorithm confusion vulnerabilities

The alg:none attack is one of the most well-known JWT vulnerabilities and serves as a useful example of how the JWT standard's flexibility can be exploited when libraries are implemented carelessly. The JWT specification allows a special algorithm value of none, which means the token is unsigned — the signature segment is empty or absent. Early JWT library implementations accepted alg:none by default, allowing an attacker to take any valid token, replace the header's alg field with none, remove the signature segment, and present it as if it were validly signed.

The fix is to never accept alg:none tokens in production and to explicitly specify the expected algorithm when calling the verify function. In jsonwebtoken: jwt.verify(token, secret, { algorithms: ['HS256'] }). In PyJWT: jwt.decode(token, secret, algorithms=['HS256']). Most modern libraries now reject alg:none by default, but relying on the default is less secure than being explicit. Explicitly allowlisting the expected algorithm also prevents the second class of vulnerability: algorithm confusion.

Algorithm confusion attacks exploit the fact that some early JWT libraries used the algorithm from the token header to determine how to verify the signature, rather than using the algorithm configured by the application. The most dangerous form involves switching from RS256 to HS256. In RS256, the server verifies the token using its public key. The public key is, by definition, publicly available. An attacker could take the server's public key, use it as an HMAC secret to sign a forged HS256 token, and submit it to a library that accepts both HS256 and RS256 and uses the algorithm in the header to decide which to apply. The library would verify the HS256 token using the public key as the HMAC secret — and the verification would succeed, because the attacker used the same key to sign it.

The defense is to specify the expected algorithm explicitly and never allow the token itself to dictate the verification algorithm. For tokens issued by an external identity provider with RS256 or ES256, use the provider's JWKS (JSON Web Key Set) endpoint to retrieve the public key and verify with the algorithm specified by the provider's documentation, not by the alg field in the incoming token.

A third vulnerability class is key confusion between environments. A token signed with the development secret can be presented to a production service if both use HS256 and the production service does not validate the iss claim. Adding an iss claim that includes the environment name (e.g., https://api.example.com/prod versus /dev) and validating it on every request prevents cross-environment token reuse. Never share signing keys between production and non-production environments.

Frequently asked questions

What does JsonWebTokenError: jwt malformed mean exactly?

It means the string passed to jwt.verify() or jwt.decode() does not have the required three-part structure of base64url.base64url.base64url, or one of the segments cannot be decoded and parsed as JSON. The most common causes are the Bearer prefix being included, whitespace characters embedded in the token string, or standard base64 characters like + and / appearing instead of the base64url equivalents - and _.

Why does my JWT work in jwt.io but fail in production?

jwt.io is deliberately lenient and strips whitespace, accepts Bearer prefixes, and tolerates minor encoding variations. Production JWT libraries are strict. Test by running the token through your actual library in your actual runtime environment. Also verify that the token is being extracted from headers, cookies, or storage correctly before being passed to the library.

How do I fix 'not enough segments' in python-jose or PyJWT?

The token string has fewer than three period-separated segments. Split the string on '.' and count the parts — you should have exactly three. If you have two, the signature segment is missing. If you have one, the entire token structure is broken. Common causes are the Bearer prefix (which contains no periods before the token), a truncated token, or a token stored with newlines that broke the string.

What is the difference between base64 and base64url in JWT?

Base64url is a URL-safe variant that replaces + with - and / with _ and omits the trailing = padding characters. Standard base64 uses + and /, which are safe in HTTP headers but can cause issues in URLs, cookies, and strict JWT parsers. JWT always uses base64url. If your token contains + or / characters in the header or payload segments, it was encoded with standard base64 rather than base64url.

Should JWT libraries validate the exp claim automatically?

Yes, most major JWT libraries validate exp by default and throw a TokenExpiredError (Node.js jsonwebtoken) or ExpiredSignatureError (PyJWT) if the token is past its expiration time. Verify that you have not disabled expiration checking (the ignoreExpiration option in jsonwebtoken, for example). Always set short expiration times for access tokens — typically 15 minutes to one hour — and use refresh tokens for longer sessions.

What is the alg:none JWT attack?

Certain early JWT libraries accepted tokens with alg set to 'none' in the header, treating them as valid unsigned tokens regardless of the signature. An attacker could modify the payload, set alg to none, remove the signature, and present a forged token. Defend against this by explicitly specifying the allowed algorithms when calling verify: jwt.verify(token, secret, { algorithms: ['HS256'] }). Never allow 'none' in the algorithms list.

What is an algorithm confusion attack in JWT?

An algorithm confusion attack exploits JWT libraries that determine the verification algorithm from the token's alg header rather than from the application configuration. The classic version switches a token from RS256 to HS256: an attacker signs a forged token using the server's public key as the HMAC secret. If the library accepts HS256 when the application expects RS256, the verification passes. Fix it by explicitly specifying the expected algorithm and never accepting algorithm choices from the incoming token.

Do I need to validate iss and aud claims manually or does the library handle it?

Most JWT libraries can validate iss and aud automatically when you pass them as options to the verify function. In jsonwebtoken: jwt.verify(token, key, { issuer: 'expected-iss', audience: 'expected-aud' }). In PyJWT: jwt.decode(token, key, algorithms=['RS256'], issuer='expected', audience='expected'). Always configure these options for production code, as missing aud validation is a common source of cross-service token reuse vulnerabilities.

Related guides

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-07.