Authentication model
VerifAIer uses two credential types, chosen by tier. Both are local-first primitives, there is no hosted OAuth, OIDC, or SSO integration in the current implementation.
auth/config.py · auth/guards.py · auth/service.py · api/auth_routes.py · cc/auth.pySession tokens (browser + API clients)
POST /api/auth/login exchanges email + password for a session. The session token is an HMAC keyed by SESSION_SECRET with an 8-hour default TTL (AUTH_TOKEN_TTL=28800). Two transports are accepted, cookie first:
vai_sessioncookie: set by login,HttpOnly;Securein production;SameSite=Lax. Browser clients use this automatically. A non-HttpOnlyvai_session_active=1flag cookie lets JS detect a session without reading the token.X-Session-Tokenheader: for API clients, the CLI and the MCP server. Backwards-compatible; carries the raw token returned in the login response body.
sequenceDiagram
autonumber
participant C as Client
participant API as /api/auth/login
participant RL as RateLimiter
participant S as session service
C->>API: POST {email, password}
API->>RL: is_allowed(login:IP, 20, 900s)
alt over limit
RL-->>C: 429 Too many login attempts
else allowed
API->>S: authenticate_user(email, password)
alt bad credentials
S-->>C: 401 Invalid credentials
else ok
S->>S: create_session(user_id) → HMAC token
API-->>C: 200 {session_token, ...} + Set-Cookie vai_session (HttpOnly)
end
end
Login flow. Source: api/auth_routes.py + auth/service.py + security/rate_limit.py.| Env var | Default | Meaning |
|---|---|---|
SESSION_SECRET | ephemeral per-process | HMAC key. Required in production: if unset, sessions do not survive restarts, and the app refuses to start when AUTH_COOKIE_SECURE=true. |
AUTH_COOKIE_NAME | vai_session | session cookie name |
AUTH_TOKEN_TTL | 28800 | session lifetime, seconds (8h) |
AUTH_COOKIE_SECURE | false | set true on HTTPS/production |
AUTH_COOKIE_SAMESITE | lax | lax · strict · none |
API keys (control tier)
The Control Center data endpoints (/api/control/*) authenticate with an X-API-Key header. Keys are formatted vrf_<8-hex-prefix>_<secret>, stored as PBKDF2-HMAC-SHA256 hashes (100k iterations) with a per-key salt, and scoped to an organization. The prefix is a fast DB lookup index; the full key is shown once at creation and never stored. Provision one with the owner bootstrap script (scripts/cc_bootstrap_owner.py → create_org_and_key).
API tiers
| Tier | Prefixes | Auth | 401 when… |
|---|---|---|---|
| open | /api/v1/*, /health, /validate, /pipeline, /dev/audit, /sentinel/verify | none | never (open) |
| session | /api/auth/*, /api/vai/*, /api/orgs/*, /api/observability/*, /api/security/*, /api/billing/* | vai_session cookie or X-Session-Token | no/invalid/expired session |
| API key | /api/control/* (data endpoints) | X-API-Key: vrf_… | missing/invalid key (403 if revoked) |
The Control Center also exposes a session-based console subset (/api/control/auth/login, /auth/me, /auth/logout and some member/settings management) that binds an org membership; those use org-membership sessions rather than an API key. See the authenticated APIs.
Authorization model
Authorization today is deliberately coarse and honest about its scope:
- Session tier, authentication-gated, no role enforcement. The
require_authenticated_userguard accepts any valid session ("No role enforcement, all authenticated users are accepted"). TheAuthenticatedUserrecord carriesorganization_id,membership_idandrolefields, but for the current session path they are empty strings, reserved for future RBAC gates, not yet enforced. - API-key tier, org-scoped. A validated
X-API-Keyresolves to anorg_id; control-center data is scoped to that organization. - Billing, org-access checks.
/api/billing/*adds an explicit_require_org_access(org_id, user)check on top of authentication. - Control console, role-aware. A subset of
/api/control/*usesrequire_admin_session/require_user_session(org-membership sessions) where an admin vs. member distinction applies.
Headers
Request headers
| Header | When | Notes |
|---|---|---|
Content-Type: application/json | all POST/PATCH | bodies are JSON |
Cookie: vai_session=… | session tier, browser | set automatically after login |
X-Session-Token: <token> | session tier, API/CLI/MCP | raw token from login response |
X-API-Key: vrf_<prefix>_<secret> | control tier | org-scoped PBKDF2 key |
Response headers
Every response carries hardening headers set by _SecurityHeadersMiddleware (with setdefault, so handlers can override but never lose them):
X-Content-Type-Options: nosniff X-Frame-Options: DENY X-XSS-Protection: 1; mode=block Referrer-Policy: strict-origin-when-cross-origin
CORS is applied by CORSMiddleware: origins from CORS_ORIGINS (JSON array, default ["*"] for the extension + dev dashboard), credentials disabled, methods limited to GET, POST, DELETE, OPTIONS. Restrict CORS_ORIGINS in production.
Security considerations
| Concern | How it is handled |
|---|---|
| Provider secrets | read from server env at call time; never accepted from or returned to callers; config stores presence booleans only. See security boundaries. |
| Credential oracles | login returns the same 401 for unknown email vs. wrong password; API-key validation returns the same 401 for bad format vs. bad value, no enumeration. |
| Password / key storage | PBKDF2-HMAC-SHA256 with per-record salt; constant-time comparison (hmac.compare_digest). |
| Session cookies | HttpOnly; Secure in production; SameSite=Lax; short TTL; revocable. |
| Login abuse | per-IP rate limit → 429 (see rate limits). |
| Startup gates | the app refuses to start when SESSION_SECRET is ephemeral and AUTH_COOKIE_SECURE=true (production posture); soft-secret warnings otherwise. |
| Prompts / inputs | never logged, never echoed into evidence, diagnostics or responses. |