Appearance
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):
Authorization: Bearer <token>headeraccess_tokenquery parameter- 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:
| Claim | Description |
|---|---|
sub | User UUID |
sid | Session ID (required; JWTs without sid are rejected to force refresh) |
exp | Expiration timestamp |
impersonated_by | Admin UUID (only in impersonation tokens) |
impersonation_session | Impersonation session ID |
Session-bound JWT flow:
- Verify JWT signature + expiry
- Look up
sidin session cache (Redis/memory) for fast path - On cache miss, validate session in
odp_sessionstable - Load user + role, set accountability, re-cache for next request
Impersonation JWT flow:
- JWT has
impersonated_by+impersonation_sessionclaims - Validate impersonation session is active
- Load target user's role and access flags
- Set
is_impersonating: trueon 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: falsealwaysrolesis intersected withallowed_rolesat auth time (SCOPE-01)scopesrestricts allowed actions (e.g.,['read'])sub_token_idset for audit traillast_used_atupdated fire-and-forget on each use
Sub-token errors:
TokenExpiredErrorif past expiryTokenRevokedErrorif manually revokedInvalidTokenErrorif not found or parent user inactive
Public Access
When no token is provided:
- Load
public_rolefromodp_settings(cached in memory after first load) - If configured, set
roleandrolesto the public role ID - 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
| Variable | Default | Description |
|---|---|---|
SECRET | (required) | JWT signing secret |
ACCESS_TOKEN_TTL | 15m | Access token lifetime |
REFRESH_TOKEN_TTL | 7d | Refresh token / session lifetime |
SESSION_COOKIE_ENABLED | true | Enable session cookie mode |
SESSION_COOKIE_NAME | odp_session_token | Cookie 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_secretis set. - SSO users: Attempting to enable TFA returns
400 INVALID_PAYLOADwith"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.