Monorepo for Aesthetic.Computer
aesthetic.computer
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