JWT Token Invalid or Expired” Errors — A Complete Guide for Node.js Authentication
If you’ve ever built a login system using JSON Web Tokens (JWT), you’ve probably seen errors like:
Users get logged out randomly.
Protected routes fail.
Mobile clients complain that they “keep getting kicked out.”
And you start to wonder…
“Is JWT even reliable?”
Yes — it is. But most JWT errors come from a few common mistakes.
Let’s fix them — cleanly and properly.
How JWT Authentication Actually Works (Simple Explanation)
Login flow:
-
User logs in
-
Server generates access token
-
Token is returned to client
-
Client includes token in every request
-
Server verifies token
-
Access granted
JWT is stateless — meaning the server doesn’t store sessions.
The token itself contains the data.
Example payload:
The expiration time is inside the token.
When expired — the browser cannot refresh it magically.
Most Common JWT Problems (And Real Fixes)
✅ 1. Tokens Expire Too Fast
Example:
Users will constantly get logged out.
Fix — use refresh tokens.
Access token lifetime:
Refresh token lifetime:
Why?
-
Short token = safer if stolen
-
Long refresh = better UX
✅ 2. Using the Wrong Secret Key
If your login server uses one key
and verification uses another…
JWT will always be invalid.
Make sure:
And NEVER commit it to GitHub.
✅ 3. Clock Skew Issues
If server time is off,
tokens may appear expired.
You’ll see users say:
“I just logged in – why am I expired?”
Fix:
Sync server time (NTP)
or add small tolerance.
✅ 4. Token Not Sent in the Right Place
Correct way (HTTP header):
Not query strings
Not body
Not cookies (unless secure HTTP-only)
✅ 5. Token Changed by Formatting Bugs
Sometimes the frontend trims or corrupts tokens.
Example mistake:
This adds quotes — breaking the token.
Implementing JWT Correctly in Node.js (Best Practice Flow)
Login — Generate Access + Refresh Token
Store refresh token in database (or Redis).
Middleware — Verify Access Token
If failed → return 401 or 403, not 500.
Refresh Endpoint — Issue New Access Token
When access token expires,
the frontend calls /refresh.
This way users stay logged in smoothly.
Security Best Practices (Very Important)
Never:
❌ store JWT in localStorage for banking-level apps
❌ allow refresh tokens without rotation
❌ use same secret for both tokens
❌ set token expiry to “forever”
Better:
✔ HTTP-only cookies when possible
✔ rotate refresh tokens
✔ revoke refresh tokens on logout
✔ use HTTPS always
JWT + HTTPS = safe
JWT + HTTP = disaster waiting
Debug Checklist — If JWT Still Fails
Ask yourself:
✔ Is token expired?
✔ Is signature key identical?
✔ Is Authorization header correct?
✔ Is token modified by frontend?
✔ Is time synced on server?
✔ Are tokens from same environment?
90% of bugs come from one of these.
Final Thoughts
JWT isn’t the problem.
Implementation usually is.
Once you separate:
🔐 short-life access tokens
🔄 long-life refresh tokens
…and validate them correctly,
authentication becomes stable and reliable.