Monorepo for Aesthetic.Computer aesthetic.computer
at main 766 lines 23 kB view raw view rendered
1# API Token System for MCP Authentication 2 3**Status:** Design Phase 4**Date:** 2026-02-12 5**Author:** Claude (via @jeffrey) 6 7--- 8 9## Executive Summary 10 11To enable users to authenticate their MCP publishing with aesthetic.computer accounts, we need a long-lived API token system. The current Auth0 Bearer tokens expire after 24 hours, making them impractical for MCP client integration. 12 13**Recommended Solution:** Implement user-managed API tokens with web UI for generation/revocation. 14 15--- 16 17## Current State Analysis 18 19### Authentication Flow (as of 2026.02.12) 20 21``` 22User Login (Web) 23 24 Auth0 OAuth2 25 26Access Token (24hr expiry) 27 28Bearer Token in Authorization header 29 30validate via /userinfo endpoint 31``` 32 33### Existing Code Components 34 351. **`system/backend/authorization.mjs`** 36 - `authorize()` function validates Bearer tokens via Auth0 37 - Calls `https://aesthetic.us.auth0.com/userinfo` 38 - Returns user object with `sub` (user ID) and email 39 402. **`system/netlify/functions/auth-cli-callback.mjs`** 41 - Handles OAuth callback for CLI tools 42 - Returns `access_token` to authenticated clients 43 - Used for temporary CLI authentication 44 453. **Publishing Endpoints** 46 - `store-piece.mjs`, `store-kidlisp.mjs`, `store-clock.mjs` 47 - All support optional Bearer token authentication 48 - Anonymous publishing works without token 49 50### Current Limitations 51 52| Issue | Impact | Priority | 53|-------|--------|----------| 54| Short-lived tokens (24hr) | Users must re-authenticate daily | 🔴 High | 55| No token management UI | Users can't generate/revoke tokens | 🔴 High | 56| No token visibility | Users don't know where to get tokens | 🔴 High | 57| Security: Can't revoke individual tokens | Compromised token affects all sessions | 🟡 Medium | 58 59--- 60 61## Problem Statement 62 63**Goal:** Enable users to obtain long-lived API tokens for MCP client authentication. 64 65**Requirements:** 661. Tokens must be long-lived (30-365 days or indefinite) 672. Users must be able to self-service generate tokens 683. Users must be able to revoke tokens independently 694. Tokens must be secure (not guessable, properly scoped) 705. System must integrate with existing `authorize()` function 716. Backward compatible with existing Auth0 token validation 72 73**Non-Goals:** 74- Replace Auth0 for web authentication 75- Implement OAuth2 server 76- Support token refresh flows 77 78--- 79 80## Proposed Solution: User-Managed API Tokens 81 82### Architecture Overview 83 84``` 85┌─────────────────────────────────────────────────────┐ 86│ User Flow │ 87├─────────────────────────────────────────────────────┤ 88│ │ 89│ 1. User logs in to aesthetic.computer (Auth0) │ 90│ 2. Visits /settings/api-tokens page │ 91│ 3. Clicks "Generate New Token" │ 92│ 4. Names token (e.g., "Claude Desktop") │ 93│ 5. Token displayed ONCE (must copy) │ 94│ 6. User adds token to MCP client config │ 95│ 7. MCP client sends: Authorization: Bearer ac_xxx │ 96│ 8. Server validates token → associates with user │ 97│ │ 98└─────────────────────────────────────────────────────┘ 99``` 100 101### Token Format 102 103``` 104ac_live_<32 random alphanumeric chars> 105 106Examples: 107- ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6 108- ac_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 109``` 110 111**Rationale:** 112- `ac_` prefix identifies as aesthetic.computer token 113- `live_` indicates production environment (future: `test_` for dev) 114- 32 chars = ~191 bits entropy (cryptographically secure) 115- Alphanumeric only (no special chars for easy copy/paste) 116 117### Database Schema 118 119**Collection:** `api_tokens` 120 121```javascript 122{ 123 _id: "ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6", // The token itself 124 user: "auth0|123456789", // User ID (sub) 125 name: "Claude Desktop", // User-provided name 126 created: ISODate("2026-02-12T10:30:00Z"), 127 lastUsed: ISODate("2026-02-12T15:45:00Z"), // Updated on each use 128 scopes: ["publish"], // Future: ["publish", "read", "admin"] 129 revoked: false, // Soft delete 130 revokedAt: null, // When revoked (if applicable) 131 metadata: { // Optional tracking 132 ip: "192.168.1.1", 133 userAgent: "Claude Desktop/1.0", 134 } 135} 136``` 137 138**Indexes:** 139```javascript 140// Primary lookup (most frequent query) 141{ _id: 1 } // Automatic 142 143// User lookup (for token list page) 144{ user: 1, revoked: 1 } 145 146// Cleanup queries 147{ revoked: 1, revokedAt: 1 } 148{ lastUsed: 1 } 149``` 150 151--- 152 153## API Endpoints 154 155### 1. Generate Token 156 157**Endpoint:** `POST /api/tokens/generate` 158 159**Authentication:** Required (Auth0 session) 160 161**Request:** 162```json 163{ 164 "name": "Claude Desktop" 165} 166``` 167 168**Response:** 169```json 170{ 171 "success": true, 172 "token": "ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6", 173 "name": "Claude Desktop", 174 "created": "2026-02-12T10:30:00Z", 175 "warning": "This token will only be shown once. Copy it now!" 176} 177``` 178 179**Error Cases:** 180- 401: Not authenticated 181- 429: Rate limit (max 10 tokens per user) 182 183--- 184 185### 2. List Tokens 186 187**Endpoint:** `GET /api/tokens/list` 188 189**Authentication:** Required (Auth0 session) 190 191**Response:** 192```json 193{ 194 "tokens": [ 195 { 196 "id": "ac_live_8k3j...", 197 "name": "Claude Desktop", 198 "created": "2026-02-12T10:30:00Z", 199 "lastUsed": "2026-02-12T15:45:00Z", 200 "preview": "ac_live_8k3j...x4y6" // First 12 + last 4 chars 201 }, 202 { 203 "id": "ac_live_a1b2...", 204 "name": "ChatGPT", 205 "created": "2026-02-10T08:00:00Z", 206 "lastUsed": "2026-02-12T12:00:00Z", 207 "preview": "ac_live_a1b2...o5p6" 208 } 209 ] 210} 211``` 212 213--- 214 215### 3. Revoke Token 216 217**Endpoint:** `DELETE /api/tokens/revoke/:tokenId` 218 219**Authentication:** Required (Auth0 session, must own token) 220 221**Response:** 222```json 223{ 224 "success": true, 225 "message": "Token 'Claude Desktop' has been revoked" 226} 227``` 228 229**Error Cases:** 230- 401: Not authenticated 231- 403: Token belongs to different user 232- 404: Token not found 233 234--- 235 236## Code Changes 237 238### 1. Update `authorization.mjs` 239 240**Current code:** 241```javascript 242export async function authorize({ authorization }, tenant = "aesthetic") { 243 try { 244 const { got } = await import("got"); 245 const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; 246 shell.log(`🔐 Attempting to authorize \`${tenant}\` user...`); 247 const result = ( 248 await got(`${baseURI}/userinfo`, { 249 headers: { Authorization: authorization }, 250 responseType: "json", 251 }) 252 ).body; 253 // ... 254 } 255} 256``` 257 258**New code:** 259```javascript 260export async function authorize({ authorization }, tenant = "aesthetic") { 261 const token = authorization?.replace("Bearer ", ""); 262 263 // 🆕 Check if it's an API token (starts with "ac_") 264 if (token?.startsWith("ac_live_") || token?.startsWith("ac_test_")) { 265 return await validateApiToken(token); 266 } 267 268 // Otherwise, validate as Auth0 token (existing logic) 269 try { 270 const { got } = await import("got"); 271 const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI; 272 shell.log(`🔐 Attempting to authorize \`${tenant}\` user...`); 273 const result = ( 274 await got(`${baseURI}/userinfo`, { 275 headers: { Authorization: authorization }, 276 responseType: "json", 277 }) 278 ).body; 279 // ... 280 } 281} 282 283// 🆕 New function 284async function validateApiToken(token) { 285 const database = await connect(); 286 const collection = database.db.collection("api_tokens"); 287 288 const tokenDoc = await collection.findOne({ 289 _id: token, 290 revoked: false 291 }); 292 293 if (!tokenDoc) { 294 await database.disconnect(); 295 return undefined; 296 } 297 298 // Update lastUsed timestamp (fire and forget) 299 collection.updateOne( 300 { _id: token }, 301 { $set: { lastUsed: new Date() } } 302 ).catch(err => shell.error("Failed to update token lastUsed:", err)); 303 304 await database.disconnect(); 305 306 // Return user object in same format as Auth0 307 return { 308 sub: tokenDoc.user, 309 email_verified: true, // Assume verified (token was generated by logged-in user) 310 source: "api_token", 311 token_name: tokenDoc.name, 312 }; 313} 314``` 315 316--- 317 318### 2. Create New Netlify Functions 319 320**Files to create:** 321- `system/netlify/functions/api-token-generate.mjs` 322- `system/netlify/functions/api-token-list.mjs` 323- `system/netlify/functions/api-token-revoke.mjs` 324 325**Add to `netlify.toml`:** 326```toml 327[[redirects]] 328from = "/api/tokens/*" 329to = "/.netlify/functions/api-token-:splat" 330status = 200 331``` 332 333--- 334 335### 3. Create Web UI 336 337**New piece:** `@api-tokens` (or add to existing settings) 338 339**Features:** 340- List existing tokens with preview, creation date, last used 341- "Generate New Token" button 342- Modal to name token 343- One-time token display with copy button 344- Revoke button for each token 345- Empty state for no tokens 346 347**Example UI (text-based for piece):** 348 349``` 350╔══════════════════════════════════════════════════╗ 351║ API Tokens for MCP Clients ║ 352╠══════════════════════════════════════════════════╣ 353║ ║ 354║ Claude Desktop ║ 355║ Token: ac_live_8k3j...x4y6 ║ 356║ Created: Feb 12, 2026 ║ 357║ Last used: 2 hours ago ║ 358║ [Revoke] ║ 359║ ║ 360║ ────────────────────────────────────────────── ║ 361║ ║ 362║ ChatGPT ║ 363║ Token: ac_live_a1b2...o5p6 ║ 364║ Created: Feb 10, 2026 ║ 365║ Last used: 5 minutes ago ║ 366║ [Revoke] ║ 367║ ║ 368║ ────────────────────────────────────────────── ║ 369║ ║ 370║ [+ Generate New Token] ║ 371║ ║ 372╚══════════════════════════════════════════════════╝ 373``` 374 375--- 376 377## Security Considerations 378 379### Token Generation 380- Use `crypto.randomBytes(32)` for secure random generation 381- Hash tokens before comparing? **No** - tokens are stored as-is (like API keys) 382- Tokens are secrets - never log full tokens 383 384### Token Storage 385- Store tokens as document IDs in MongoDB (no hashing needed) 386- Index on `_id` for O(1) lookup 387- Add TTL index for automatic cleanup of old revoked tokens 388 389### Rate Limiting 390- Max 10 active tokens per user 391- Rate limit token generation: 5 requests/hour per user 392- Rate limit API calls: 1000 requests/hour per token 393 394### Token Revocation 395- Soft delete (set `revoked: true`) 396- Allow user to view revoked tokens for audit log 397- Cleanup old revoked tokens after 90 days (TTL index) 398 399### Scope Management 400- All tokens start with `["publish"]` scope 401- Future: Add granular scopes like `["read", "publish:pieces", "publish:kidlisp"]` 402- Validate scopes in each endpoint 403 404--- 405 406## User Experience Flow 407 408### Happy Path 409 410``` 4111. User visits aesthetic.computer 4122. Clicks profile → "API Tokens" or "Settings" 4133. Sees empty state: "No API tokens yet" 4144. Clicks "Generate New Token" 4155. Modal appears: "Name this token (e.g., Claude Desktop)" 4166. User enters "Claude Desktop" and clicks "Generate" 4177. Success modal shows token ONE TIME: 418 419 ┌─────────────────────────────────────────────┐ 420 │ ✅ Token Generated │ 421 ├─────────────────────────────────────────────┤ 422 │ │ 423 │ Token: ac_live_8k3jf9d2l4m6n8p0q2r4s6... │ 424 │ [Copy to Clipboard] │ 425 │ │ 426 │ ⚠️ This token will only be shown once. │ 427 │ Copy it now and store it securely! │ 428 │ │ 429 │ Add to your MCP client: │ 430 │ │ 431 │ { │ 432 │ "mcpServers": { │ 433 │ "aesthetic-computer": { │ 434 │ "command": "npx", │ 435 │ "args": ["-y", "@aesthetic.compu... │ 436 │ "env": { │ 437 │ "AC_TOKEN": "ac_live_8k3jf9d2..." │ 438 │ } │ 439 │ } │ 440 │ } │ 441 │ } │ 442 │ │ 443 │ [I've Saved It] [Download Config] │ 444 └─────────────────────────────────────────────┘ 445 4468. User copies token 4479. Token appears in list (masked) 44810. User adds to MCP client config 44911. Publishing now associates with user account 450``` 451 452### Error Cases 453 454**Token Limit Reached:** 455``` 456❌ Token limit reached (10/10) 457 You must revoke an existing token before creating a new one. 458``` 459 460**Unauthorized Revoke Attempt:** 461``` 462❌ Permission denied 463 This token belongs to a different user. 464``` 465 466**Token Already Revoked:** 467``` 468⚠️ Token already revoked 469 This token was revoked on Feb 10, 2026. 470``` 471 472--- 473 474## Implementation Checklist 475 476### Phase 1: Backend (Estimated: 4-6 hours) 477- [ ] Update `authorization.mjs` with API token validation 478- [ ] Create token generation utility (crypto randomness) 479- [ ] Create `api-token-generate.mjs` Netlify function 480- [ ] Create `api-token-list.mjs` Netlify function 481- [ ] Create `api-token-revoke.mjs` Netlify function 482- [ ] Add MongoDB indexes to `api_tokens` collection 483- [ ] Add rate limiting middleware 484- [ ] Update `netlify.toml` with new routes 485- [ ] Write unit tests for token validation 486 487### Phase 2: Frontend (Estimated: 6-8 hours) 488- [ ] Create `@api-tokens` piece or integrate into settings 489- [ ] Implement token list UI 490- [ ] Implement token generation modal 491- [ ] Implement one-time token display with copy button 492- [ ] Implement token revocation with confirmation 493- [ ] Add empty state UI 494- [ ] Add loading states and error handling 495- [ ] Add usage instructions and documentation links 496 497### Phase 3: Documentation (Estimated: 2 hours) 498- [ ] Update MCP README with token generation instructions 499- [ ] Update website docs with token management guide 500- [ ] Add troubleshooting section 501- [ ] Create video walkthrough (optional) 502 503### Phase 4: Testing & Launch (Estimated: 2-3 hours) 504- [ ] Test token generation flow 505- [ ] Test token validation in MCP publishing 506- [ ] Test token revocation 507- [ ] Test rate limiting 508- [ ] Test concurrent token usage 509- [ ] Deploy to production 510- [ ] Monitor logs for errors 511- [ ] Announce feature to users 512 513**Total Estimated Time:** 14-19 hours 514 515--- 516 517## Alternative Approaches Considered 518 519### Option A: Extend Auth0 Token Expiry 520**Pros:** 521- No new infrastructure 522- Reuses existing auth flow 523 524**Cons:** 525- Auth0 token limits (max ~30 days) 526- Can't revoke individual tokens 527- More expensive (Auth0 pricing) 528- Less user control 529 530**Verdict:** ❌ Not recommended 531 532--- 533 534### Option B: Simple Token Page (Auth0 tokens) 535**Pros:** 536- Very quick to implement (1-2 hours) 537- No database changes needed 538 539**Cons:** 540- Tokens still expire after 24 hours 541- Users must re-authenticate frequently 542- Poor UX for MCP clients 543 544**Verdict:** ⚠️ Good for MVP, but not long-term solution 545 546**Implementation:** 547```javascript 548// GET /api/my-token 549export async function handler(event) { 550 const user = await authorize(event.headers); 551 if (!user) return respond(401, { error: "Unauthorized" }); 552 553 // Return the Auth0 token that was just validated 554 const token = event.headers.authorization?.replace("Bearer ", ""); 555 return respond(200, { token, expires: "24 hours" }); 556} 557``` 558 559--- 560 561### Option C: OAuth2 Device Flow 562**Pros:** 563- Industry standard 564- Good for CLI tools 565- Handles refresh tokens 566 567**Cons:** 568- Complex implementation 569- Overkill for simple use case 570- Still requires user interaction 571 572**Verdict:** ❌ Over-engineered 573 574--- 575 576## Migration Strategy 577 578### Backward Compatibility 579 580The proposed solution is **100% backward compatible**: 581 5821. Existing Auth0 tokens continue to work 5832. No changes to existing API contracts 5843. New token format is distinct (`ac_` prefix) 5854. Anonymous publishing still works without any token 586 587### Rollout Plan 588 589**Week 1: Soft Launch** 590- Deploy backend changes 591- Create token management UI 592- Announce to beta testers only 593- Monitor for issues 594 595**Week 2: Documentation** 596- Update all MCP documentation 597- Create video tutorials 598- Add in-app help tooltips 599 600**Week 3: Public Launch** 601- Announce via social media 602- Post in Discord/community channels 603- Update registry metadata if needed 604 605**Week 4: Monitoring** 606- Track token generation rate 607- Monitor API performance impact 608- Gather user feedback 609- Iterate on UX 610 611--- 612 613## Success Metrics 614 615### Key Performance Indicators (KPIs) 616 617| Metric | Target | How to Measure | 618|--------|--------|---------------| 619| Token generation rate | 100+ tokens/week | MongoDB query count | 620| Token usage rate | 80%+ tokens used within 7 days | Check `lastUsed` field | 621| Revocation rate | <5% tokens revoked within first month | Track revocations | 622| Support tickets | <10 token-related tickets/month | Support system | 623| MCP publishing auth rate | 30%+ of publishes authenticated | Compare anon vs auth | 624 625### Success Criteria 626 627- [ ] Users can generate tokens without support help 628- [ ] Token validation adds <50ms latency to API calls 629- [ ] Zero security incidents related to tokens 630- [ ] Positive user feedback on token management UX 631- [ ] Increased rate of authenticated (non-anonymous) publishing 632 633--- 634 635## Open Questions 636 6371. **Token expiry:** Should tokens expire after inactivity (e.g., 1 year unused)? 638 - **Recommendation:** Yes, expire after 1 year of inactivity. Send email warning at 11 months. 639 6402. **Token naming:** Should we enforce unique token names per user? 641 - **Recommendation:** No, allow duplicates. Users might want "Claude Desktop" on multiple machines. 642 6433. **Token export:** Should users be able to export token list (without secrets)? 644 - **Recommendation:** Yes, add "Export to CSV" for audit logs. 645 6464. **Token transfer:** Should tokens be transferable between accounts? 647 - **Recommendation:** No, security risk. Users must generate new tokens. 648 6495. **Notification:** Should users get notified when their token is used from new IP? 650 - **Recommendation:** Phase 2 feature. Not critical for MVP. 651 652--- 653 654## Appendix: Example Code Snippets 655 656### Token Generation (Cryptographic) 657 658```javascript 659import crypto from 'crypto'; 660 661export function generateApiToken() { 662 const randomBytes = crypto.randomBytes(32); 663 const base62 = randomBytes.toString('base64') 664 .replace(/\+/g, '') 665 .replace(/\//g, '') 666 .replace(/=/g, '') 667 .slice(0, 32); 668 669 return `ac_live_${base62}`; 670} 671 672// Example output: ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6 673``` 674 675### Token Validation (Fast Path) 676 677```javascript 678export async function validateApiToken(token) { 679 // Early return for invalid format 680 if (!token || !token.startsWith('ac_')) { 681 return undefined; 682 } 683 684 const database = await connect(); 685 const collection = database.db.collection('api_tokens'); 686 687 // Single query with projection (only fetch needed fields) 688 const tokenDoc = await collection.findOne( 689 { _id: token, revoked: false }, 690 { projection: { user: 1, name: 1, scopes: 1 } } 691 ); 692 693 if (!tokenDoc) { 694 await database.disconnect(); 695 return undefined; 696 } 697 698 // Fire-and-forget update (don't await) 699 collection.updateOne( 700 { _id: token }, 701 { $set: { lastUsed: new Date() } } 702 ).catch(() => {}); // Silently fail 703 704 await database.disconnect(); 705 706 return { 707 sub: tokenDoc.user, 708 email_verified: true, 709 source: 'api_token', 710 token_name: tokenDoc.name, 711 scopes: tokenDoc.scopes || ['publish'], 712 }; 713} 714``` 715 716### Rate Limiting Middleware 717 718```javascript 719import * as KeyValue from "./kv.mjs"; 720 721export async function checkRateLimit(userId, action, limit, window) { 722 const key = `ratelimit:${action}:${userId}`; 723 await KeyValue.connect(); 724 725 const count = await KeyValue.get(key) || 0; 726 727 if (count >= limit) { 728 await KeyValue.disconnect(); 729 return { allowed: false, remaining: 0 }; 730 } 731 732 // Increment counter 733 await KeyValue.incr(key); 734 await KeyValue.expire(key, window); // TTL in seconds 735 736 await KeyValue.disconnect(); 737 738 return { allowed: true, remaining: limit - count - 1 }; 739} 740 741// Usage: 742const rateLimit = await checkRateLimit(user.sub, 'token_generate', 5, 3600); // 5 per hour 743if (!rateLimit.allowed) { 744 return respond(429, { error: "Rate limit exceeded. Try again later." }); 745} 746``` 747 748--- 749 750## Conclusion 751 752The proposed API token system provides a secure, user-friendly way for users to authenticate their MCP publishing. The implementation is straightforward, backward compatible, and follows industry best practices. 753 754**Recommended Next Steps:** 7551. Review and approve this design document 7562. Create implementation tickets 7573. Begin Phase 1 (Backend) development 7584. Iterate based on beta tester feedback 759 760**Estimated Launch Date:** 2-3 weeks from approval 761 762--- 763 764**Questions or Feedback?** 765Contact: @jeffrey on aesthetic.computer 766GitHub Issues: https://github.com/whistlegraph/aesthetic-computer/issues