6.9 KiB
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 |
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
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 IDexternal_name— Jellyfin usernamecredentialsdict —{"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:
- The tool returns
ToolResult.fail("Please login first using /login jellyfin") - The LLM relays this message to the user in Discord
- The user types
/login jellyfin→ Quick Connect flow → re-linked → try again