Skip to content

Multi-User Authentication

oAI-Web supports multiple users with role-based access control, TOTP MFA, and API key authentication.


Roles

Role Capabilities
admin Full access: all settings, all audit logs, user management, credentials, global whitelists, bash tool
user Own settings, own audit entries, personal API keys, browser trusted domains, no admin panels

Session cookies

Sessions are HMAC-signed cookies:

base64url(json_payload) + "." + hmac_sha256(base64url, secret)[:32]

Payload:

{"uid": "user-uuid", "un": "username", "role": "admin", "iat": 1234567890}

Signing secret: Auto-generated at startup, stored in credentials as system:session_secret. Rotating this secret invalidates all existing sessions.

Lifetime: 30 days from issue. No refresh mechanism — users must re-login after expiry.

Why HMAC cookies instead of JWTs?

Same security properties as JWT (signed, not encrypted — so user can decode payload but not forge it). Simpler implementation with fewer moving parts. No library dependency for the core verification.


Password hashing

Argon2id via argon2-cffi. Industry recommendation for modern password hashing (preferred over bcrypt by OWASP since 2022). Default parameters from the library are used.


MFA (TOTP)

Multi-factor authentication uses TOTP (RFC 6238), compatible with any authenticator app (Google Authenticator, Authy, 1Password, etc.).

Setup flow

  1. Admin calls POST /api/admin/users/{id}/mfa-setup
  2. Server generates a secret, returns a QR code (PNG) and provisioning URI
  3. User scans the QR code in their authenticator app
  4. On next login, user is prompted to enter the 6-digit TOTP code
  5. If correct (valid_window=1 = ±30 seconds tolerance), session is created

Challenge flow

  1. Username + password validated
  2. MFA challenge token created in mfa_challenges table (5-minute expiry)
  3. User redirected to /mfa page with the challenge token
  4. User enters TOTP code
  5. If valid: session cookie set, user redirected to original destination

Disabling MFA

Admin calls POST /api/admin/users/{id}/mfa-disable. The totp_secret is cleared. Normal password-only login resumes.


API key authentication

API keys are per-user tokens that can be set in Settings → Profile.

X-API-Key: your-api-key

or

Authorization: Bearer your-api-key

API key auth resolves to SYNTHETIC_API_ADMIN — a synthetic admin user with no DB entry. This means API key access always has admin privileges.

The key is stored (hashed) in user_settings["api_key"]. The plaintext is only shown once at generation time.


Auth middleware

_AuthMiddleware in main.py runs on every request:

  1. Check for API key in headers → synthetic admin user
  2. Decode and verify session cookie
  3. Check if user is active (is_active=True)
  4. If MFA challenge in progress: allow /mfa and /api/auth/mfa through
  5. Store the resolved CurrentUser in the current_user ContextVar

Public routes that bypass auth: - /login, /setup, /health - /webhook/* (protected by their own token mechanism) - Static assets (/static/*)

_require_auth() vs _require_admin()

Route handlers call these helpers directly (dependency injection alternative):

# Allow any authenticated user
user = await _require_auth(request)

# Admin only
user = await _require_admin(request)

Both return the CurrentUser object or raise HTTPException(401/403).


First-run setup

If no users exist in the database, all traffic is redirected to /setup. This page creates the first admin account. After the admin is created, /setup is no longer accessible.


User folders

Each user optionally gets a private filesystem folder: - Location: {system:users_base_folder}/{username}/ - Created automatically on first access - Non-admin users get BoundFilesystemTool scoped to this folder (cannot escape it) - Admin users get the global FilesystemTool (whitelist-based)

Set system:users_base_folder in Settings → Credentials to enable this feature.


Login rate limiting

server/login_limiter.py implements IP-based rate limiting for the /login endpoint: - 5 failed attempts per 15 minutes per IP - Lockout sends HTTP 429 with Retry-After header - In-memory (resets on server restart)