This page is part of the **wikibot.io PRD** (Product Requirements Document). See also: [[Design/Platform_Overview]], [[Design/Data_Model]], [[Design/Implementation_Phases]], [[Design/Operations]]. --- ## Auth & Multi-tenancy > **Superseded.** This page describes the WorkOS-based auth system for wikibot.io. See [[Design/VPS_Architecture]] for the current plan (ATProto OAuth, self-hosted MCP OAuth AS). The ACL model, permission headers, and three-path auth convergence pattern carry forward; the WorkOS integration does not. ### User registration and login **OAuth-only login** — no email/password accounts. Users sign in with Google, GitHub, Microsoft, or Apple. This eliminates password management, credential stuffing, and throwaway-account spam (every user needs a real OAuth identity). Combined with tier limits (1 wiki, 500 pages), this provides sufficient abuse prevention without rate limiting on the free tier. ### Auth provider: WorkOS AuthKit **Why WorkOS**: 1M MAU free tier (vs Clerk's 50K MRU or Cognito's 10K MAU). All four OAuth providers supported. Clean hosted UI (AuthKit). First-class MCP OAuth 2.1 support with a documented FastMCP integration. Python/Flask examples. The free tier is a loss leader for enterprise SSO/SCIM sales ($125/connection/month) — it's strategically sustainable, not charity. **WorkOS cost curve** (2026 pricing): Free to 1M MAU. Beyond that, $2,500/mo per additional 1M block. Custom domain is $99/mo (deferrable — the default `*.authkit.app` subdomain is unbranded). For comparison, Clerk at 100K users costs ~$1,825/mo; WorkOS costs $0. **Two token flows** (important — these are distinct): 1. **Browser login**: User authenticates via WorkOS AuthKit → our `/auth/token` endpoint issues a **platform JWT** (signed with our own RS256 key in Secrets Manager) → all subsequent browser/API requests validate against our key. WorkOS is not in the runtime path. 2. **MCP OAuth (Claude.ai)**: WorkOS acts as the OAuth 2.1 Authorization Server via its Standalone Connect flow. Claude.ai authenticates with WorkOS → WorkOS issues its own access token → our MCP endpoint validates against WorkOS JWKS. FastMCP has a [documented WorkOS integration](https://gofastmcp.com/integrations/authkit) that handles DCR, PKCE, AS metadata, and token validation. **Outage blast radius**: WorkOS outage blocks new logins and new MCP OAuth connections. Active browser sessions (platform JWT) and existing Claude Code bearer tokens are unaffected. Claude.ai MCP connections drop when the WorkOS access token expires and refresh fails — token lifetime determines the window. **Migration path off WorkOS**: We store `oauth_provider` + `oauth_provider_sub` (raw provider subject ID, retrieved via WorkOS API during first login). This enables migration to: - **Self-hosted OAuth** with authlib — handles browser login (2-3 days). But also requires building an OAuth 2.1 AS for MCP (DCR, PKCE, token endpoint, JWKS) — realistically 1-2 weeks total, unless FastMCP's built-in auth providers can replace it. - **Clerk** — 50K MRU free tier, MCP support added mid-2025, free custom domain. - **Cognito** — free, native AWS, ugly UI, 10K MAU free tier. **Apple provider sub caveat**: WorkOS exposes raw provider `sub` claims via API for Google, GitHub, and Microsoft. Apple support for this is undocumented — **verify in Phase 0** that we can retrieve Apple's raw sub. If not, Apple users would need to re-authenticate during a provider migration. **Design rules**: - Browser/API middleware validates platform JWTs (our key). Never touches WorkOS at runtime. - MCP middleware validates WorkOS access tokens (WorkOS JWKS). FastMCP integration handles this. - Both paths converge on the same ACL check: resolve user → look up permissions in DynamoDB → set Otterwiki headers. ### Wiki-level auth Three access methods, all converging on the same ACL check: 1. **Browser session** (web UI): Platform JWT → middleware validates against our RS256 key → extracts user identity → checks ACL in DynamoDB → sets Otterwiki headers 2. **MCP OAuth** (Claude.ai): WorkOS access token → middleware validates against WorkOS JWKS (via FastMCP integration) → extracts user identity → checks ACL → sets Otterwiki headers 3. **Bearer token** (Claude Code, API clients): Token in `Authorization` header → middleware hashes token, looks up in Wikis table → resolves to user + wiki → checks ACL ### MCP auth Two paths: **Claude.ai (OAuth 2.1)**: WorkOS acts as the Authorization Server. FastMCP's WorkOS integration handles the plumbing (DCR, PKCE, AS metadata, token validation). The MCP endpoint serves `/.well-known/oauth-protected-resource` pointing to WorkOS. Claude.ai discovers this, authenticates via WorkOS, and presents the access token to the MCP endpoint. Per-wiki authorization happens in our middleware, not in WorkOS — WorkOS identifies the user, we check whether they can access the wiki. **Claude Code / API clients (bearer token)**: Each wiki gets a unique MCP bearer token, generated at wiki creation time, stored as a bcrypt hash in DynamoDB. The user sees the token once (at creation) and can regenerate it from the dashboard. Usage: `claude mcp add --transport http`. MCP endpoint URL: `https://{username}.wikibot.io/{wiki}/mcp`. ### ACL model Simple role-based model: | Role | Read | Write | Delete | Manage ACL | Delete wiki | |------|------|-------|--------|------------|-------------| | viewer | yes | no | no | no | no | | editor | yes | yes | yes | no | no | | owner | yes | yes | yes | yes | yes | Wiki creator is always owner. Owners can grant viewer/editor access to other registered users (by email). Free tier: up to 3 collaborators. Premium tier: up to 25. --- ## AAA Model (Authentication, Authorization, Accounting) ### Authentication (who are you?) All authentication happens at the platform layer. Otterwiki never sees credentials — it runs in `PROXY_HEADER` auth mode and trusts headers from the platform middleware. Four entry points, all converging to the same identity: ``` Browser → OAuth (Google/GitHub/Microsoft/Apple) → JWT Claude Code → MCP bearer token Claude.ai → OAuth 2.1 → JWT Git CLI → Git credential (bearer token) All paths → platform middleware → resolves to User record in DynamoDB ``` The platform middleware is the single authentication boundary. Everything downstream trusts it. ### Authorization (what can you do?) **Layer 1 — Platform middleware** (before Otterwiki sees the request): 1. Resolve user identity from JWT or bearer token 2. Resolve wiki from `{username}.wikibot.io/{wiki_slug}` 3. Look up ACL: does this user have a grant on this wiki? 4. If no grant and wiki is not public → 403 5. Check tier limits (page count, attachment size) on writes 6. Map ACL role to Otterwiki permission headers: | ACL role | `x-otterwiki-permissions` header | |----------|----------------------------------| | viewer | `READ` | | editor | `READ,WRITE,UPLOAD` | | owner | `READ,WRITE,UPLOAD,ADMIN` | | anonymous (public wiki) | Synthetic user with `READ` only | 7. Set headers: `x-otterwiki-email`, `x-otterwiki-name`, `x-otterwiki-permissions` 8. Forward to Otterwiki **Layer 2 — Otterwiki** (`AUTH_METHOD=PROXY_HEADER`): - Reads headers, creates ephemeral user object per request - No local user database — all identity comes from headers - Enforces READ/WRITE/UPLOAD/ADMIN based on the permissions header - Owner gets the admin panel; editors can edit; viewers can read **For MCP and API paths**, Otterwiki is not involved in auth — the handlers read the git repo directly. Authorization happens entirely in Layer 1. #### Public wiki access When a wiki is set to public, unauthenticated visitors need read access. Rather than changing Otterwiki's `READ_ACCESS` config per wiki, the platform middleware injects a synthetic anonymous user with READ permission for public wikis. Otterwiki config stays identical for all wikis: ```python AUTH_METHOD = "PROXY_HEADER" READ_ACCESS = "APPROVED" # always — public access handled by middleware WRITE_ACCESS = "APPROVED" ATTACHMENT_ACCESS = "APPROVED" DISABLE_REGISTRATION = True # no Otterwiki-level registration ``` #### Per-wiki Otterwiki configuration Each wiki needs its own Otterwiki config. Platform-managed settings (set by the platform, not the user): ```python AUTH_METHOD = "PROXY_HEADER" READ_ACCESS = "APPROVED" WRITE_ACCESS = "APPROVED" ATTACHMENT_ACCESS = "APPROVED" DISABLE_REGISTRATION = True ``` User-configurable settings (via Otterwiki admin panel, stored in the wiki's config): ```python SITE_NAME = "Third Gulf War" # wiki display name SITE_DESCRIPTION = "..." # meta description SITE_LOGO = "..." # custom logo URL # sidebar config, content/editing prefs, etc. ``` ### Otterwiki Admin Panel — Section Disposition The wiki owner (ACL role `owner`) gets `ADMIN` permission, which grants access to Otterwiki's admin panel at `/-/admin/*`. Some sections are useful; others conflict with platform-managed settings and must be disabled. | Admin section | Route | Disposition | Reason | |--------------|-------|-------------|--------| | Application Preferences | `/-/admin` | **Keep** | Wiki branding: site name, description, logo, favicon, language, custom home page, robot crawlers | | Sidebar Preferences | `/-/admin/sidebar_preferences` | **Keep** | UI layout: sidebar shortcuts, custom menu items, page index mode/focus | | Content and Editing | `/-/admin/content_and_editing` | **Keep** | Git workflow: commit message mode/template, page name casing, underscore handling, WikiLink style | | Repository Management | `/-/admin/repository_management` | **Disable** | Conflicts with platform Git management. Git web server, remote push/pull, SSH keys — all managed by platform. Premium external Git sync replaces this. | | Permissions and Registration | `/-/admin/permissions_and_registration` | **Disable** | Conflicts with platform auth. READ/WRITE/ATTACHMENT levels, registration, approval, email confirmation — all managed by platform middleware. | | User Management | `/-/admin/user_management` | **Disable** | No local user database in ProxyHeaderAuth mode. Add/edit/delete user forms are non-functional. User management happens in the platform dashboard (ACL grants). | | Mail Preferences | `/-/admin/mail_preferences` | **Disable for MVP** | SMTP configuration for notifications. Not relevant until we add wiki-level email notifications (e.g., notify collaborators on page edit). Could re-enable as a premium feature later. | **Implementation**: Override the admin navigation template to hide disabled sections. Return 404 from disabled routes in middleware (defense in depth — don't rely solely on hiding the links). This is a small fork change: modify `templates/settings.html` to conditionally render nav items, and add a decorator or middleware check on the disabled routes. ### Accounting (resource tracking and tier enforcement) All enforcement happens in Layer 1 middleware, before Otterwiki runs. | Resource | Where tracked | Checked on | |----------|--------------|------------| | Wiki count per user | Metadata store (`User.wiki_count`) | Wiki creation | | Page count per wiki | Metadata store (`Wiki.page_count`) | Page creation (write path) | | Attachment total size per wiki | Metadata store (updated on upload) | Attachment upload | | Single attachment size | Computed from request | Attachment upload | | Collaborator count per wiki | Metadata store (count ACL grants) | ACL grant | | Semantic search access | Metadata store (`Wiki.semantic_search_enabled`) | Semantic search request | | REST API access | Metadata store (`User.tier`) | Any `/api/v1/*` request | | Custom domain | Metadata store (`User.tier`) | Domain configuration | | Git write access | Metadata store (`User.tier`) | `git push` (receive-pack) | A free user hitting the 500-page limit gets a clear error ("Upgrade to premium for unlimited pages") before any git operation occurs. Tier checks are fast (single DynamoDB read, cached in warm Lambda).