Files

6.9 KiB

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

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

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:

-- 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