JWT Blacklist — Revoking JWTs Before Expiration with Server-Side Blocklists
In this tutorial, you will learn about JWT Blacklist. We cover key concepts, practical examples, and best practices to help you master this topic.
JWT blacklisting is a technique for revoking JWTs before their natural expiry by maintaining a server-side list of revoked token IDs.
What You'll Learn
How JWT blacklisting works, when it is appropriate, implementing with Redis, and the trade-offs between blacklisting and short TTL.
Why It Matters
Stateless JWTs cannot be revoked — once issued, they are valid until expiry. For immediate revocation (user logs out, account suspended), blacklisting provides a mechanism. Without it, a stolen JWT is usable until it expires.
Real-World Use
Auth0 uses blacklisting for immediate token revocation. Keycloak maintains a token blacklist. Durga Antivirus Pro uses Redis-based blacklisting so security analysts can immediately revoke access when a partner integration is compromised.
flowchart LR
A["Request + JWT"] --> B["Server"]
B --> C{"In blacklist?"}
C -->|"Yes"| D["401 Unauthorized"]
C -->|"No"| E{"Signature valid?"}
E -->|"Yes"| F["Process Request"]
E -->|"No"| D
style A fill:#dbeafe,stroke:#2563eb
style B fill:#fef3c7,stroke:#d97706
style D fill:#fecaca,stroke:#dc2626
style F fill:#dcfce7,stroke:#16a34a
How Blacklisting Works
- Each JWT includes a unique
jti(JWT ID) claim - When a token needs revocation, its
jtiis added to a blocklist - On each request, the server checks if the
jtiis in the blocklist - Blocklist entries have TTL matching the token's remaining lifetime
Code Example: Redis-Based Blacklist
import redis
import jwt
import datetime
import uuid
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
SECRET = "your-secret"
def issue_token(user_id):
"""Issue a JWT with unique jti."""
jti = str(uuid.uuid4())
payload = {
"sub": user_id,
"jti": jti,
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1),
"iat": datetime.datetime.utcnow()
}
token = jwt.encode(payload, SECRET, algorithm="HS256")
return token, jti
def revoke_token(jti, ttl_seconds=3600):
"""Add token to blacklist with TTL matching remaining lifetime."""
r.setex(f"blacklist:{jti}", ttl_seconds, "revoked")
def is_revoked(jti):
"""Check if token is blacklisted."""
return r.exists(f"blacklist:{jti}") > 0
# Middleware
@require_auth
def protected_endpoint():
auth_header = request.headers.get("Authorization", "")
token = auth_header[7:]
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
jti = payload.get("jti")
if not jti or is_revoked(jti):
return jsonify({"error": "Token revoked"}), 401
g.user = payload
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return endpoint_function()
# Logout endpoint
@app.route("/api/auth/logout", methods=["POST"])
def logout():
auth_header = request.headers.get("Authorization", "")
token = auth_header[7:]
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
jti = payload.get("jti")
# Calculate remaining TTL
exp = payload.get("exp")
remaining = max(0, exp - datetime.datetime.utcnow().timestamp())
revoke_token(jti, int(remaining))
except Exception:
pass
return jsonify({"message": "Logged out"})
Blacklist vs Short TTL
| Approach | Pros | Cons |
|---|---|---|
| Short TTL + Refresh | No server state, scales infinitely | Slight delay before revocation |
| Blacklist | Immediate revocation | Server state, Redis needed |
| Hybrid | Balance of both | More complex |
Common Mistakes
1. Blacklisting Without TTL
If you never remove entries from the blacklist, it grows indefinitely. Always set TTL matching the token's remaining lifetime.
2. Not Including jti in Tokens
Without a unique identifier, you cannot blacklist individual tokens. Always issue each token with a unique jti.
3. Blacklisting Expired Tokens
Tokens past their exp are already invalid. Check expiry before checking the blacklist to save Redis lookups.
4. Relying Only on Blacklisting
Blacklisting adds server state, losing the stateless benefit of JWT. Use short TTL as primary and blacklisting as secondary.
5. Not Handling Redis Failures
If Redis is down, all tokens appear invalid (if checking) or valid (if not checking). Implement graceful degradation — allow valid tokens if Redis is unavailable.
Practice Questions
- What JWT claim is used for blacklisting?
- Why should blacklist entries have a TTL?
- When is blacklisting necessary despite short TTL?
- How does blacklisting affect JWT statelessness?
- What happens if Redis fails in a blacklist-based system?
Answers:
- The
jti(JWT ID) claim — a unique identifier for each token. - To prevent the blacklist from growing indefinitely. Once the token's natural expiry passes, the blacklist entry is no longer needed.
- For logout, immediate account suspension, or when a token is known to be compromised before its natural expiry.
- Blacklisting reintroduces server state, reducing (but not eliminating) the scaling benefits of stateless JWT.
- Implement fail-open (allow requests) or fail-closed (deny all) based on your security requirements. Fail-closed is safer.
Challenge: Build a blacklist system with Redis that supports immediate revocation, automatic TTL cleanup, and graceful degradation when Redis is unavailable.
FAQ
Mini Project
Build a Flask API with JWT blacklisting using Redis. Implement login (issues JWT with jti), logout (adds jti to blacklist), and protected endpoints that check the blacklist before processing.
What's Next
Now learn about JKU and JWK — how to dynamically provide signing keys for JWT verification.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro