Skip to content

Session Management ​

AuthHero uses a dual-session architecture with Login Sessions and Sessions to handle authentication flows and user sessions. Understanding how these work together is crucial for developers working with the AuthHero authentication system.

Overview ​

  • Login Sessions: Temporary sessions that track authentication flows (e.g., OAuth authorization, universal login)
  • Sessions: Long-lived user sessions that persist across multiple applications and login flows

Login Sessions ​

Login sessions are temporary, short-lived sessions that track a specific authentication flow from start to finish.

Purpose ​

  • Track OAuth authorization flows
  • Store authentication parameters (client_id, redirect_uri, scope, etc.)
  • Maintain state during multi-step authentication processes
  • Handle CSRF protection via state parameters

Lifecycle ​

  1. Created when an authentication flow starts (e.g., /authorize endpoint)
  2. Updated as the flow progresses (e.g., linking to user sessions)
  3. Completed when authentication succeeds and tokens are issued
  4. Expired after a short time (typically 5-10 minutes)

Key Properties ​

  • id: Unique identifier (used as OAuth state parameter)
  • authParams: OAuth parameters (client_id, redirect_uri, scope, etc.)
  • session_id: Reference to linked user session (optional)
  • expires_at: Short expiration time
  • csrf_token: CSRF protection

Example ​

typescript
// Created during /authorize
const loginSession = {
  id: "login_abc123",
  authParams: {
    client_id: "app123",
    redirect_uri: "https://app.com/callback",
    scope: "openid email profile",
  },
  session_id: null, // Initially not linked
  expires_at: "2025-09-04T12:05:00Z", // 5 minutes from now
  csrf_token: "csrf_xyz789",
};

Sessions ​

Sessions are long-lived user sessions that persist across multiple applications and authentication flows.

Purpose ​

  • Represent an authenticated user session
  • Enable Single Sign-On (SSO) across applications
  • Store session metadata (device info, last activity, etc.)
  • Support session management (logout, revocation)

Lifecycle ​

  1. Created when a user successfully authenticates
  2. Reused for subsequent authentication flows (SSO)
  3. Updated when linked to new login sessions or applications
  4. Expired after inactivity or explicit logout

Key Properties ​

  • id: Unique session identifier
  • user_id: Reference to the authenticated user
  • login_session_id: Reference to the login session that created this session
  • clients: List of applications that have used this session
  • expires_at: Long expiration time
  • device: Device and browser information

Example ​

typescript
// Created after successful authentication
const session = {
  id: "session_def456",
  user_id: "email|user123",
  login_session_id: "login_abc123",
  clients: ["app123"],
  expires_at: "2025-09-11T12:00:00Z", // 7 days from now
  device: {
    last_ip: "192.168.1.1",
    last_user_agent: "Mozilla/5.0...",
  },
};

Session Linking ​

The relationship between login sessions and sessions is dynamic and changes throughout authentication flows.

Initial Creation ​

When a user first authenticates:

  1. Login session is created for the OAuth flow
  2. User completes authentication (password, social, etc.)
  3. New session is created and linked to the login session
  4. Login session's session_id is set to the new session's id

Session Reuse (SSO) ​

When a user has an existing session:

  1. New login session is created for the OAuth flow
  2. System detects existing valid session (via cookie)
  3. Existing session is linked to the new login session
  4. No new session is created - the existing one is reused

Example Flow ​

First Login ​

1. GET /authorize → Creates login_session_1 (session_id: null)
2. User authenticates → Creates session_1 (login_session_id: login_session_1)
3. login_session_1 updated → (session_id: session_1)

Subsequent Login (SSO) ​

1. GET /authorize → Creates login_session_2 (session_id: null)
2. System detects session_1 via cookie
3. login_session_2 updated → (session_id: session_1)
4. session_1 updated → (clients: [..., new_client])

Implementation Patterns ​

Creating Sessions ​

Always create sessions linked to a login session:

typescript
// ✅ Correct
const session = await env.data.sessions.create("tenantId", {
  id: "sessionId",
  user_id: "email|userId",
  login_session_id: loginSession.id, // Always link to login session
  clients: ["clientId"],
  // ... other fields
});

Linking Existing Sessions ​

When reusing an existing session for a new login flow:

typescript
// Link existing session to new login session
await env.data.loginSessions.update("tenantId", loginSession.id, {
  session_id: existingSession.id,
});

// Update session's client list if needed
await env.data.sessions.update("tenantId", existingSession.id, {
  clients: [...existingSession.clients, newClientId],
});

Authentication Response ​

Always pass the session ID to ensure session reuse:

typescript
return createFrontChannelAuthResponse(ctx, {
  user,
  authParams: loginSession.authParams,
  client,
  loginSession: { ...loginSession, session_id: existingSession.id },
  sessionId: existingSession.id, // Ensures session reuse
});

Security Considerations ​

CSRF Protection ​

  • Login session IDs are used as OAuth state parameters
  • Prevents CSRF attacks during authentication flows

Session Hijacking ​

  • Sessions are tied to device metadata
  • IP address and user agent tracking
  • Secure cookie settings

Session Expiration ​

  • Login sessions: Short-lived (5-10 minutes)
  • User sessions: Longer-lived (hours to days)
  • Configurable expiration policies

Common Patterns ​

Universal Login Flow ​

  1. User visits /authorize
  2. Redirected to universal login (/u/login/identifier)
  3. User authenticates
  4. Session created and linked to login session
  5. User redirected back to application

Check Account Flow ​

  1. User has existing session (cookie)
  2. New authorization flow starts
  3. Redirected to /u/check-account
  4. Existing session linked to new login session
  5. Authentication completes without re-login

Silent Authentication ​

  1. Application checks authentication status
  2. Uses existing session if valid
  3. No user interaction required
  4. Tokens issued based on existing session

For third-party clients, silent auth additionally checks the stored OAuth consents. If the requested scopes are not covered, the silent flow returns the OIDC error consent_required instead of issuing tokens — the SPA must fall back to an interactive /authorize redirect so the user can grant consent through the universal-login screen.

Debugging Tips ​

sql
-- Find login session by state parameter
SELECT * FROM login_sessions WHERE id = 'state_parameter';

-- Find session linked to login session
SELECT s.* FROM sessions s
JOIN login_sessions ls ON s.id = ls.session_id
WHERE ls.id = 'state_parameter';

-- Find all sessions for a user
SELECT * FROM sessions WHERE user_id = 'email|user123';

Common Issues ​

  • Session not linked: Login session session_id is null
  • Multiple sessions: User has multiple active sessions (normal for different devices)
  • Session reuse failure: Existing session not properly linked to new login session

Revocation ​

Administratively ending a session — the Management API session delete/revoke endpoints, or blocking the user — also revokes every refresh token issued under it. Nothing else does: neither logout nor expiry touches the tokens. The rule the code follows is that revocation couples, lifetime does not: a refresh token is designed to outlive the browser session it was minted in, so killing tokens on an SSO idle timeout would sign every long-lived native client out on each timeout. Logout sits on the lifetime side of that line — it ends the browser session (and notifies clients), but a native app holding a refresh token keeps it.

Cascades to refresh tokens:

PathSessionRefresh tokens
DELETE /api/v2/sessions/{id}deletedrevoked
POST /api/v2/sessions/{id}/revokerevoked_at setrevoked
Blocking a user (blocked: true)all sessions revokedrevoked
GET /v2/logout, GET /oidc/logoutrevoked_at setnot revoked
Absolute / idle expiryexpires on its own clocknot revoked
cleanupSessions / cleanupUserSessionsrow deletednot revoked

The cascade (packages/authhero/src/helpers/revoke-session-refresh-tokens.ts) runs two sweeps in sequence:

  1. session_id — the ownership edge, and the complete one for every token minted since the column was introduced. Rows older than that carry no session_id and are only reachable through the second sweep.
  2. login_id — a legacy fallback for rows minted before session_id existed, using the session's originating login_session_id. That link is never repointed on SSO reuse, so on its own it misses tokens minted during a later re-authorization; it is kept only until pre-existing rows age out.

Both sweeps guard on revoked_at IS NULL, so running them in sequence (or re-running the cascade on an already-revoked session) is idempotent rather than a double write. User block goes through revoke-user-sessions.ts, which pages every session for the user before mutating anything and runs the cascade even for sessions that were already revoked — those were revoked before the cascade existed and can still have live tokens.

Separately, the authorization-code flow revokes refresh tokens by login_id when it detects a code being redeemed twice.

Deleting or revoking a session also sends back-channel logout notifications.

Logout Notifications ​

When a session is revoked — via GET /v2/logout, GET /oidc/logout, or the Management API session delete/revoke endpoints — AuthHero sends a signed logout token to every client that participated in the session and has back-channel logout URLs registered. See OIDC Back-Channel Logout 1.0 for the token format, delivery semantics, and how to register a client.

Cleanup ​

Expired login_sessions, sessions, and refresh_tokens can be swept two ways:

  • Per-request — call cleanupUserSessions(ctx, { tenantId, userId }) via waitUntil after creating a login session. Scoped to one user, cheap to run often.
  • Scheduled — call cleanupSessions(data, { tenantId?, userId? }) from a cron / Cloudflare Worker scheduled() handler. Omit both scopes to sweep everything.
typescript
import { cleanupSessions } from "authhero";

// inside scheduled()
await cleanupSessions(env.data);

See Audit Logging → Scheduled Jobs for a full scheduled() handler example combining session and outbox cleanup.

Cleanup is expiry, not revocation: it removes rows that have already aged out of each table on that table's own clock, and never revokes a live refresh token because the session it points at was swept. This is why refresh_tokens.session_id is not a foreign key — the pointer is allowed to dangle.

Best Practices ​

  1. Always link sessions: Every session should be created with a login_session_id
  2. Reuse existing sessions: Don't create new sessions if a valid one exists
  3. Update client lists: Add new clients to existing sessions when reusing
  4. Handle expiration: Implement proper session cleanup and renewal
  5. Security first: Always validate session ownership and device metadata

Dual-licensed: AGPL-3.0-only or commercial license.