JWT and Refresh Tokens: What I Actually Learned
After implementing auth at Mapersive, here is how I now think about token lifecycles, rotation, and secure session management.
Before I Built It
I had used JWTs before — mostly by following tutorials that ended at "store it in localStorage, send it in the Authorization header." That gets you a working demo. It does not get you a secure system.
Building auth from scratch at Mapersive forced me to think through the full lifecycle.
The Basic JWT Flow
A JWT is a signed token containing claims — user ID, role, expiry. The server signs it; the client stores it and sends it back on every request.
The problem: JWTs cannot be invalidated before expiry. If a token is stolen, the attacker has access until the token expires.
Why Refresh Tokens
The solution is to use two tokens:
- Access token — short-lived (15 minutes). Sent with every API request.
- Refresh token — long-lived (7 days). Sent only to get a new access token pair.
This limits the damage window for a stolen access token while keeping the user experience smooth.
Refresh Token Rotation
Rotation means: every time you use a refresh token, you get a new one and the old one is invalidated.
In Django with simplejwt:
SIMPLE_JWT = {
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
}
Logout and Blacklisting
JWTs are stateless, but logout requires state. A blacklist table of invalidated refresh token JTIs solves this.
Token Storage
The most debated part:
- localStorage: Vulnerable to XSS
- httpOnly cookie: Not accessible to JavaScript, but introduces CSRF considerations
- Memory: Safest against XSS, but lost on page refresh
I used httpOnly cookie for the refresh token and memory for the access token.
What I Would Tell My Past Self
- Short-lived access tokens are not optional if you care about security
- Rotation is not extra complexity — it is the minimum for a responsible implementation
- Token storage decisions are tradeoffs, not best practices