# Auth — Service Registry & Persistence The authentication system lets Discord users link their accounts to external services (currently **Jellyfin**) so the agent can perform actions on their behalf (e.g. checking watch history). --- ## Architecture ``` gateway/auth/ gateway/v1/auth.py ┌──────────────────────┐ ┌──────────────────────────────┐ │ AuthService (ABC) │ │ GET /api/v1/auth/login │ │ ├─ JellyfinAuth │◀─────────│ POST /api/v1/auth/login │ │ └─ (Plex, Seerr…) │ │ GET /api/v1/auth/status │ │ │ │ GET /api/v1/auth/reset │ └─────────┬────────────┘ └──────────────────────────────┘ │ ▼ src/auth_store.py ┌──────────────────────┐ │ SQLite │ │ ├─ link_tokens │ one-time tokens sent via Discord DM │ └─ user_auth │ per-user, per-service credentials └──────────────────────┘ ``` --- ## Files | File | Purpose | |---|---| | `gateway/auth/__init__.py` | Abstract `AuthService` base class + global registry | | `gateway/auth/jellyfin.py` | Jellyfin implementation — Quick Connect + username/password | | `gateway/v1/auth.py` | REST endpoints for the web-based login flow | | `src/auth_store.py` | SQLite persistence for link tokens and stored credentials | --- ## Flow: Discord User Links Jellyfin ``` Discord DM Web Browser Jellyfin Server │ │ │ │ 1. /login jellyfin │ │ │ ──────────────────────────────▶│ │ │ Bot creates link token in │ │ │ SQLite, DMs the user a URL │ │ │ │ │ │ 2. User clicks link │ │ │ ◀─────────────────────────────▶│ │ │ │ GET /api/v1/auth/login │ │ │ ?service=jellyfin │ │ │ &token=xxx&discord_id=123 │ │ │ │ │ │ 3. Serve Quick Connect form │ │ │ ◀──────────────────────────── │ │ │ │ │ │ 4. Initiate Quick Connect │ │ │ ─────────────────────────────▶│ │ │ POST /QuickConnect/Initiate │ │ │ ◀── { Code: "ABC123" } │ │ │ │ │ 5. User enters code in │ │ │ Jellyfin app │ │ │ │ │ │ │ 6. Poll: is it authorized? │ │ │ ─────────────────────────────▶│ │ │ GET /QuickConnect/Connect │ │ │ ◀── Authenticated + Token │ │ │ │ │ 7. auth_store saves: │ │ │ (discord_id, jellyfin, │ │ │ AccessToken, username) │ │ │ │ │ │ 8. "✅ Linked to Jellyfin!" │ │ │ ◀───────────────────────────── │ │ ``` --- ## AuthService Base Class ```python class AuthService(ABC): name: str # "jellyfin" display_name: str # "Jellyfin" def render_login_form(token, discord_id) -> str: ... async def authenticate(form_data) -> AuthResult: ... ``` Add a new service (e.g. Plex, Seerr) by subclassing `AuthService`, dropping the module in `gateway/auth/`, and calling `register_auth_service()` at import time. The REST endpoints and auth store work generically — no changes needed. --- ## Current Implementation: Jellyfin `gateway/auth/jellyfin.py` supports two flows: | Method | How it works | |---|---| | **Quick Connect** (primary) | Calls Jellyfin's `/QuickConnect/Initiate` → polls `/QuickConnect/Connect` → stores the `AccessToken` | | **Username/Password** (fallback) | Renders an HTML form → user submits credentials → calls `/Users/AuthenticateByName` → stores the `AccessToken` | The stored credentials include: - `external_user_id` — Jellyfin user ID - `external_name` — Jellyfin username - `credentials` dict — `{"AccessToken": "...", "ServerURL": "..."}` --- ## Auth Store (SQLite) Two tables in `data/auth.db`: ```sql -- One-time tokens for the web login flow (expire after 10 min) CREATE TABLE link_tokens ( token TEXT PRIMARY KEY, discord_id INTEGER NOT NULL, service TEXT NOT NULL, created_at TEXT NOT NULL, used INTEGER DEFAULT 0 ); -- Per-user, per-service stored credentials CREATE TABLE user_auth ( discord_id INTEGER NOT NULL, service TEXT NOT NULL, external_user_id TEXT, external_name TEXT, credentials TEXT, -- JSON created_at TEXT NOT NULL, PRIMARY KEY (discord_id, service) ); ``` --- ## Skill-Level Auth Gating Skills can declare `requires_auth=["jellyfin"]`. When a tool is executed, the skill system checks the auth store. If the user isn't linked: 1. The tool returns `ToolResult.fail("Please login first using /login jellyfin")` 2. The LLM relays this message to the user in Discord 3. The user types `/login jellyfin` → Quick Connect flow → re-linked → try again