How to implement “one device at a time” login using Redis overwrite semantics—solving the JWT revocation problem without complex token blacklists.
🔐 The Problem: JWT’s Stateless Trade-off
The Stateless Advantage
JWT (JSON Web Token) is popular because it’s stateless:
- No server-side session storage needed
- Horizontally scalable—any server can validate the token
- Self-contained—user info embedded in the token
The Revocation Problem
But stateless comes with a critical downside:
JWT’s revocation problem is not about impossibility, but about the loss of statelessness once revocation is required.
You can revoke JWTs using blacklists, token versions, or jti claims with a store—but all of these reintroduce server-side state, defeating the original purpose.
Consequences of staying purely stateless:
- ❌ Can’t force logout a user
- ❌ Can’t invalidate stolen tokens immediately
- ❌ Can’t implement “one device at a time” restriction
Business Requirement: Account Security
Our system originally used pure stateless JWT—simple, scalable, and horizontally friendly. But then product requirements changed:
“We need to limit users to one active session at a time to prevent account sharing.”
For high-security applications (trading, banking, subscriptions), you need:
- Single session enforcement — Only one device can be logged in at a time
- Device kick — New login automatically logs out the previous device
- Immediate revocation — Compromised accounts can be locked instantly
This meant we had to intentionally sacrifice statelessness to regain session control.
🔧 The Solution: Redis Overwrite Strategy
Core Concept
Instead of tracking all tokens (blacklist), we track only the current valid token per user:
|
|
When a new login occurs, we simply overwrite the old value. The previous token becomes invalid because it no longer matches what’s stored.
⚠️ This is a Hybrid Auth Model: JWT acts as a bearer credential, but Redis is the authoritative source of session validity—not the JWT itself. We intentionally reintroduce server-side state to regain control over sessions.
Data Model
|
|
💡 We store a hash of the token, not the token itself. This protects token confidentiality if Redis is read-compromised, but does not prevent session forgery if Redis integrity (write access) is compromised.
💻 Implementation
Login Flow
|
|
The Kick-out Flow
|
|
Auth Middleware
|
|
Explicit Logout
|
|
⚠️ Production Caveats
Token Refresh Handling
If you use refresh tokens, decide:
- Option A: Refresh token also gets single-session treatment
- Option B: Only access token is tracked; refresh creates new session
|
|
Redis Failure Mode
If Redis is down:
- Fail-open: Allow requests (security risk)
- Fail-closed: Deny all requests (availability risk)
|
|
Race Condition on Concurrent Login
If two devices login at the exact same millisecond:
|
|
Redis SET is atomic, so one will win. But both devices might briefly think they’re logged in until their next request.
This is usually acceptable for single-session enforcement, but if strict ordering matters, consider using WATCH/MULTI or Lua scripts.
📝 Summary
- JWT’s weakness — Can’t revoke tokens before expiration
- Redis overwrite — Store only the current valid token hash; new login overwrites old
- Device kick — Automatic; no explicit “logout other devices” needed
- Trade-off — Adds Redis dependency to auth flow; requires fail-mode decision
This pattern provides immediate session control without maintaining a growing blacklist of revoked tokens.