← Back to Dossiers (Case 03)
2024–2025
Common Auth Service: Instant Revocation in Distributed Microservices
Decoupling roles from JWT payloads into versioned Redis claims; unified across Plasmon, Sapmapayı & Diventra WMS
<1ms permission check overhead · Sub-millisecond session termination
Stack: Spring Boot 3 · C# .NET 10 · Redis (Redlock) · Keycloak SSO · OAuth2 / OIDC · OpenAPI
1. Context
Across Diverjans products—Plasmon (IoT telemetry), Sapmapayı (compliance), and Diventra WMS (warehouse operations)—microservices run on heterogeneous stacks (.NET 10 and Spring Boot 3). A single unified identity and authorization plane was required to govern warehouse personnel, field technicians, and B2B integrations.
2. Problem
Standard stateless JWT architectures present an unacceptable security vulnerability in enterprise operations:
- When an operator is terminated or an edge device compromised, the existing signed JWT remains fully valid until its expiration (e.g., 60 minutes), allowing unauthorized access to critical machinery endpoints.
- Embedding deep granular role trees into the JWT inflated HTTP Authorization headers to 4KB–8KB, multiplying bandwidth costs across millions of sensor requests.
3. Architecture & Verification Flow
[ Client / Field Device / Web ]
│ Authorization: Bearer <jwt_id_token>
▼
┌────────────────────────────────────────────────────────┐
│ API Gateway / Downstream Microservice │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 1. Cryptographic Signature Verification (JWKS) │ │
│ │ -> Token not expired & signature valid? │ │
│ └─────────────────────────┬──────────────────────────┘ │
│ │ Valid
│ ▼
│ ┌────────────────────────────────────────────────────┐ │
│ │ 2. Redis Permission & Version Assertion │ │
│ │ GET user:perms:{user_id} │ │
│ │ -> User active? Permission version matches? │ │
│ └─────────────────────────┬──────────────────────────┘ │
└───────────────────────────┼────────────────────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
[ Claim Valid ] [ Session Revoked ]
Allow Request (<0.4ms) HTTP 401/403 Immediate Cut
4. Decisions & Rejected Alternatives
- Hybrid Model (Stateless JWT ID + Redis Claims) chosen, pure stateless JWT rejected: The JWT holds only immutable identity claims (
sub,iss,exp). Volatile roles (permissions: ["telemetry:write", "chiller:override"]) reside in Redis. Revoking access takes a single atomic key update, cutting off subsequent requests in<1ms. - Versioned Claims chosen, Token Blacklists rejected: Querying an ever-growing Redis set of revoked token IDs incurs ballooning memory overhead. Instead, each identity maintains an incrementing integer
token_version; bumping the version instantly invalidates all previously issued tokens. - Keycloak retained for SSO core: Federation, MFA, and OAuth2 identity management remain with Keycloak, while high-frequency authorization and instant revocation are delegated to localized Redis filter layers.
5. Production Code: Instant Revocation Filter
public class InstantRevocationAuthFilter : IAsyncActionFilter
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<InstantRevocationAuthFilter> _logger;
public InstantRevocationAuthFilter(IConnectionMultiplexer redis, ILogger<InstantRevocationAuthFilter> logger)
{
_redis = redis;
_logger = logger;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var user = context.HttpContext.User;
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var tokenVersionClaim = user.FindFirst("ver")?.Value;
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(tokenVersionClaim))
{
context.Result = new UnauthorizedResult();
return;
}
var db = _redis.GetDatabase();
// Read current token version from Redis in <0.3ms
var currentVersion = await db.StringGetAsync($"auth:ver:{userId}");
if (!currentVersion.HasValue || currentVersion.ToString() != tokenVersionClaim)
{
_logger.LogWarning("Session instantly revoked for User {UserId}", userId);
context.Result = new ObjectResult(new { error = "session_revoked" }) { StatusCode = 401 };
return;
}
await next();
}
}
6. Outcome & Measurable Results
- Revocation Delay: Dropped from 60 minutes to
<1 millisecondacross all microservices. - Header Payload: JWT footprint decreased from 5.2KB down to
<400 bytes, critical for low-bandwidth cellular field gateways. - Overhead: Added Redis check adds only
0.3ms – 0.5msper request. - Verified Scale: 15,000+ concurrent active sessions;
<0.4msRedis lookup latency.
7. What I Would Do Differently
- An in-memory local fallback cache (5s TTL) should have been included earlier to handle transient Redis cluster partitions gracefully, avoiding fail-closed rejection of legitimate requests during momentary network blips.