Commit d70fe3

2026-03-20 19:52:59 Claude (MCP): [mcp] Rewrite Design/Auth for VPS/ATProto OAuth; remove AWS/WorkOS content
Design/Auth.md ..
@@ 1,60 1,33 @@
- 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]].
-
+ ---
+ status: current
+ platform: robot.wtf (VPS)
---
- ## 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.
+ Extracted from the original wikibot.io design. AWS-specific content archived at [[Archive/AWS_Design/Auth]].
- **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.
+ See also: [[Design/VPS_Architecture]], [[Design/Data_Model]].
- **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
+ ## Auth Overview
- Three access methods, all converging on the same ACL check:
+ **ATProto OAuth login** — users authenticate via their ATProto identity (Bluesky handle/DID). The platform runs a self-hosted OAuth 2.1 Authorization Server for MCP connections.
- 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
+ Three auth paths, all converging on the same identity resolution and ACL check:
- ### MCP auth
+ 1. **Browser session**: ATProto OAuth → platform issues a session JWT (signed with our RS256 key) → middleware validates JWT on each request → resolves user → checks ACL → sets Otterwiki headers.
+ 2. **MCP OAuth (Claude.ai)**: Self-hosted OAuth 2.1 AS handles DCR, PKCE, token issuance, JWKS. Claude.ai discovers `/.well-known/oauth-protected-resource`, authenticates, presents access token. Per-wiki authorization in our middleware.
+ 3. **Bearer token (Claude Code, API clients)**: Token in `Authorization` header → middleware hashes token, looks up in DB → resolves to user + wiki → checks ACL.
- Two paths:
+ The platform middleware is the single authentication boundary. Everything downstream trusts it.
- **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.
+ ## MCP Auth
- **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`.
+ **OAuth 2.1 (Claude.ai)**: Self-hosted AS (authlib-based) provides DCR, PKCE, AS metadata, token endpoint, and JWKS. The MCP endpoint serves `/.well-known/oauth-protected-resource` pointing to the local AS. Per-wiki authorization happens in middleware — the AS identifies the user, middleware checks wiki access. See [[Design/VPS_Architecture]] for implementation details.
- MCP endpoint URL: `https://{username}.wikibot.io/{wiki}/mcp`.
+ **Bearer token (Claude Code / API)**: Each wiki gets a unique MCP bearer token, stored as a bcrypt hash in the platform DB. The user sees the token once (at creation) and can regenerate it from the dashboard. Usage: `claude mcp add --transport http`.
- ### ACL model
+ ## ACL Model
Simple role-based model:
@@ 64,39 37,17 @@
| 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.
-
- ---
+ Wiki creator is always owner. Owners can grant viewer/editor access to other registered users.
- ## 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?)
+ ## Authorization Flow
**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}`
+ 2. Resolve wiki from request routing
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:
+ 5. Map ACL role to Otterwiki permission headers:
| ACL role | `x-otterwiki-permissions` header |
|----------|----------------------------------|
@@ 105,21 56,20 @@
| 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
+ 6. Set headers: `x-otterwiki-email`, `x-otterwiki-name`, `x-otterwiki-permissions`
+ 7. 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
+ ### 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:
+ 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"
@@ 129,57 79,18 @@
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)
+ ## Otterwiki Admin Panel — Section Disposition
- All enforcement happens in Layer 1 middleware, before Otterwiki runs.
+ The wiki owner (ACL role `owner`) gets `ADMIN` permission → access to `/-/admin/*`.
- | 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) |
+ | Admin section | Disposition | Reason |
+ |--------------|-------------|--------|
+ | Application Preferences | **Keep** | Wiki branding: site name, description, logo, favicon, language |
+ | Sidebar Preferences | **Keep** | UI layout: sidebar shortcuts, custom menu items |
+ | Content and Editing | **Keep** | Git workflow: commit message mode/template, page name casing, WikiLink style |
+ | Repository Management | **Disable** | Conflicts with platform Git management |
+ | Permissions and Registration | **Disable** | Conflicts with platform auth (middleware-managed) |
+ | User Management | **Disable** | No local user database in ProxyHeaderAuth mode |
+ | Mail Preferences | **Disable for MVP** | SMTP notifications — revisit later |
- 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).
\ No newline at end of file
+ **Implementation**: Override admin navigation template to hide disabled sections. Return 404 from disabled routes in middleware (defense in depth).
\ No newline at end of file
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9