Core Docs

Revocation & Security

Revocation & Security



How tokens and sessions are revoked, how attacks are detected, and how core.auth responds to security events.

Revocation Strategies



core.auth implements multiple layers of revocation, each covering different scenarios:

StrategyMechanismLatencyUse Case






Short expiryJWT expires in 15 min15 min window if stolenDefault protection
Session deletionDelete/revoke session rowImmediate on next refreshLogout, admin action
BlacklistToken JTI in blacklist tableImmediate for advanced scopesMid-window revocation of sensitive ops
Explicit logoutUser logs out → session killedImmediateUser-initiated
Global kill"Log out everywhere" → all sessions killedImmediateEmergency / compromise

How They Layer



JWT stolen:
└── Attacker has up to 15 min window (short expiry)
├── No way to revoke mid-window for simple scopes
│ └── After expiry, token is worthless
└── For advanced scopes: blacklist check catches it
└── Attacker blocked even within the window

Session compromised:
└── Admin/user deletes session row
└── Next refresh fails → attacker locked out within 15 min max


Session Revocation



The primary revocation mechanism. Deleting a session row is the nuclear option for that session — the JWT becomes worthless as soon as it expires.

Single Session Logout



User clicks "Log out" in todo-app


App calls core.console: POST /v1/auth/session/logout
├── JWT access token (to identify the session via sid claim)
└── session_ref cookie (sent automatically)


core.auth:
├── Reads session ID from JWT payload (sid) or cookie
├── Revokes the session row (SET revoked_at = NOW(), revoke_reason = 'logout')
├── Clears session_ref cookie
├── Logs logout event
└── Returns success


App clears local JWT


User is logged out of todo-app
(User still has sessions on other apps)


Global Logout ("Log Out Everywhere")



User clicks "Log out everywhere" in account dashboard


Account dashboard calls: POST /session/logout-all
├── JWT access token
└── password (re-verification)


core.auth:
├── Revokes ALL sessions for this user
├── Logs global logout event
└── Returns success


All JWTs expire within 15 minutes
User must re-authenticate everywhere


Admin Revocation



An admin can revoke any user's sessions:

POST /admin/sessions/revoke
├── Admin JWT (with admin scope)
├── target_user_id
└── reason: "admin_action"


core.auth revokes all sessions for the target user


Blacklist (Advanced Scopes)



For advanced-scope operations, core.auth checks a token blacklist in addition to JWT signature and expiry. This handles the 15-minute window between token issuance and expiry for sensitive operations.

Table



CREATE TABLE token_blacklist (
id SERIAL PRIMARY KEY,
token_jti VARCHAR(64) UNIQUE NOT NULL, -- unique token identifier (sid from JWT)
user_id UUID NOT NULL REFERENCES users(id),
reason VARCHAR(50) NOT NULL, -- logout, compromise, admin
ip_address INET,
blacklisted_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL -- auto-cleanup after JWT TTL
);


When Tokens Are Blacklisted




TriggerReasonScope Affected




User logs outlogoutCurrent session's token
Admin actionadminAll user's tokens
User reports compromisecompromiseAll user's tokens

Performance Consideration



The blacklist is checked only for advanced and critical scopes, not for simple scopes. This keeps performance high for routine requests (profile reads, app access) while adding a security layer for sensitive operations (email changes, data exports).

Since JWTs are short-lived (15 min), blacklist entries only need to persist for the maximum token lifetime. A cleanup job removes expired entries:

DELETE FROM token_blacklist WHERE expires_at < NOW();


Blacklist Check Flow



App request with advanced scope


Verify JWT signature → valid


Check exp claim → not expired


Check blacklist: SELECT 1 FROM token_blacklist WHERE token_jti = ?

├── Not found → proceed (token is clean)
└── Found → 401 (token is blacklisted)


The token_jti used for the blacklist is the session ID (sid) from the JWT payload. This means blacklisting a session ID immediately invalidates the JWT for advanced/critical operations, even within its 15-minute window.

IP Logging



All authentication events are logged with IP address for audit and anomaly detection.

What Gets Logged




Eventevent_typeAdditional Data









Successful loginloginIP, user agent, device fingerprint, geo
Failed loginlogin_failedIP, user agent, reason (wrong password, 2fa fail)
Token refreshrefreshIP, user agent
LogoutlogoutIP, user agent
Password changepassword_changeIP, user agent
Email changeemail_changeIP, user agent, old + new email
2FA enabled/disabled2fa_changeIP, user agent
Session revokedsession_revokedIP, user agent, reason

