JWT Refresh Tokens — Long-Lived Credentials for Seamless Session Renewal
In this tutorial, you will learn about JWT Refresh Tokens. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT refresh tokens are long-lived credentials issued alongside access tokens, enabling clients to obtain new access tokens without requiring the user to re-authenticate.
What You'll Learn
How refresh tokens complement access tokens, refresh token rotation, secure storage strategies, and handling refresh token revocation.
Why It Matters
Short-lived access tokens (15 minutes) are secure but require frequent renewal. Refresh tokens solve this by providing a long-lived credential that lives on the device and silently obtains new access tokens.
Real-World Use
Auth0 uses refresh token rotation for mobile apps. Google issues refresh tokens that last until revoked. Durga Antivirus Pro uses 7-day refresh tokens with rotation for its partner API.
sequenceDiagram
participant Client
participant Auth as Auth Server
participant API as Resource Server
Client->>Auth: Login
Auth->>Client: Access Token (15m) + Refresh Token (7d)
Client->>API: Request + Access Token
API->>Client: 401 Expired
Client->>Auth: Refresh + Refresh Token
Auth->>Client: New Access Token + New Refresh Token
Client->>API: Request + New Access Token
API->>Client: 200 OK
Refresh Token Rotation
With rotation, each refresh request returns both a new access token AND a new refresh token, invalidating the previous refresh token. This prevents a stolen refresh token from being reused.
| Event | Valid Refresh Tokens |
|---|---|
| Initial login | RT-1 |
| First refresh | RT-2 (RT-1 invalidated) |
| Attacker tries RT-1 | Rejected (already used) |
| Legitimate refresh with RT-2 | RT-3 issued (RT-2 invalidated) |
Code Example: Refresh Token with Rotation
import jwt, secrets, datetime, hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
SECRET = "your-secret"
# Store: refresh_token_hash -> token_data
refresh_store = {}
@app.route("/api/auth/login", methods=["POST"])
def login():
# Validate credentials (simplified)
data = request.get_json()
if data.get("password") != "correct":
return jsonify({"error": "Invalid credentials"}), 401
# Generate access token
access_token = jwt.encode({
"token_type": "access",
"sub": data["username"],
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
# Generate refresh token
refresh_token = secrets.token_urlsafe(32)
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
refresh_store[token_hash] = {
"user": data["username"],
"family": secrets.token_hex(16),
"active": True
}
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"expires_in": 900
})
@app.route("/api/auth/refresh", methods=["POST"])
def refresh():
data = request.get_json()
old_refresh = data.get("refresh_token")
if not old_refresh:
return jsonify({"error": "Refresh token required"}), 400
old_hash = hashlib.sha256(old_refresh.encode()).hexdigest()
stored = refresh_store.get(old_hash)
if not stored or not stored["active"]:
# Possible token theft — revoke all tokens in family
revoke_family(stored.get("family") if stored else None)
return jsonify({"error": "Refresh token revoked"}), 401
# Rotate: invalidate old, issue new
stored["active"] = False
new_access = jwt.encode({
"token_type": "access",
"sub": stored["user"],
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
}, SECRET, algorithm="HS256")
new_refresh = secrets.token_urlsafe(32)
new_hash = hashlib.sha256(new_refresh.encode()).hexdigest()
refresh_store[new_hash] = {
"user": stored["user"],
"family": stored["family"],
"active": True
}
return jsonify({
"access_token": new_access,
"refresh_token": new_refresh,
"expires_in": 900
})
def revoke_family(family_id):
"""Revoke all refresh tokens in a family (defense against theft)."""
for key, data in list(refresh_store.items()):
if data.get("family") == family_id:
data["active"] = False
Common Mistakes
1. Not Rotating Refresh Tokens
Without rotation, a stolen refresh token works until it expires. Rotation limits the window of opportunity.
2. Storing Refresh Tokens in localStorage
XSS Attacks can read localStorage. Use httpOnly cookies or secure device storage.
3. Making Refresh Tokens Permanent
Refresh tokens must expire. 7-30 days is standard. Permanent refresh tokens cannot be revoked if compromised.
4. Not Detecting Token Theft
When an already-rotated refresh token is presented, the server should revoke all tokens in that family (token theft detection).
5. Sending Refresh Tokens with API Requests
Refresh tokens are only for the refresh endpoint. Never send them to regular API endpoints.
Practice Questions
- Why are refresh tokens needed if we have access tokens?
- What is refresh token rotation and why is it important?
- How should refresh tokens be stored on the client?
- What happens when a refresh token expires?
- How do you detect refresh token theft?
Answers:
- Access tokens are short-lived (15 min) for security. Refresh tokens provide long-lived sessions without frequent logins.
- Each refresh returns a new refresh token, invalidating the old one. A stolen refresh token can only be used once.
- httpOnly cookies (web) or secure device storage/Keychain/Keystore (mobile). Never localStorage.
- The server returns 401 on refresh. The user must re-authenticate to obtain a new refresh token.
- When an already-rotated (inactive) refresh token is presented, revoke all tokens in the same family and alert the user.
Challenge: Implement refresh token rotation with family tracking and automatic theft detection. When an old refresh token is presented after rotation, revoke all tokens for that family.
FAQ
Mini Project
Build a Flask refresh token service with rotation: login issues access + refresh tokens, refresh endpoint rotates both, and automatic detection and response to token theft.
What's Next
Now learn about JWT Token Expiry — how expiry times work and strategies for handling them.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro