Skip to content

Auth Middleware & Token Strategy

Overview

Authentication is handled by src/middleware/authenticate.ts. It runs as a preHandler on every request and sets request.accountability — the security context used by all downstream services and permission checks.

If no token is provided, the request gets public accountability (possibly with a public role if configured in settings).

Token Extraction

Before authenticate runs, extractToken reads the token from (in order):

  1. Authorization: Bearer <token> header
  2. access_token query parameter
  3. Session cookie (SESSION_COOKIE_NAME, default: odp_session_token)

The extracted value is stored in request.token.

4-Strategy Authentication Pipeline

Strategy 1: JWT Verification

The JWT is signed with SECRET. A valid JWT payload contains:

ClaimDescription
subUser UUID
sidSession ID (required; JWTs without sid are rejected to force refresh)
expExpiration timestamp
impersonated_byAdmin UUID (only in impersonation tokens)
impersonation_sessionImpersonation session ID

Session-bound JWT flow:

  1. Verify JWT signature + expiry
  2. Look up sid in session cache (Redis/memory) for fast path
  3. On cache miss, validate session in odp_sessions table
  4. Load user + role, set accountability, re-cache for next request

Impersonation JWT flow:

  1. JWT has impersonated_by + impersonation_session claims
  2. Validate impersonation session is active
  3. Load target user's role and access flags
  4. Set is_impersonating: true on accountability

Strategy 2: Static Token

Users can have a long-lived static token stored in odp_users.token. Matched directly against the token string.

  • No expiry
  • Never cached — always queries odp_users
  • Admin users can generate/revoke via POST /users/me/token

Strategy 3: Session Token (Direct)

The raw session token from odp_sessions.token can be used directly (without a JWT wrapper). Used by some internal flows.

Strategy 4: Sub-Token (odp: prefix)

Sub-tokens are scoped, limited-lifetime tokens prefixed with odp:. Stored in odp_user_tokens.

odp:xxxxxxxx...

Sub-token accountability:

  • admin: false, tech: false, app: false always
  • roles is intersected with allowed_roles at auth time (SCOPE-01)
  • scopes restricts allowed actions (e.g., ['read'])
  • sub_token_id set for audit trail
  • last_used_at updated fire-and-forget on each use

Sub-token errors:

  • TokenExpiredError if past expiry
  • TokenRevokedError if manually revoked
  • InvalidTokenError if not found or parent user inactive

Public Access

When no token is provided:

  1. Load public_role from odp_settings (cached in memory after first load)
  2. If configured, set role and roles to the public role ID
  3. Otherwise, user: null, role: null, admin: false

Clear the public role cache by calling clearPublicRoleCache() (invoked automatically when settings change).

Accountability Structure

typescript
interface Accountability {
  user: string | null;         // User UUID
  role: string | null;         // Primary role UUID
  roles: string[];             // All role UUIDs (for policy evaluation)
  admin: boolean;              // Bypasses all permission checks
  tech: boolean;               // System access (implies admin)
  app: boolean;                // App panel access
  ip: string | null;
  userAgent: string | null;
  origin: string | null;
  // Optional — only set in special flows
  is_impersonating?: boolean;
  impersonated_by?: string | null;
  impersonation_session?: string | null;
  sub_token_id?: string | null;
  scopes?: string[] | null;
}

Extension Hook

Extensions can override authentication entirely via the authenticate filter:

typescript
emitter.onFilter('authenticate', async (accountability, { req }) => {
  // Return a custom accountability object to bypass all strategies
  return myCustomAccountability;
});

If the filter throws or returns the same default accountability, the normal pipeline continues.

Token Lifetime Configuration

VariableDefaultDescription
SECRET(required)JWT signing secret
ACCESS_TOKEN_TTL15mAccess token lifetime
REFRESH_TOKEN_TTL7dRefresh token / session lifetime
SESSION_COOKIE_ENABLEDtrueEnable session cookie mode
SESSION_COOKIE_NAMEodp_session_tokenCookie name

Session Cache

Sessions are cached in memory (or Redis when REDIS_ENABLED=true) to avoid a DB hit on every request. The cache key is the sid from the JWT. TTL matches REFRESH_TOKEN_TTL.

On session revocation (logout, delete), the cache entry is explicitly deleted to prevent stale access.

TFA and SSO Users

Two-factor authentication (TFA/TOTP) is only applicable to users who authenticate via local credentials (email + password). SSO-authenticated users — those with any linked provider in odp_user_providers — are not permitted to enable TFA.

The rationale: SSO users have their identity verified by an external Identity Provider (IdP). The IdP is responsible for any additional authentication factors (MFA at the IdP level). Adding a local TOTP layer on top would be redundant and unsupported.

Behavior summary:

  • Local users: TFA enable/disable is available. Login requires OTP if tfa_secret is set.
  • SSO users: Attempting to enable TFA returns 400 INVALID_PAYLOAD with "TFA is not available for SSO-authenticated users".
  • SSO login flow (/auth/sso/verify, /auth/saml/verify): TFA check is skipped entirely — SSO flows never prompt for OTP.

ODP Internal API Documentation