Skip to content

Session Management

Surge issues cookie-based sessions for browser-authenticated users and token-based sessions for programmatic access. Sessions have a configurable time-to-live and can be revoked individually or in bulk.

Session creation

Sessions are minted when a login flow completes successfully or when a service with direct_auth grant authenticates a user programmatically. The engine produces an IssuedSession containing both the internal session ID and the plaintext token — this plaintext is returned exactly once.

bash
# Browser: login flow completion sets session cookie automatically
# Programmatic: direct auth returns session token
curl -X POST http://localhost:3000/v1/authenticate/password \
  -H "Authorization: Bearer aeg_svc_..." \
  -H "Content-Type: application/json" \
  -d '{"username": "alice", "password": "correct-horse-battery-staple"}'
json
{
  "session": { "id": "018f9a1b-...", "token": "aeg_s_1a2b3c4d5e6f7g8h" }
}

If you lose the token, you cannot recover it — the database stores only the SHA-256 hash. Revoke the session and issue a new one.

Token format

Session tokens are generated by SessionToken::generate():

rust
// 128-bit random → base62-encoded → "aeg_s_" prefix → 22-char padded
let token = SessionToken::generate();
// Example: "aeg_s_1a2b3c4d5e6f7g8h9i0j"
PropertyValue
Entropy128 bits (cryptographic RNG)
EncodingBase62 (alphanumeric)
Prefixaeg_s_
Length22 characters (zero-padded)
StorageSHA-256 hash only

The aeg_s_ prefix helps identify session tokens in logs and traffic, distinct from service tokens (aeg_svc_) and flow IDs (aeg_f_).

For browser endpoints, Surge sets a session cookie on successful login flow completion. Cookie behavior is configured via environment variables:

SettingDefaultDescription
SURGE_COOKIE_DOMAIN.panit.devCookie domain (leading dot = subdomains)
SURGE_SESSION_TTL_HOURS72Session time-to-live
bash
# Override domain for your deployment
export SURGE_COOKIE_DOMAIN=.example.com
export SURGE_SESSION_TTL_HOURS=24

The Set-Cookie header includes:

  • Domain: from SURGE_COOKIE_DOMAIN (shared across subdomains for SSO)
  • Path: / — valid for all routes under the auth mount
  • HttpOnly: true — inaccessible to JavaScript
  • SameSite: Lax — sent on top-level navigation, not cross-site subrequests
  • Secure: true (when deployed over HTTPS)

Session verification

Sessions are automatically extracted from the cookie on browser endpoints (/whoami, /logout). The incoming cookie value is hashed and compared against the database — the plaintext token is never stored.

Service-to-service (API)

Backend services verify sessions by sending the token via the verify endpoint:

bash
curl -X POST http://localhost:3000/v1/sessions/verify \
  -H "Authorization: Bearer aeg_svc_..." \
  -H "Content-Type: application/json" \
  -d '{"token": "aeg_s_1a2b3c4d5e6f7g8h"}'

Verification checks three conditions:

  1. revoked_at IS NULL — session hasn't been explicitly revoked
  2. expires_at > now() — session hasn't expired
  3. The identity is in Active state

If all three pass, the response includes the identity and session metadata. If any fail, the token is rejected.

json
{
  "identity": { "id": "018f9a1b-...", "username": "alice" },
  "session": { "id": "...", "expires_at": "2026-07-11T12:00:00Z" }
}

Session revocation

Single session

Revoke a specific session by its token. Only the SHA-256 hash is needed — you can revoke a session even if you only have the hashed token stored:

bash
# Revoke by token hash (service with revoke grant)
curl -X POST http://localhost:3000/v1/sessions/revoke \
  -H "Authorization: Bearer aeg_svc_..." \
  -H "Content-Type: application/json" \
  -d '{"token": "aeg_s_1a2b3c4d5e6f7g8h"}'

Bulk revocation

Revoke all sessions for a specific identity — useful for "log out everywhere" or when disabling an account:

bash
curl -X POST http://localhost:3000/v1/identities/{id}/revoke-sessions \
  -H "Authorization: Bearer aeg_svc_..."

This sets revoked_at on every active session belonging to that identity, immediately invalidating them.

Expiry and garbage collection

Session TTL

Sessions expire after the configured TTL (SURGE_SESSION_TTL_HOURS, default 72 hours). Expiry is checked at verification time — an expired session is rejected the same as a revoked one.

rust
// Config is parsed from Duration (derived from SURGE_SESSION_TTL_HOURS)
let config = SessionConfig {
    session_ttl: Duration::hours(72),
};

Garbage collection

A background garbage collector periodically deletes sessions that are expired or revoked. This keeps the session table from growing unboundedly:

sql
-- What the GC effectively does:
DELETE FROM session
WHERE expires_at <= now() OR revoked_at IS NOT NULL;

GC runs on a configurable interval. Sessions that are still active remain in the table until their natural expiry or revocation.

Related: Login Flows, Password Authentication