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:

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:

Assumptions

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

  1. User logs in and receives an access token (short TTL) and a refresh token (longer TTL). Session metadata is recorded in a session store.

  2. API calls use access token; minimal or no server lookup for every request (verify signature + claims).

  3. 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).

  4. 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

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:

Refresh token contents options

Rotation flow

  1. Client posts refresh token to /token/refresh.

  2. Server hashes it, compares to stored hash in session record. If mismatch or session revoked -> reject.

  3. If valid, create new refresh token (random), store new hash, update session LastSeenAt, issue new access token and refresh token.

  4. 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

  1. Refresh-only revocation (recommended default)

    • Only refresh tokens are checked server-side; access tokens are stateless and live out their TTL. Revocation prevents future refreshes and forces clients to log in again after their short-lived access token expires. This is simple and performant.

  2. Active access-token revocation

    • Keep a revocation list (Redis set of jti for access tokens). On every API request, check jti against Redis. This gives immediate revocation but adds a fast lookup per request. Use for high-security apps.

  3. Hybrid / grace-check

    • Check access-token revocation only for sensitive endpoints. Or use a sliding window: check every N requests or when token age crosses threshold.

  4. Push-based invalidation

    • When session is revoked, publish an event to gateway instances so they can invalidate local caches. Useful when gateways cache revocation states.

Tradeoffs

Implementing in .NET (ASP.NET Core)

Key pieces

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)

UX tips

Scaling and performance

Audit, monitoring and alerting

Security hardening and best practices

  1. Use HTTPS only. Secure cookie or secure local storage for refresh tokens. Prefer HttpOnly Secure cookies for refresh tokens to reduce XSS risk.

  2. 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).

  3. Rotate refresh tokens on use; revoke old one immediately.

  4. Detect token reuse: if an old refresh token is used after rotation, mark session compromised and revoke.

  5. Limit concurrent sessions per user or allow configurable max sessions.

  6. Shorten access token lifetime to reduce window of abuse.

  7. Protect revocation store: Redis should be in private network with ACLs.

  8. Rate limit refresh endpoint to prevent brute-force.

  9. Use asymmetric signing (RSA/ECDSA) for access tokens so resource servers can verify without sharing symmetric secret. Rotate keys carefully and publish JWKS endpoints.

  10. Store only hashed refresh tokens and avoid storing plaintext tokens.

Test strategy

Common pitfalls

Conclusion and next steps

A Token Revocation + Session Tracking Engine is essential for secure, resilient authentication. The recommended practical approach is: