Token Revocation & Forced Logout Playbook
Overview
Token revocation is the process of invalidating JWT tokens before their natural expiration. This playbook documents when and how to force logout users by revoking their active tokens in StockEase Frontend.
When to Revoke Tokens
Critical Security Incidents
1. Suspected Account Compromise
Scenario: User's password is suspected compromised
ββ User reports unusual login activity
ββ Multiple failed login attempts detected
ββ Account shows activity from unexpected locations
ββ Action: Revoke all tokens immediately
Steps:
- Force user logout on all devices
- Invalidate all issued tokens
- Require password reset before re-login
- Send security alert to user email
2. Data Breach
Scenario: Application security incident with potential token exposure
ββ JWT tokens leaked in logs
ββ Database breach with token storage
ββ Client-side token exposure (XSS)
ββ Action: Revoke tokens for affected users
Steps:
- Identify affected users
- Batch revoke their tokens
- Force re-authentication
- Rotate signing keys (see key-rotation.md)
3. Unauthorized Access
Scenario: Unauthorized user actions detected
ββ Admin account used without authorization
ββ Unusual API calls from user token
ββ Permission escalation detected
ββ Action: Revoke suspect token immediately
Steps:
- Terminate active session
- Revoke all tokens for affected user
- Log security incident
- Notify user of suspicious activity
Planned Maintenance
4. Policy Changes
Scenario: Security policy updated requiring re-authentication
ββ MFA now required
ββ Permission levels changed
ββ Role assignments modified
ββ Action: Revoke tokens to enforce new policy
Steps:
- Notify users of upcoming change
- Set revocation date/time
- Batch revoke tokens
- Users automatically redirect to login
5. Account Deletion
Scenario: User account is deleted or suspended
ββ User requests account deletion
ββ Account suspended for policy violation
ββ Employee terminated (HR system)
ββ Action: Revoke all tokens immediately
Steps:
- Immediately terminate all sessions
- Revoke all issued tokens
- Delete or archive account data
- Update audit logs
Operational Changes
6. Role or Permission Changes
Scenario: User role or permissions are modified
ββ User promoted/demoted
ββ Permission level changed
ββ Department transfer
ββ Action: Revoke token to force re-authentication with new permissions
Steps:
- Update user role in database
- Revoke all tokens for user
- User forced to re-login
- New token issued with updated claims
Current Token Architecture
Token Storage in StockEase
Location:
localStorage.token
// From src/services/apiClient.ts
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}Token Lifecycle:
1. Login β Backend generates JWT β Frontend stores in localStorage
2. API Request β Request interceptor attaches Bearer token
3. Response β Response interceptor checks status
4. 401 Error β Response interceptor removes token (cleanup)
5. Token Expired β Frontend redirect to login
6. Logout β Frontend removes token from localStorage
Current Logout Implementation
File:
src/services/apiClient.ts
// Response interceptor
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}How it works:
- Backend returns 401 (Unauthorized)
- Frontend interceptor removes token
- User redirected to login page
- Effectively logs user out β
Limitation: This requires server to reject the token. If token is still valid in backend, user can reuse it if:
- Request doesn't trigger 401 response
- Token is cached in browser memory
- localStorage is not fully cleared
Forced Logout Implementation
Option 1: Server-Side Token Blacklist (Recommended)
Concept:
Backend maintains list of revoked tokens
Frontend presents token
Backend checks: "Is this token in revoked list?"
ββ Yes β Reject request (401)
ββ No β Accept request (200)
Implementation:
Backend (Node.js example):
// Redis cache for revoked tokens
const redis = require('redis');
const client = redis.createClient();
// On token revocation request
app.post('/auth/revoke', authMiddleware, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
// Add token to revoked list
// TTL = token expiration time (prevents memory leak)
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await client.setex(`revoked_${token}`, ttl, '1');
res.json({ success: true, message: 'Token revoked' });
});
// In JWT verification middleware
const verifyToken = async (token) => {
const isRevoked = await client.get(`revoked_${token}`);
if (isRevoked) {
throw new Error('Token has been revoked');
}
return jwt.verify(token, secret);
};Frontend (StockEase):
// src/services/apiClient.ts - Add revocation endpoint
export async function revokeToken(): Promise<void> {
try {
await axios.post('/auth/revoke', {}, {
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`
}
});
} finally {
// Clear local storage regardless of response
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('role');
window.location.href = '/login';
}
}
// Usage in logout
export function logout(): void {
revokeToken();
}Advantages:
- β Immediate invalidation
- β Works across all sessions
- β Prevents token reuse
- β Secure and reliable
Disadvantages:
- β οΈ Requires backend changes
- β οΈ Uses backend resources (Redis/cache)
- β οΈ Need to manage TTL expiration
Option 2: Distributed Cache Invalidation
Concept:
Multiple backend servers share revoked token list
ββ Server A revokes token
ββ Writes to distributed cache (Redis/Memcached)
ββ All servers check same cache
ββ Token invalidated everywhere
Implementation:
// Backend - Redis distributed cache
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
db: process.env.REDIS_DB
});
// Revoke token endpoint
app.post('/auth/revoke', authMiddleware, async (req, res) => {
const token = req.headers.authorization.split(' ')[1];
const decoded = jwt.decode(token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
// Publish revocation event to all servers
await client.publish('token_revoked', JSON.stringify({
token: token,
userId: decoded.sub,
timestamp: new Date().toISOString()
}));
// Store in revoked list
await client.setex(`revoked_${token}`, ttl, JSON.stringify({
userId: decoded.sub,
revokedAt: new Date().toISOString()
}));
res.json({ success: true, message: 'Token revoked' });
});Advantages:
- β Works across distributed systems
- β Real-time propagation
- β Scalable to many servers
Disadvantages:
- β οΈ Complex infrastructure
- β οΈ Requires Redis/Memcached setup
- β οΈ Network latency
Option 3: Client-Side Only (Current - Limited)
Concept:
Frontend removes token from localStorage
ββ Token invalidated in browser
ββ But remains valid on backend
ββ If leaked/cached, can still be used
Current Implementation:
// src/services/apiClient.ts
export function logout(): void {
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('role');
window.location.href = '/login';
}Limitations:
- β Token still valid on backend
- β Cached token can be reused
- β No protection against token theft
- β Not suitable for security incidents
Use Case:
- Normal user-initiated logout only
- Not for security incidents
Forced Logout Workflow
User-Initiated Logout (Current)
User clicks "Logout"
β
logout() called
β
Remove token from localStorage
Remove username from localStorage
Remove role from localStorage
β
Redirect to /login
β
User must re-login
File:
src/services/apiClient.ts
Current Code:
export function logout(): void {
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('role');
window.location.href = '/login';
}Admin-Initiated Revocation (Recommended)
Admin revokes user token
β
Backend receives revoke request
β
Add token to blacklist (Redis)
β
Publish revocation event
β
If user online:
ββ Next API call returns 401
ββ Frontend interceptor removes token
ββ User redirected to login
β
User must re-authenticate
Implementation Steps:
1. Backend Revocation Endpoint
POST /auth/revoke
Headers: { Authorization: 'Bearer <token>' }
Body: { userId?: '<user-id>' } // Optional: revoke other user
Response: { success: true, message: 'Token revoked' }2. Frontend Logout Component
// src/services/apiClient.ts
export async function revokeAndLogout(): Promise<void> {
try {
await axios.post('/auth/revoke', {});
} catch (error) {
console.error('Revocation failed:', error);
} finally {
// Clear local storage
localStorage.removeItem('token');
localStorage.removeItem('username');
localStorage.removeItem('role');
// Redirect to login
window.location.href = '/login';
}
}3. Component Usage
// In logout button handler
import { revokeAndLogout } from '../services/apiClient';
<button onClick={revokeAndLogout}>
Logout
</button>Batch Token Revocation
Scenario: Security Incident
Use Case: Revoke tokens for multiple users after data breach
Backend Implementation:
// POST /auth/revoke-batch (admin only)
app.post('/auth/revoke-batch', adminAuth, async (req, res) => {
const { userIds, reason } = req.body;
if (!userIds || !Array.isArray(userIds)) {
return res.status(400).json({ error: 'userIds must be array' });
}
try {
// Find all tokens for these users
const tokens = await Token.find({ userId: { $in: userIds } });
// Add to revocation list
for (const token of tokens) {
const decoded = jwt.decode(token.token);
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
await redis.setex(
`revoked_${token.token}`,
ttl,
JSON.stringify({
userId: token.userId,
reason: reason,
revokedAt: new Date().toISOString(),
revokedBy: req.user.id
})
);
}
// Log security incident
await AuditLog.create({
action: 'BATCH_TOKEN_REVOCATION',
userIds: userIds,
reason: reason,
count: tokens.length,
performedBy: req.user.id,
timestamp: new Date()
});
res.json({
success: true,
message: `Revoked ${tokens.length} tokens for ${userIds.length} users`,
tokensRevoked: tokens.length
});
} catch (error) {
console.error('Batch revocation error:', error);
res.status(500).json({ error: 'Batch revocation failed' });
}
});Frontend Admin Panel:
import axios from 'axios';
async function batchRevokeTokens(userIds: string[], reason: string): Promise<void> {
try {
const response = await axios.post('/auth/revoke-batch', {
userIds,
reason
});
console.log(`Revoked ${response.data.tokensRevoked} tokens`);
alert(`Successfully revoked tokens for ${userIds.length} users`);
} catch (error) {
console.error('Batch revocation failed:', error);
alert('Failed to revoke tokens');
}
}
// Usage
await batchRevokeTokens(
['user1', 'user2', 'user3'],
'Security incident - potential data breach'
);Monitoring & Verification
Verify Token Revocation
Check if token is revoked:
# Using curl
curl -X GET https://api.stockease.com/auth/verify \
-H "Authorization: Bearer <token>"
# If token is revoked:
# Response: 401 Unauthorized
# If token is valid:
# Response: 200 OK { "valid": true, "userId": "..." }Check revoked token count:
# Redis CLI
redis-cli keys "revoked_*" | wc -l
# Expected: Number of revoked tokens in cacheAudit Logging
Log all revocation events:
// After revocation
await logAuditEvent({
action: 'TOKEN_REVOKED',
timestamp: new Date(),
userId: revokedUserId,
initiator: initiatorId,
reason: revocationReason,
ipAddress: req.ip,
userAgent: req.headers['user-agent']
});Audit Trail Example:
2025-11-13T14:32:15Z | TOKEN_REVOKED | user123 | admin456 | Security incident | 192.168.1.100
2025-11-13T14:35:42Z | TOKEN_REVOKED | user456 | user456 | User logout | 10.0.0.50
2025-11-13T15:01:08Z | BATCH_TOKEN_REVOCATION | users: [u1,u2,u3] | admin789 | Data breach response | 203.0.113.42
Checklist for Token Revocation
β Before Implementing
β During Implementation
β After Implementation
Common Issues & Solutions
Issue 1: Token Still Works After Revocation
Problem: User is revoked but can still access API
Cause: Backend not checking revocation list
Solution:
// Ensure middleware checks revocation
const verifyToken = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
// Check if revoked FIRST
const isRevoked = await redis.get(`revoked_${token}`);
if (isRevoked) {
return res.status(401).json({ error: 'Token has been revoked' });
}
// Then verify signature
const decoded = jwt.verify(token, SECRET);
req.user = decoded;
next();
};Issue 2: Redis Not Connected
Problem: Revocation endpoint fails, tokens not actually revoked
Cause: Redis connection issue
Solution:
// Add retry logic
const redis = require('redis');
const client = redis.createClient({
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis connection failed');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted');
}
return Math.min(options.attempt * 100, 3000);
}
});
// Fallback: Use database instead of Redis
const revokeToken = async (token) => {
if (redisAvailable) {
await redis.setex(`revoked_${token}`, ttl, '1');
} else {
await RevokedToken.create({ token, expiresAt });
}
};Issue 3: Revocation List Memory Leak
Problem: Redis keeps growing, memory runs out
Cause: Not setting TTL on revoked tokens
Solution:
// Always set TTL = token expiration time
const decoded = jwt.decode(token);
const expirationTime = decoded.exp * 1000; // Convert to ms
const ttl = Math.ceil((expirationTime - Date.now()) / 1000); // Seconds
// Only cache until token would expire anyway
await redis.setex(`revoked_${token}`, ttl, '1');Related Documentation
- JWT Tokens: See JWT Token Handling
- Authentication: See Authentication Flow
- Key Rotation: See Key Rotation & Rollout
- API Security: See API Communication Security
Last Updated: November 13, 2025
Status: Recommended Implementation
Priority: High (Security Incident
Response)
Maintainer: Security Team