Skip to content

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

KeyTypeDefaultDescription
EnforceSingleSessionbooltrueEnable/disable session limiting
GlobalDefaultMaxSessionsint1Max concurrent sessions per user
EnableTokenIntrospectionbooltrueValidate tokens against Redis on each request
CacheDurationSecondsint60How long to cache a valid introspection result
EvictionPolicystringOldestFirstWhich 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
      }
    }
  }
}
ClientMax SessionsRationale
erp-angular (default)1Prevent credential sharing
erp-mobile3User may have phone + tablet
businessowner portal5Admins may use multiple browsers
Service accounts0 (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:

  1. Old session JTI is added to session:revoked:{jti} in Redis
  2. Old session entry is removed from session:active:{userId} sorted set
  3. The next request from the old session returns 401 Unauthorized
  4. Frontend receives 401 and redirects to login page
  5. 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 WITHSCORES

Metrics (via Seq / OpenTelemetry)

MetricDescription
session.limit.evictionsCount of sessions evicted
session.introspection.cache_hitsRedis cache hit ratio
session.introspection.latency_msP95 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:

  1. EnforceSingleSession: false in config
  2. Redis unavailable (session limiting disabled on failure — fail-open for availability)
  3. 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

ScenarioBehavior
Redis unavailableFail-open: session limiting temporarily disabled. Tokens are still validated against Keycloak.
Token replay after revocationBlocked within CacheDurationSeconds window (default 60s)
Concurrent logins from same IPStill limited — session limiting is user-based, not IP-based
Service account tokensNot subject to session limiting (MaxSessions: 0 for service clients)

Internal Documentation — Microtec Platform Team