Blame

0e123c Claude (MCP) 2026-03-13 17:49:46
[mcp] Normalize spaces to underscores
1
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]].
dc5336 Claude (Dev) 2026-03-13 01:50:43
[mcp] Port PRD auth to wiki
2
3
---
4
5
## Auth & Multi-tenancy
6
c1d97b Claude (MCP) 2026-03-15 01:19:19
[mcp] Add superseded banner to Auth
7
> **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.
8
dc5336 Claude (Dev) 2026-03-13 01:50:43
[mcp] Port PRD auth to wiki
9
### User registration and login
10
11
**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.
12
13
### Auth provider: WorkOS AuthKit
14
15
**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.
16
17
**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.
18
19
**Two token flows** (important — these are distinct):
20
21
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.
22
23
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.
24
25
**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.
26
27
**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:
28
- **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.
29
- **Clerk** — 50K MRU free tier, MCP support added mid-2025, free custom domain.
30
- **Cognito** — free, native AWS, ugly UI, 10K MAU free tier.
31
32
**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.
33
34
**Design rules**:
35
- Browser/API middleware validates platform JWTs (our key). Never touches WorkOS at runtime.
36
- MCP middleware validates WorkOS access tokens (WorkOS JWKS). FastMCP integration handles this.
37
- Both paths converge on the same ACL check: resolve user → look up permissions in DynamoDB → set Otterwiki headers.
38
39
### Wiki-level auth
40
41
Three access methods, all converging on the same ACL check:
42
43
1. **Browser session** (web UI): Platform JWT → middleware validates against our RS256 key → extracts user identity → checks ACL in DynamoDB → sets Otterwiki headers
44
2. **MCP OAuth** (Claude.ai): WorkOS access token → middleware validates against WorkOS JWKS (via FastMCP integration) → extracts user identity → checks ACL → sets Otterwiki headers
45
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
46
47
### MCP auth
48
49
Two paths:
50
51
**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.
52
53
**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`.
54
55
MCP endpoint URL: `https://{username}.wikibot.io/{wiki}/mcp`.
56
57
### ACL model
58
59
Simple role-based model:
60
61
| Role | Read | Write | Delete | Manage ACL | Delete wiki |
62
|------|------|-------|--------|------------|-------------|
63
| viewer | yes | no | no | no | no |
64
| editor | yes | yes | yes | no | no |
65
| owner | yes | yes | yes | yes | yes |
66
67
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.
68
69
---
70
71
## AAA Model (Authentication, Authorization, Accounting)
72
73
### Authentication (who are you?)
74
75
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.
76
77
Four entry points, all converging to the same identity:
78
79
```
80
Browser → OAuth (Google/GitHub/Microsoft/Apple) → JWT
81
Claude Code → MCP bearer token
82
Claude.ai → OAuth 2.1 → JWT
83
Git CLI → Git credential (bearer token)
84
85
All paths → platform middleware → resolves to User record in DynamoDB
86
```
87
88
The platform middleware is the single authentication boundary. Everything downstream trusts it.
89
90
### Authorization (what can you do?)
91
92
**Layer 1 — Platform middleware** (before Otterwiki sees the request):
93
94
1. Resolve user identity from JWT or bearer token
95
2. Resolve wiki from `{username}.wikibot.io/{wiki_slug}`
96
3. Look up ACL: does this user have a grant on this wiki?
97
4. If no grant and wiki is not public → 403
98
5. Check tier limits (page count, attachment size) on writes
99
6. Map ACL role to Otterwiki permission headers:
100
101
| ACL role | `x-otterwiki-permissions` header |
102
|----------|----------------------------------|
103
| viewer | `READ` |
104
| editor | `READ,WRITE,UPLOAD` |
105
| owner | `READ,WRITE,UPLOAD,ADMIN` |
106
| anonymous (public wiki) | Synthetic user with `READ` only |
107
108
7. Set headers: `x-otterwiki-email`, `x-otterwiki-name`, `x-otterwiki-permissions`
109
8. Forward to Otterwiki
110
111
**Layer 2 — Otterwiki** (`AUTH_METHOD=PROXY_HEADER`):
112
113
- Reads headers, creates ephemeral user object per request
114
- No local user database — all identity comes from headers
115
- Enforces READ/WRITE/UPLOAD/ADMIN based on the permissions header
116
- Owner gets the admin panel; editors can edit; viewers can read
117
118
**For MCP and API paths**, Otterwiki is not involved in auth — the handlers read the git repo directly. Authorization happens entirely in Layer 1.
119
120
#### Public wiki access
121
122
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:
123
124
```python
125
AUTH_METHOD = "PROXY_HEADER"
126
READ_ACCESS = "APPROVED" # always — public access handled by middleware
127
WRITE_ACCESS = "APPROVED"
128
ATTACHMENT_ACCESS = "APPROVED"
129
DISABLE_REGISTRATION = True # no Otterwiki-level registration
130
```
131
132
#### Per-wiki Otterwiki configuration
133
134
Each wiki needs its own Otterwiki config. Platform-managed settings (set by the platform, not the user):
135
136
```python
137
AUTH_METHOD = "PROXY_HEADER"
138
READ_ACCESS = "APPROVED"
139
WRITE_ACCESS = "APPROVED"
140
ATTACHMENT_ACCESS = "APPROVED"
141
DISABLE_REGISTRATION = True
142
```
143
144
User-configurable settings (via Otterwiki admin panel, stored in the wiki's config):
145
146
```python
147
SITE_NAME = "Third Gulf War" # wiki display name
148
SITE_DESCRIPTION = "..." # meta description
149
SITE_LOGO = "..." # custom logo URL
150
# sidebar config, content/editing prefs, etc.
151
```
152
153
### Otterwiki Admin Panel — Section Disposition
154
155
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.
156
157
| Admin section | Route | Disposition | Reason |
158
|--------------|-------|-------------|--------|
159
| Application Preferences | `/-/admin` | **Keep** | Wiki branding: site name, description, logo, favicon, language, custom home page, robot crawlers |
160
| Sidebar Preferences | `/-/admin/sidebar_preferences` | **Keep** | UI layout: sidebar shortcuts, custom menu items, page index mode/focus |
161
| Content and Editing | `/-/admin/content_and_editing` | **Keep** | Git workflow: commit message mode/template, page name casing, underscore handling, WikiLink style |
162
| 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. |
163
| 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. |
164
| 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). |
165
| 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. |
166
167
**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.
168
169
### Accounting (resource tracking and tier enforcement)
170
171
All enforcement happens in Layer 1 middleware, before Otterwiki runs.
172
173
| Resource | Where tracked | Checked on |
174
|----------|--------------|------------|
175
| Wiki count per user | Metadata store (`User.wiki_count`) | Wiki creation |
176
| Page count per wiki | Metadata store (`Wiki.page_count`) | Page creation (write path) |
177
| Attachment total size per wiki | Metadata store (updated on upload) | Attachment upload |
178
| Single attachment size | Computed from request | Attachment upload |
179
| Collaborator count per wiki | Metadata store (count ACL grants) | ACL grant |
180
| Semantic search access | Metadata store (`Wiki.semantic_search_enabled`) | Semantic search request |
181
| REST API access | Metadata store (`User.tier`) | Any `/api/v1/*` request |
182
| Custom domain | Metadata store (`User.tier`) | Domain configuration |
183
| Git write access | Metadata store (`User.tier`) | `git push` (receive-pack) |
184
185
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).