How This Data Is Used



  • "Recent activity" view — users can see their login history in the account dashboard

  • "New device" alerts — email notification when a login happens from a new device or location

  • Anomaly detection — flag rapid IP changes, unusual geolocations, multiple failed logins

  • Security audit — investigate incidents by tracing IP/user agent history

  • Rate limiting — block IPs with excessive failed login attempts

Retention



ident_events accumulate fast. Retention policy:
  • Last 30 days — always available, shown in "recent activity"

  • 30–90 days — available for security audit, not shown in UI

  • 90+ days — pruned by cleanup job (configurable)

DELETE FROM ident_events
WHERE created_at < NOW() - INTERVAL '90 days'
AND event_type NOT IN ('login_failed');


Security-critical events (failed logins) may be retained longer.

Anomaly Detection on Refresh



Without refresh token rotation, stolen-token detection relies on signals at the refresh endpoint. The approach is conservative: log everything, act only on strong signals, never punish users for normal network behavior.

Users switch networks constantly — WiFi → 5G, home → office, VPN on/off, traveling. IP changes and geo jumps are normal. Revoking sessions on every network hop would make the app unusable.

What Gets Logged (Always)



Every refresh request logs: IP, user agent, geo (if available), timestamp. This data feeds the "recent activity" view and is available for security audits. It does not trigger automatic revocation.

What Triggers Action




SignalStrengthAction





User agent changedStrong — browser/OS doesn't change mid-sessionFlag, log, notify user
Rate limit exceededStrong — 10+ refreshes in 5 minTemporary IP block, log
IP changeWeak — normal behaviorLog only, no action
Geo jumpWeak — unreliable, VPNs, mobile networksLog only, no action

Why Only User Agent?



The user agent is the one thing that shouldn't change during a legitimate session. A user can switch from WiFi to 5G (IP change), travel between cities (geo jump), or turn on a VPN (both). But they won't spontaneously switch from Chrome on Windows to Safari on macOS.

A user agent mismatch on refresh is a strong signal that the session cookie has been stolen and is being used from a different device.

Response to User Agent Mismatch



POST /session/refresh


core.auth looks up session row


Session found — compare user agents:
├── Session origin: Chrome 125, Windows
├── Current request: Safari 19, macOS
└── MISMATCH


1. Log security event (ident_events, event_type: 'user_agent_mismatch')
2. Notify user via email: "New device detected on your session"
3. DO NOT revoke — let the user decide
(they may have legitimately switched devices)
4. Issue JWT as normal


The notification lets the user decide: "Was this you?" If not, they can revoke the session from the account dashboard. This avoids false positives (user switched browsers) while still alerting on genuine compromises.

Why Not Auto-Revoke?



Auto-revocation on anomaly sounds secure but causes real problems:
  • User switches from phone browser to phone app → user agent changes → locked out

  • VPN connects/disconnects → IP jumps → locked out

  • Mobile carrier changes IP range → geo jumps → locked out

The security gain from auto-revocation is marginal. The primary protection layers are:
  1. Short JWT TTL (15 min) — stolen token expires fast

  2. Rate limiting — brute-force blocked

  3. HttpOnly cookie — XSS can't steal the session ref

  4. Hard session cap (30 days) — abandoned sessions expire

  5. User notification — user can self-revoke if something looks wrong

Anomaly detection is an early warning system, not a kill switch.

Future: Adaptive Scoring



When the userbase grows, the anomaly detection can evolve into a risk scoring system:

risk_score = 0
risk_score += ip_changed ? 1 : 0
risk_score += geo_changed ? 1 : 0
risk_score += user_agent_changed ? 3 : 0
risk_score += rapid_refresh ? 2 : 0

if risk_score >= 5: require_re_auth()
elif risk_score >= 3: notify_user()
else: log_and_proceed()


This lets you combine multiple weak signals into a strong one without punishing any single normal behavior. But for now, user agent mismatch + rate limiting is sufficient.

Rate Limiting



core.auth applies rate limiting at multiple levels:


EndpointLimitWindowAction




/api/login5 attempts15 minLock account + notify
/session/refresh10 attempts5 minTemporary IP block
/api/verify-2fa3 attempts5 minLock 2FA + require recovery code

Rate limits are per-IP and per-user. Exceeding limits triggers:
  1. Temporary block (HTTP 429)

  2. Event logging

  3. User notification (for login attempts)




Next: SSO Flow — cross-app login and navigation.