Appearance
Session Limiting
Section: 14 — Auth
Last Updated: 2026-05-30
Scope: Redis-backed session tracking, single-session enforcement, configuration
Overview
Microtec ERP enforces session limits to prevent credential sharing and unauthorized concurrent access. The session limiting system is Redis-backed and integrated into both the Keycloak token validation pipeline and the ERP Auth Service.
Default: 1 active session per user globally.
How It Works
Key behaviors:
- Eviction policy: The oldest session is evicted — not the current one. The newest login always wins.
- Scope: Per-user (all companies/branches share the same session slot)
- Storage: Redis sorted set, scored by
iat(issued-at timestamp)
Redis Data Model
# Active sessions registry (sorted set, score = issued_at)
ZADD session:active:{userId} {iat} {jti}
# Revocation list (for blacklisted tokens)
SET session:revoked:{jti} 1 EX {ttl_seconds}
# Session metadata
HSET session:meta:{jti} userId {userId} companyId {companyId} branchId {branchId} issuedAt {iat}Example Redis State
bash
# User has 1 active session
127.0.0.1:6379> ZRANGE session:active:user-uuid 0 -1 WITHSCORES
1) "jti-abc123"
2) "1748560000"
# After second login (old session evicted, new stored)
127.0.0.1:6379> ZRANGE session:active:user-uuid 0 -1 WITHSCORES
1) "jti-xyz789"
2) "1748561800"
# Old token blacklisted
127.0.0.1:6379> GET session:revoked:jti-abc123
"1"Configuration
appsettings.json
json
{
"SessionValidation": {
"EnforceSingleSession": true,
"GlobalDefaultMaxSessions": 1,
"EnableTokenIntrospection": true,
"CacheDurationSeconds": 60,
"EvictionPolicy": "OldestFirst"
}
}Configuration Reference
| Key | Type | Default | Description |
|---|---|---|---|
EnforceSingleSession | bool | true | Enable/disable session limiting |
GlobalDefaultMaxSessions | int | 1 | Max concurrent sessions per user |
EnableTokenIntrospection | bool | true | Validate tokens against Redis on each request |
CacheDurationSeconds | int | 60 | How long to cache a valid introspection result |
EvictionPolicy | string | OldestFirst | Which session to evict: OldestFirst or NewestFirst |
NOTE
CacheDurationSeconds: 60 means after a session is revoked, old tokens remain valid for up to 60 more seconds (cache window). Set to 0 for immediate enforcement at the cost of Redis load.
Per-Realm / Per-Client Override
The global default can be overridden per realm or per client for special use cases (e.g., service accounts, mobile apps that may have multiple devices):
json
{
"SessionValidation": {
"GlobalDefaultMaxSessions": 1,
"RealmOverrides": {
"businessowner": {
"MaxSessions": 5
}
},
"ClientOverrides": {
"erp-mobile": {
"MaxSessions": 3
},
"erp-service-account": {
"MaxSessions": 0
}
}
}
}| Client | Max Sessions | Rationale |
|---|---|---|
erp-angular (default) | 1 | Prevent credential sharing |
erp-mobile | 3 | User may have phone + tablet |
businessowner portal | 5 | Admins may use multiple browsers |
| Service accounts | 0 (unlimited) | M2M, no session limiting needed |
Token Introspection
When EnableTokenIntrospection: true, every API request triggers a lightweight Redis check:
This adds ~0.5–2ms per request (Redis latency). The 60-second cache prevents Redis from being a hot path on every request.
What Happens When a Session Is Evicted
When user logs in on a new device and the session limit is exceeded:
- Old session JTI is added to
session:revoked:{jti}in Redis - Old session entry is removed from
session:active:{userId}sorted set - The next request from the old session returns
401 Unauthorized - Frontend receives 401 and redirects to login page
- User sees: "Your session has been terminated because you logged in from another device"
Monitoring and Observability
Redis Key Patterns to Monitor
bash
# Count active sessions (all users)
redis-cli KEYS "session:active:*" | wc -l
# Count revoked tokens
redis-cli KEYS "session:revoked:*" | wc -l
# Check a specific user's sessions
redis-cli ZRANGE session:active:{userId} 0 -1 WITHSCORESMetrics (via Seq / OpenTelemetry)
| Metric | Description |
|---|---|
session.limit.evictions | Count of sessions evicted |
session.introspection.cache_hits | Redis cache hit ratio |
session.introspection.latency_ms | P95 introspection latency |
Troubleshooting
"401 immediately after login"
Cause: Redis eviction race condition — old session evicted before new token was returned to client.
Fix: Increase CacheDurationSeconds to 120. The issue typically occurs when GlobalDefaultMaxSessions: 1 and the frontend retries login multiple times quickly.
"Session not evicted — multiple concurrent logins working"
Causes:
EnforceSingleSession: falsein config- Redis unavailable (session limiting disabled on failure — fail-open for availability)
- Client override with
MaxSessions: 0
Check:
bash
# Verify config
grep -r "EnforceSingleSession" /app/appsettings*.json
# Check Redis connectivity
redis-cli -h <host> -p 10000 --tls PING"Logged out on all devices after deploying new config"
Cause: GlobalDefaultMaxSessions reduced — all existing sessions beyond the new limit were evicted.
Prevention: Deploy session limit changes during maintenance window. Consider a grace period migration.
Security Notes
| Scenario | Behavior |
|---|---|
| Redis unavailable | Fail-open: session limiting temporarily disabled. Tokens are still validated against Keycloak. |
| Token replay after revocation | Blocked within CacheDurationSeconds window (default 60s) |
| Concurrent logins from same IP | Still limited — session limiting is user-based, not IP-based |
| Service account tokens | Not subject to session limiting (MaxSessions: 0 for service clients) |