Modern web apps commonly use JWTs (JSON Web Tokens) for stateless authentication. JWTs are compact, verifiable, and excellent for horizontally scaled systems. But JWTs also bring operational challenges: once issued, a JWT remains valid until expiry — you cannot "take it back" easily. That becomes a problem when a user logs out, an admin must forcefully log out a compromised session, or a refresh token needs to be revoked.
A Token Revocation + Session Tracking Engine solves these problems. It gives you the ability to:
Track every authenticated session (who, when, where).
Revoke tokens immediately (logout everywhere, block stolen refresh tokens).
Support short-lived access tokens + long-lived refresh tokens safely.
Provide audit trails and admin controls to list and kill sessions.
Scale across services with minimal latency.
This article is a production-ready, senior-developer focused guide. It gives architecture, data models, flow diagrams, .NET implementation patterns (ASP.NET Core), optional Redis-backed revocation, refresh-token lifecycle, token introspection, Angular UI for session management, monitoring and best practices.
Goals and assumptions
Goals for this design:
Allow immediate session invalidation (admin/user-initiated).
Avoid per-request DB lookup for access token validation where possible.
Keep access tokens short (recommended 1–15 minutes) and validate refresh tokens via server.
Provide a reliable audit log of sessions and revocations.
Be horizontally scalable and resilient.
Assumptions
You control the authentication server (not relying solely on opaque third-party tokens).
Tokens are signed (JWT with asymmetric keys or HMAC).
You can add a small, fast server-side check (Redis or DB) at refresh time or optionally at access-token validation time.
High-level architecture
+------------------+ +----------------------+
| Browser / App | <--------> | API Gateway / App |
+------------------+ Access +----------------------+
| | |
(1) Login | | | (4) Access with AccessToken (short-lived)
v v v
+----------------+ +----------------------+
| Auth Server | | Revocation Store |
| (Issue tokens) | | (Redis / DB cache) |
+----------------+ +----------------------+
| ^
(2) Issue: access + refresh (3) Refresh checks + revoke decisions
Key flows
User logs in and receives an access token (short TTL) and a refresh token (longer TTL). Session metadata is recorded in a session store.
API calls use access token; minimal or no server lookup for every request (verify signature + claims).
When access token expires, the client calls refresh endpoint with refresh token. The refresh endpoint verifies token and session state in revocation store; if valid, issues new access token (and optionally new refresh token).
To revoke a session immediately, mark session as revoked in revocation store. Refresh attempts fail, and depending on strategy you can also force API calls to be rejected (see strategies).
Workflow diagram
User -> Auth Server: Login -> Auth Server stores SessionRecord -> returns AccessToken & RefreshToken
Client -> API: call with AccessToken -> API validates signature and (optional) revocation check -> Serve resource
Client -> Auth Server: Refresh with RefreshToken -> Auth Server checks SessionRecord & revocation -> issue new tokens -> update SessionRecord
Admin/User -> Auth Server: Revoke Session -> mark SessionRecord revoked -> optionally push revocation event to API Gateways or push cache invalidation
Flowchart: session lifecycle
Start
|
v
User authenticates -> create sessionId, save metadata
|
v
Issue accessToken (short), refreshToken (long, bound to sessionId)
|
v
Client uses accessToken for requests until expiry
|
v
If accessToken expired -> client calls /refresh with refreshToken
|
v
Validate refreshToken signature -> lookup SessionRecord -> isRevoked?
/ \
Yes No
| |
v v
Reject Issue new tokens (rotate refreshToken optionally)
Update SessionRecord
Session model and storage
Minimal session record:
Session
- SessionId (GUID) PRIMARY KEY
- UserId
- IssuedAt (UTC)
- LastSeenAt (UTC)
- ClientIp
- UserAgent
- DeviceInfo (optional)
- RefreshTokenId (GUID) or hashed token identifier
- RefreshTokenExpiresAt
- AccessTokenExpiresAt // optional
- IsRevoked (bool)
- RevokeReason (string)
- RevokedAt (datetime)
- Meta JSON (custom)
Storage options
Relational DB (Postgres/SQL Server): durable, good for audit. Use for long-term storage.
Redis (fast in-memory): best for revocation checks and TTL expiration. Use as the fast revocation store with authoritative DB as source of truth.
Hybrid: write-through to DB and set TTL keys in Redis for quick checks.
Important: store only token identifiers (e.g., refreshTokenId or jti) and never store raw refresh token values. Store a cryptographic hash of the refresh token (HMAC/SHA256) to compare on use.
Token design and rotation
Recommended approach:
Access token: short-lived (e.g., 5–15 minutes). Contains minimal claims: sub, exp, iat, jti, sid (session id). Signed with private key (RSA/ECDSA) or HMAC secret. Avoid embedding sensitive data.
Refresh token: longer (days/weeks). Stored as opaque string by client; server stores hashed token id and binds it to session record. Use refresh token rotation: every time a refresh is used, issue a new refresh token and invalidate the previous one (rotate and persist the new hashed id). This prevents replay attacks on stolen refresh tokens.
Refresh token contents options
Opaque random value (Base64(32 bytes)) with the server storing hashed value. Safer.
JWT-based refresh token is possible, but still treat the refresh token as an opaque secret because server must be able to revoke/track it.
Rotation flow
Client posts refresh token to /token/refresh.
Server hashes it, compares to stored hash in session record. If mismatch or session revoked -> reject.
If valid, create new refresh token (random), store new hash, update session LastSeenAt, issue new access token and refresh token.
Invalidate old refresh token (delete hash). If attacker replays old refresh token, reject and optionally revoke session.
Revocation strategies
There are design tradeoffs between immediate revocation and performance.
Options
Refresh-only revocation (recommended default)
Active access-token revocation
Hybrid / grace-check
Push-based invalidation
Tradeoffs
Per-request checks add latency but give immediate revocation. Redis lookup is fast (single GET), typically acceptable for most apps.
Refresh-only revocation is low-cost but requires short access token TTL to reduce window. Use it if performance is important and short TTL is acceptable.
Implementing in .NET (ASP.NET Core)
Key pieces
SessionService — creates, rotates, revokes sessions. Uses DB + Redis.
TokenService — issues and validates JWTs (access tokens) and creates refresh tokens.
RefreshController — /token/refresh endpoint.
RevocationStore — Redis cache storing revoked jti or session revocation flags.
JwtValidationMiddleware — custom middleware or use JwtBearer events to run revocation checks (if you choose active revocation).
Refresh token hashing (safe storage)
public string HashToken(string token)
{
using var sha256 = SHA256.Create();
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
return Convert.ToBase64String(hash);
}
Store only the hashed value.
Issuing tokens (simplified)
public (string accessToken, string refreshToken) IssueTokens(Guid userId, Guid sessionId)
{
var now = DateTime.UtcNow;
var accessClaims = new [] {
new Claim(JwtRegisteredClaimNames.Sub, userId.ToString()),
new Claim("sid", sessionId.ToString()),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var accessToken = _jwtFactory.CreateToken(accessClaims, expiresInMinutes: 10);
var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
var refreshHash = HashToken(refreshToken);
_sessionRepo.SaveRefreshToken(sessionId, refreshHash, DateTime.UtcNow.AddDays(14));
return (accessToken, refreshToken);
}
Refresh endpoint
[HttpPost("token/refresh")]
public async Task<IActionResult> Refresh([FromBody] RefreshRequest req)
{
var refreshHash = HashToken(req.RefreshToken);
var session = await _sessionRepo.GetByRefreshHashAsync(refreshHash);
if (session == null || session.IsRevoked || session.RefreshTokenExpiresAt < DateTime.UtcNow)
return Unauthorized();
// rotate
var newRefresh = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
var newHash = HashToken(newRefresh);
session.RefreshTokenHash = newHash;
session.RefreshTokenExpiresAt = DateTime.UtcNow.AddDays(14);
session.LastSeenAt = DateTime.UtcNow;
await _sessionRepo.UpdateAsync(session);
var access = _jwtFactory.CreateToken(...); // new access token
return Ok(new { accessToken = access, refreshToken = newRefresh });
}
Revocation API
[HttpPost("sessions/{sessionId}/revoke")]
public async Task<IActionResult> Revoke(Guid sessionId, [FromBody] RevokeRequest r)
{
var session = await _sessionRepo.GetById(sessionId);
if (session == null) return NotFound();
session.IsRevoked = true;
session.RevokedAt = DateTime.UtcNow;
session.RevokeReason = r.Reason;
await _sessionRepo.UpdateAsync(session);
// push quick invalidation
await _revocationStore.MarkSessionRevoked(sessionId, ttl: TimeSpan.FromDays(14));
return Ok();
}
MarkSessionRevoked writes a Redis key like revoked:sid:{sessionId} = 1 with TTL matching token rotation expectations.
JWT validation with revocation check (optional)
If you want immediate revocation for access tokens:
options.Events = new JwtBearerEvents
{
OnTokenValidated = async ctx => {
var sid = ctx.Principal.FindFirst("sid")?.Value;
if (!string.IsNullOrEmpty(sid))
{
var revoked = await _revocationStore.IsSessionRevoked(Guid.Parse(sid));
if (revoked)
{
ctx.Fail("session_revoked");
}
}
}
};
Angular UI: session listing and kill session
Provide a small admin/user UI to list active sessions and allow revocation.
SessionListComponent (sketch)
Fetch /api/sessions (paginated) — list sessions with device, ip, lastSeen, expiresAt.
Provide Logout button per session calling /sessions/{id}/revoke.
Show current session as highlighted.
UX tips
Show location (geo-IP) and device parsing from UserAgent.
Show last active and client application.
Warn user when revoking own session (do you want to log out now?).
Allow "revoke all other sessions" action.
Scaling and performance
Put short-lived keys in Redis with TTL — eviction happens automatically.
For huge scale, shard session data by user id to different Redis clusters.
Cache session status locally on API gateway with a small TTL (e.g., 30s) to reduce Redis hits but accept a small delay in revocation.
Use message bus (Kafka/RabbitMQ) for push invalidation to multiple gateway instances.
Audit, monitoring and alerting
Log these events: session_created, session_refreshed, session_revoked, refresh_failed_replay, token_rotation_failed. Include userId, sessionId, ip, userAgent, reason.
Monitor suspicious patterns: repeated refresh failures (possible stolen token), spike in session creations from same IP.
Alert on abnormal revocation rates.
Security hardening and best practices
Use HTTPS only. Secure cookie or secure local storage for refresh tokens. Prefer HttpOnly Secure cookies for refresh tokens to reduce XSS risk.
Bind refresh tokens to client fingerprint (optional): store minimal fingerprint (e.g., hashed UA + IP partial) and reject refresh from drastically different environment. Use with caution (mobile networks can change IP).
Rotate refresh tokens on use; revoke old one immediately.
Detect token reuse: if an old refresh token is used after rotation, mark session compromised and revoke.
Limit concurrent sessions per user or allow configurable max sessions.
Shorten access token lifetime to reduce window of abuse.
Protect revocation store: Redis should be in private network with ACLs.
Rate limit refresh endpoint to prevent brute-force.
Use asymmetric signing (RSA/ECDSA) for access tokens so resource servers can verify without sharing symmetric secret. Rotate keys carefully and publish JWKS endpoints.
Store only hashed refresh tokens and avoid storing plaintext tokens.
Test strategy
Unit tests for SessionService, refresh rotation, hashing, and revocation.
Integration tests that exercise login → refresh → rotate → replay old token detection.
End-to-end tests for admin revocation and immediate effect (depending on revocation strategy).
Security tests: attempt refresh with rotated token, attempt access with revoked session.
Common pitfalls
Long access token TTL without active revocation leads to long windows for abuse.
Storing raw refresh tokens in DB creates a risk if DB is compromised. Always store hash.
Not rotating refresh tokens allows replay attacks.
Using IP binding strictly on mobile clients causes legitimate failures.
Forgetting to expire Redis keys leads to stale revocation data.
Conclusion and next steps
A Token Revocation + Session Tracking Engine is essential for secure, resilient authentication. The recommended practical approach is:
Use short-lived access tokens and rotate refresh tokens.
Keep authoritative session records in DB and a fast revocation store in Redis.
Verify refresh tokens against hashed values; rotate on use.
For immediate revocation of access tokens, add a Redis-based revocation check on token validation or push invalidation events to edge gateways.
Provide an Angular UI for session visibility and admin controls.
Monitor and audit all session operations and token anomalies.