JSON Web Tokens (JWT) are the backbone of modern API authentication, but when they fail, they fail silently or with cryptic, frustrating error messages. The most notorious is the Invalid Signature JWT error, followed closely by Token Expired JWT and Invalid Audience JWT. Debugging these issues is a common pain point for developers because the token itself is an opaque, encoded string. You cannot easily read it without tools.
When faced with a failing token, the instinct is often to copy it and paste it into the first online decoder you find. Stop. Do not do this. Pasting production JWTs into random websites is a severe security risk. If a third-party site logs your token, they gain immediate impersonation access to your production APIs. This is exactly why we built the ZeroData JWT Debugger. It decodes base64url payloads entirely offline, in your browser. Your token never touches a server.
In this comprehensive guide, we will dissect the top five JWT errors developers encounter. We will explain exactly why they happen and provide concrete, step-by-step diagnostic workflows to fix them safely.
Never expose production tokens. Use our 100% client-side tools:
- JWT Debugger — Safely decode base64url payloads and headers offline.
- JWT Expiry Checker — Instantly verify the 'exp' and 'iat' timestamps.
- JWT Signature Verifier — Verify RS256/HS256 signatures locally.
Error #1: Invalid Signature JWT
The Invalid Signature error is the most critical and common roadblock. It means the backend API successfully parsed the token but rejected it because the cryptographic signature attached to the token does not match the signature computed by the backend.
Why It Happens
- Secret Mismatch: In HS256 (symmetric), the secret key used by the issuer is different from the secret key configured on the API.
- Public/Private Key Confusion: In RS256 (asymmetric), the API might be incorrectly trying to verify the token using the private key instead of the public key.
- Tampered Payload: A proxy, client, or attacker modified the payload claims (e.g., changing
role: usertorole: admin). Because the payload is part of the signature generation, any modification renders the signature invalid. - Wrong Algorithm: The token was signed with RS256, but the server is explicitly attempting to verify it using HS256.
Step-by-Step Diagnostic Workflow
- Check the Algorithm: Decode the token header (the first part before the first dot). Check the
algparameter. If it saysRS256, your backend must verify using a public key. If it saysHS256, your backend must verify using a shared secret. - Verify the Secret/Key: If using HS256, double-check your environment variables. A common mistake is a trailing space or newline character in the
JWT_SECRETenv variable. If using RS256, ensure your backend is correctly fetching the JWKS (JSON Web Key Set) endpoint. - Inspect Token Integrity: Ensure no middleware in your network stack is modifying the Authorization header. Some aggressive WAFs (Web Application Firewalls) or proxies might strip or alter headers.
How to fix: Configure your backend JWT middleware to explicitly fetch the correct keys and whitelist the expected algorithm. Here is a robust example for Node.js using RS256 and JWKS:
For a deeper dive into how signatures work and how to protect against advanced attacks (like the algorithm confusion attack), read our JWT Security Complete Guide.
Error #2: Token Expired JWT (The 'exp' Claim)
A Token Expired JWT error is intentionally designed behavior. It means the token was valid, but its lifetime has elapsed. The API correctly rejected it. However, if this happens unexpectedly or immediately upon login, it indicates a configuration or architectural flaw.
Why It Happens
- Natural Expiration: The user has been active longer than the token's lifespan (e.g., 15 minutes) and your frontend lacks a refresh token mechanism.
- Clock Skew: The server that issued the token and the server validating the token have desynchronized system clocks. If the verifying server is 2 minutes ahead of the issuing server, a 1-minute token will expire before it even arrives.
- Milliseconds vs Seconds: The JWT specification (RFC 7519) mandates that the
expandiatclaims must be Unix timestamps in seconds. If you issue a token using JavaScript'sDate.now()(which returns milliseconds), the token will be severely miscalculated.
Step-by-Step Diagnostic Workflow
- Inspect the Claims: Paste your token into a secure, offline tool like our JWT Expiry Checker. Look at the
iat(Issued At) andexp(Expiration Time) values. - Check NTP Sync: Ensure all servers in your fleet (auth servers and resource servers) are synchronized using NTP (Network Time Protocol).
How to fix: First, ensure your backend allows a slight "clock skew" or "leeway" to tolerate minor time drift between servers. Most robust libraries support this configuration:
Second, implement a robust Refresh Token pattern. When the frontend receives a 401 Unauthorized (Token Expired) response, it should automatically use an HTTP-only refresh cookie to request a new access token, without forcing the user to log in again.
Error #3: Invalid Audience JWT (The 'aud' Claim)
The Invalid Audience JWT error is heavily prevalent when integrating with identity providers like Auth0, Okta, or AWS Cognito. The aud claim specifies the intended recipient (the "audience") of the token.
Why It Happens
If your frontend requests a token from Auth0, it must specify the API identifier (e.g., https://api.my-app.com). The identity provider embeds this into the aud claim. When the backend receives the token, it checks: "Is this token meant for me?" If the backend expects https://api.my-app.com but the token says aud: "my-frontend-client-id", the backend throws an Invalid Audience error.
Step-by-Step Diagnostic Workflow
- Decode the Token: Use the ZeroData offline debugger to view the payload. Locate the
audfield. It can be a single string or an array of strings. - Compare with Backend Config: Check your backend code (like the Express middleware example above). The string defined in the backend must match the token's
audexactly.
How to fix: Modify your frontend's authentication request to include the correct audience. For example, in an Auth0 SPA configuration, ensure you pass the audience parameter during the login redirect.
Error #4: Issuer Mismatch (The 'iss' Claim)
Similar to the audience claim, the iss (Issuer) claim dictates exactly who generated the token. If an API expects tokens from a specific authority but receives one from elsewhere, it throws an Issuer Mismatch error.
Why It Happens
This almost exclusively happens in environments with multiple tenants or stages. A developer might log into the Staging environment (issuer: https://staging.auth0.com/), but accidentally send that token to the Local or Production backend, which expects https://prod.auth0.com/.
Another common culprit is the trailing slash. https://my-domain.com is NOT the same issuer as https://my-domain.com/.
How to fix
Decode your token, observe the exact string in the iss claim, and ensure your backend environment variables perfectly match that string, character for character, including any trailing slashes.
Error #5: Malformed Token & Decoding Base64URL
A Malformed Token error means the parser couldn't even reach the signature or claim validation phase. The token string itself is structurally invalid.
Why It Happens
- Missing Dots: A valid JWT must have exactly two dots separating three base64url-encoded strings (header, payload, signature).
- Truncation: HTTP header limits or poor string concatenation truncated the token.
- Base64 vs Base64URL: The payload was encoded using standard Base64 (which includes
+,/, and=padding) instead of Base64URL (which replaces those characters to ensure URL safety).
How to Decode Base64URL in Custom Code
When building custom middleware or debugging scripts, developers often struggle to decode the token correctly because standard Base64 decoders fail on Base64URL strings. You must convert Base64URL back to standard Base64, add padding, and then decode.
JavaScript (Browser or Node.js) implementation:
Python implementation:
Error #6: Algorithm Confusion ('alg: none' & Key Switching)
One of the most insidious and catastrophic cryptographic vulnerabilities in API authentication is the **Algorithm Confusion Attack** (also known as the `alg: none` vulnerability or asymmetric-to-symmetric key switching). While many modern libraries have added defaults to block this, legacy middleware and misconfigured microservices still fall prey to this exploit daily.
The 'alg: none' Attack Mechanism
The JWT specification originally defined a special algorithm named none intended for situations where token integrity is already guaranteed by an underlying transport layer (such as two mutually authenticated backend servers communicating over mTLS). When a token's header specifies alg: "none", the signature section at the end of the token string is completely omitted (leaving only `header.payload.`).
If your verification code naïvely trusts the token header and evaluates the signature algorithm dynamically from the token (jwt.verify(token, secret, { algorithms: [token.header.alg] })), an attacker can simply intercept their token, change `role: "user"` to `role: "superadmin"`, change the header to {"alg": "none", "typ": "JWT"}, strip the signature, and send it to your API. The vulnerable server sees `alg: none`, assumes no signature check is required, and grants admin privileges.
The RS256-to-HS256 Key Switching Attack
In an asymmetric setup (`RS256`), your authentication server signs tokens using a strictly guarded **Private Key**, while your backend APIs verify those tokens using a widely distributed **Public Key** (often hosted openly at `/.well-known/jwks.json`).
If an attacker modifies the token header to specify `alg: "HS256"` (a symmetric algorithm where the same key is used for both signing and verifying) and signs the modified payload using your **Public Key string** as the HMAC secret, a misconfigured verification library will look at the header, switch to `HS256` mode, load the configured verification key (which happens to be the Public Key), and successfully validate the attacker's forged token!
How to Diagnose & Prevent Algorithm Confusion
Never allow the token itself to dictate which algorithm is acceptable. Your verification middleware must hardcode or strictly whitelist only the exact cryptographic algorithms expected by your architectural tier:
- Explicit Algorithm Whitelisting: Always pass an explicit `algorithms: ["RS256"]` array to your JWT verification function. If a token arrives with `alg: "HS256"` or `alg: "none"`, the library immediately drops the request with a `JsonWebTokenError: invalid algorithm` exception before inspecting the signature.
- Separate Key Stores: Never pass an asymmetric Public Key into a function configured to accept symmetric (`HS256`) algorithms. Maintain isolated verification pipelines for different token authorities.
Error #7: JWKS Key Rotation & 'kid' Mismatch
When diagnosing intermittent authentication failures across distributed microservice clusters, one of the most baffling symptoms is an API suddenly rejecting valid tokens issued by Auth0, Okta, or AWS Cognito with the exact error: `Error: unable to find key with kid
Why 'kid' Mismatch Occurs During Key Rotation
To maintain long-term cryptographic resilience, enterprise identity providers regularly rotate their RSA signing keys (often every 90 days or on-demand during security audits). Every JWT header includes a `kid` (Key ID) property identifying exactly which public key inside the JSON Web Key Set (`/.well-known/jwks.json`) was used to generate that specific signature:
{
"alg": "RS256",
"typ": "JWT",
"kid": "NjVBRjY5MDlCMUIwNzU4RTA2QzZFMDQ4QzQ2MDA2QzU="
}
When your backend verifies a token, it checks its local in-memory JWKS cache for the matching `kid`. If the identity provider just rotated keys, newly issued tokens will arrive with a brand-new `kid` that your backend cache hasn't downloaded yet. If your caching middleware is configured with static TTLs without dynamic cache-invalidation triggers, every single user presenting a newly rotated token will experience 401 Unauthorized errors until the cache expires.
Diagnostic & Remediation Workflow
- Compare Token 'kid' against Live Endpoint: Decode the failing token's header using the ZeroData JWT Debugger and copy the `kid` string. Open your identity provider's live `https://your-domain.auth0.com/.well-known/jwks.json` URL in a browser or via `curl`. Verify whether that exact `kid` exists in the live `keys` array.
- Check Rate-Limit Throttling: If the `kid` exists on the live JWKS endpoint but your server still rejects it, check your server error logs for rate-limiting exceptions. If hundreds of concurrent requests arrive during a rotation event, your JWKS client (`jwks-rsa`) might attempt hundreds of simultaneous HTTP fetches, triggering rate limits on the identity provider and causing key lookups to fail.
Production-Hardened JWKS Caching Config: Always configure your JWKS middleware with `cache: true`, a reasonable `cacheMaxEntries` limit, and an explicit `jwksRequestsPerMinute` rate limiter to prevent denial-of-service loops during rotations:
const jwksRsa = require('jwks-rsa');
const jwksClient = jwksRsa({
cache: true, // Cache public keys in memory
cacheMaxEntries: 10, // Retain up to 10 historical keys during transition periods
cacheMaxAge: 24 * 60 * 60 * 1000, // 24 hours standard TTL
rateLimit: true, // Enable rate limiting to prevent flood loops
jwksRequestsPerMinute: 10, // Max 10 remote lookups per minute when cache misses occur
jwksUri: 'https://login.yourdomain.com/.well-known/jwks.json'
}); Error #8: Header Truncation, Whitespace & Bearer Prefix Errors
Often, tokens that decode perfectly inside local testing environments fail instantly when deployed behind production load balancers, API gateways (like Kong or AWS API Gateway), or Nginx reverse proxies. In these cases, the token itself is valid, but the HTTP transport layer mangles the payload before your application code ever sees it.
The Double-Prefix & Case-Sensitivity Trap
The RFC 6750 specification stipulates that OAuth 2.0 Access Tokens must be transmitted inside the `Authorization` header using the exact prefix `Bearer ` followed by a single space and the token string:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsIn... Common transport-level failures include:
- Case-Sensitivity Issues: Some reverse proxies normalize HTTP header keys to lowercase (`authorization`), while legacy backend frameworks strictly search for uppercase `Authorization`. Make sure your header extraction logic is case-insensitive.
- Double Bearer Prefixing: If an API client library automatically prepends `Bearer ` and a developer manually adds `Bearer ` when constructing the request (
headers: { Authorization: 'Bearer ' + token }), the header arrives as `Bearer Bearer eyJhbG...`. When the backend splits on whitespace, it attempts to parse the word `Bearer` as the JWT string, throwing an immediate `Malformed Token` exception. - Invisible Newlines and URL Encoding: When copying tokens from terminal logs or environment variable files (`.env`), developers frequently copy trailing carriage returns (`\r\n`) or tab characters. Furthermore, if tokens are passed inside URL query parameters (`?access_token=ey...`) without exact URL encoding (`encodeURIComponent`), plus signs (`+`) inside base64 strings get converted to spaces by web servers, instantly corrupting the cryptographic signature.
Production Hardening Checklist for Microservices
Before deploying JWT authentication workflows to enterprise production environments in 2026, verify that your backend architecture enforces every item in this hardening checklist:
| Security Vector | Required Enterprise Standard | Why It Is Critical |
|---|---|---|
| Algorithm Whitelisting | Explicitly set `algorithms: ['RS256']` or `['HS256']` in verification middleware. | Prevents `alg: none` and asymmetric-to-symmetric algorithm confusion attacks. |
| Audience & Issuer Checks | Enforce exact string matching on `aud` and `iss` claims on every endpoint. | Prevents tokens issued for one tenant or frontend app from accessing internal backend services. |
| Short Expiration Lifespans | Set `exp` between 5 to 15 minutes max for Access Tokens. | Minimizes the window of vulnerability if a bearer token is intercepted by an adversary. |
| Clock Skew Leeway | Configure 30 to 60 seconds of `clockTolerance` / `leeway`. | Prevents false-positive `Token Expired` rejections across distributed server instances with slight NTP drift. |
| Secure Storage (No LocalStorage) | Store Refresh Tokens strictly inside `HttpOnly, Secure, SameSite=Strict` cookies. | Eliminates Cross-Site Scripting (XSS) attacks from exfiltrating long-lived tokens from browser memory. |
| Zero Server Logging | Scrub `Authorization` headers and `bearer` tokens from all HTTP access logs (`nginx`, `winston`, `cloudwatch`). | Prevents log-aggregation services and internal employees from harvesting live production tokens. |
Frequently Asked Questions (FAQ)
- What does Invalid Signature JWT mean?
- An Invalid Signature JWT error occurs when the cryptographic signature of the token does not match the computed signature on the backend. This means either the secret/key is incorrect, the payload has been tampered with, or the wrong algorithm was used to verify it.
- How do I fix a Token Expired JWT error?
- A Token Expired JWT error means the current time is past the timestamp in the 'exp' claim. To fix this, your client application must detect the expiration and use a Refresh Token to obtain a new Access Token. Also, ensure your backend correctly handles small clock skews between servers.
- Why do I get Invalid Audience JWT?
- The Invalid Audience JWT error happens when the 'aud' (audience) claim in the token does not match the expected audience configured in your API. This often occurs when using identity providers like Auth0 or Cognito and requesting a token for the wrong resource server.
- How do I safely decode base64url JWTs?
- To decode base64url JWTs safely, split the token by dots and base64url-decode the middle part (payload). Always do this locally. Do not paste production tokens into random third-party sites. Tools like the ZeroData JWT Debugger perform this decoding entirely within your browser.
- Can I debug JWTs without exposing the secret?
- Yes. The payload of a JWT is not encrypted, only encoded. You can decode and inspect the claims (like exp, aud, iss) to debug configuration issues without ever needing the secret key or private key.
- Is it safe to use online JWT debuggers?
- It is highly unsafe to paste production JWTs into server-backed online debuggers, as they may log your tokens. Only use 100% client-side, browser-based tools (like ZeroData Tools) where the token never leaves your machine.
- Why is my exp claim causing immediate expiry?
- The JWT specification requires the 'exp' claim to be a Unix timestamp in seconds, not milliseconds. If you accidentally set it using JavaScript's Date.now() (which returns milliseconds), the token will be evaluated as expiring in the year 52,000+ but some strict libraries might fail parsing, or conversely, if you set it to a future millisecond date that translates to a past second date, it expires instantly.
- What is the difference between aud and iss claims?
- The 'iss' (issuer) claim identifies who created and signed the token (e.g., your Auth0 tenant). The 'aud' (audience) claim identifies the intended recipient of the token (e.g., your specific backend API). A token must have the correct issuer and audience to be accepted.
- How do I verify RS256 signatures?
- RS256 uses an asymmetric key pair. The token is signed by the identity provider using a Private Key, and your backend verifies the signature using a Public Key (often fetched dynamically from a JWKS endpoint).
- Why does my JWT have no signature part?
- If your JWT ends with a dot and has nothing after it, it was created using the 'none' algorithm. This is a severe security risk. You should configure your backend library to reject unsigned tokens and explicitly whitelist accepted algorithms like HS256 or RS256.
- What causes a malformed JWT error?
- A malformed JWT error occurs when the token string is not in the expected 'header.payload.signature' format. This usually happens if the token gets truncated in HTTP headers, has invisible whitespace, or was base64-encoded instead of base64url-encoded.
- How to handle clock skew in JWT verification?
- Distributed systems rarely have perfectly synchronized clocks. Most JWT libraries allow you to configure a 'clock skew' tolerance (e.g., 30-60 seconds). This prevents tokens from being rejected immediately due to slight time differences between the issuing and verifying servers.
- Should I log JWTs when debugging?
- Never log full JWTs in production server logs. They are bearer tokens, meaning anyone who accesses the logs can impersonate users. If you must log for debugging, log only the decoded payload claims (like user ID) and obscure the signature.