Monorepo for Aesthetic.Computer aesthetic.computer

API Token System for MCP Authentication#

Status: Design Phase Date: 2026-02-12 Author: Claude (via @jeffrey)


Executive Summary#

To 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.

Recommended Solution: Implement user-managed API tokens with web UI for generation/revocation.


Current State Analysis#

Authentication Flow (as of 2026.02.12)#

User Login (Web)
    ↓
  Auth0 OAuth2
    ↓
Access Token (24hr expiry)
    ↓
Bearer Token in Authorization header
    ↓
validate via /userinfo endpoint

Existing Code Components#

  1. system/backend/authorization.mjs

    • authorize() function validates Bearer tokens via Auth0
    • Calls https://aesthetic.us.auth0.com/userinfo
    • Returns user object with sub (user ID) and email
  2. system/netlify/functions/auth-cli-callback.mjs

    • Handles OAuth callback for CLI tools
    • Returns access_token to authenticated clients
    • Used for temporary CLI authentication
  3. Publishing Endpoints

    • store-piece.mjs, store-kidlisp.mjs, store-clock.mjs
    • All support optional Bearer token authentication
    • Anonymous publishing works without token

Current Limitations#

Issue Impact Priority
Short-lived tokens (24hr) Users must re-authenticate daily 🔴 High
No token management UI Users can't generate/revoke tokens 🔴 High
No token visibility Users don't know where to get tokens 🔴 High
Security: Can't revoke individual tokens Compromised token affects all sessions 🟡 Medium

Problem Statement#

Goal: Enable users to obtain long-lived API tokens for MCP client authentication.

Requirements:

  1. Tokens must be long-lived (30-365 days or indefinite)
  2. Users must be able to self-service generate tokens
  3. Users must be able to revoke tokens independently
  4. Tokens must be secure (not guessable, properly scoped)
  5. System must integrate with existing authorize() function
  6. Backward compatible with existing Auth0 token validation

Non-Goals:

  • Replace Auth0 for web authentication
  • Implement OAuth2 server
  • Support token refresh flows

Proposed Solution: User-Managed API Tokens#

Architecture Overview#

┌─────────────────────────────────────────────────────┐
│  User Flow                                          │
├─────────────────────────────────────────────────────┤
│                                                     │
│  1. User logs in to aesthetic.computer (Auth0)     │
│  2. Visits /settings/api-tokens page                │
│  3. Clicks "Generate New Token"                     │
│  4. Names token (e.g., "Claude Desktop")            │
│  5. Token displayed ONCE (must copy)                │
│  6. User adds token to MCP client config            │
│  7. MCP client sends: Authorization: Bearer ac_xxx  │
│  8. Server validates token → associates with user   │
│                                                     │
└─────────────────────────────────────────────────────┘

Token Format#

ac_live_<32 random alphanumeric chars>

Examples:
- ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6
- ac_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Rationale:

  • ac_ prefix identifies as aesthetic.computer token
  • live_ indicates production environment (future: test_ for dev)
  • 32 chars = ~191 bits entropy (cryptographically secure)
  • Alphanumeric only (no special chars for easy copy/paste)

Database Schema#

Collection: api_tokens

{
  _id: "ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6", // The token itself
  user: "auth0|123456789",                          // User ID (sub)
  name: "Claude Desktop",                           // User-provided name
  created: ISODate("2026-02-12T10:30:00Z"),
  lastUsed: ISODate("2026-02-12T15:45:00Z"),        // Updated on each use
  scopes: ["publish"],                              // Future: ["publish", "read", "admin"]
  revoked: false,                                   // Soft delete
  revokedAt: null,                                  // When revoked (if applicable)
  metadata: {                                       // Optional tracking
    ip: "192.168.1.1",
    userAgent: "Claude Desktop/1.0",
  }
}

Indexes:

// Primary lookup (most frequent query)
{ _id: 1 }  // Automatic

// User lookup (for token list page)
{ user: 1, revoked: 1 }

// Cleanup queries
{ revoked: 1, revokedAt: 1 }
{ lastUsed: 1 }

API Endpoints#

1. Generate Token#

Endpoint: POST /api/tokens/generate

Authentication: Required (Auth0 session)

Request:

{
  "name": "Claude Desktop"
}

Response:

{
  "success": true,
  "token": "ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6",
  "name": "Claude Desktop",
  "created": "2026-02-12T10:30:00Z",
  "warning": "This token will only be shown once. Copy it now!"
}

Error Cases:

  • 401: Not authenticated
  • 429: Rate limit (max 10 tokens per user)

2. List Tokens#

Endpoint: GET /api/tokens/list

Authentication: Required (Auth0 session)

Response:

{
  "tokens": [
    {
      "id": "ac_live_8k3j...",
      "name": "Claude Desktop",
      "created": "2026-02-12T10:30:00Z",
      "lastUsed": "2026-02-12T15:45:00Z",
      "preview": "ac_live_8k3j...x4y6" // First 12 + last 4 chars
    },
    {
      "id": "ac_live_a1b2...",
      "name": "ChatGPT",
      "created": "2026-02-10T08:00:00Z",
      "lastUsed": "2026-02-12T12:00:00Z",
      "preview": "ac_live_a1b2...o5p6"
    }
  ]
}

3. Revoke Token#

Endpoint: DELETE /api/tokens/revoke/:tokenId

Authentication: Required (Auth0 session, must own token)

Response:

{
  "success": true,
  "message": "Token 'Claude Desktop' has been revoked"
}

Error Cases:

  • 401: Not authenticated
  • 403: Token belongs to different user
  • 404: Token not found

Code Changes#

1. Update authorization.mjs#

Current code:

export async function authorize({ authorization }, tenant = "aesthetic") {
  try {
    const { got } = await import("got");
    const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;
    shell.log(`🔐 Attempting to authorize \`${tenant}\` user...`);
    const result = (
      await got(`${baseURI}/userinfo`, {
        headers: { Authorization: authorization },
        responseType: "json",
      })
    ).body;
    // ...
  }
}

New code:

export async function authorize({ authorization }, tenant = "aesthetic") {
  const token = authorization?.replace("Bearer ", "");

  // 🆕 Check if it's an API token (starts with "ac_")
  if (token?.startsWith("ac_live_") || token?.startsWith("ac_test_")) {
    return await validateApiToken(token);
  }

  // Otherwise, validate as Auth0 token (existing logic)
  try {
    const { got } = await import("got");
    const baseURI = tenant === "aesthetic" ? aestheticBaseURI : sotceBaseURI;
    shell.log(`🔐 Attempting to authorize \`${tenant}\` user...`);
    const result = (
      await got(`${baseURI}/userinfo`, {
        headers: { Authorization: authorization },
        responseType: "json",
      })
    ).body;
    // ...
  }
}

// 🆕 New function
async function validateApiToken(token) {
  const database = await connect();
  const collection = database.db.collection("api_tokens");

  const tokenDoc = await collection.findOne({
    _id: token,
    revoked: false
  });

  if (!tokenDoc) {
    await database.disconnect();
    return undefined;
  }

  // Update lastUsed timestamp (fire and forget)
  collection.updateOne(
    { _id: token },
    { $set: { lastUsed: new Date() } }
  ).catch(err => shell.error("Failed to update token lastUsed:", err));

  await database.disconnect();

  // Return user object in same format as Auth0
  return {
    sub: tokenDoc.user,
    email_verified: true, // Assume verified (token was generated by logged-in user)
    source: "api_token",
    token_name: tokenDoc.name,
  };
}

2. Create New Netlify Functions#

Files to create:

  • system/netlify/functions/api-token-generate.mjs
  • system/netlify/functions/api-token-list.mjs
  • system/netlify/functions/api-token-revoke.mjs

Add to netlify.toml:

[[redirects]]
from = "/api/tokens/*"
to = "/.netlify/functions/api-token-:splat"
status = 200

3. Create Web UI#

New piece: @api-tokens (or add to existing settings)

Features:

  • List existing tokens with preview, creation date, last used
  • "Generate New Token" button
  • Modal to name token
  • One-time token display with copy button
  • Revoke button for each token
  • Empty state for no tokens

Example UI (text-based for piece):

╔══════════════════════════════════════════════════╗
║  API Tokens for MCP Clients                     ║
╠══════════════════════════════════════════════════╣
║                                                  ║
║  Claude Desktop                                  ║
║  Token: ac_live_8k3j...x4y6                     ║
║  Created: Feb 12, 2026                          ║
║  Last used: 2 hours ago                         ║
║  [Revoke]                                       ║
║                                                  ║
║  ──────────────────────────────────────────────  ║
║                                                  ║
║  ChatGPT                                        ║
║  Token: ac_live_a1b2...o5p6                     ║
║  Created: Feb 10, 2026                          ║
║  Last used: 5 minutes ago                       ║
║  [Revoke]                                       ║
║                                                  ║
║  ──────────────────────────────────────────────  ║
║                                                  ║
║  [+ Generate New Token]                         ║
║                                                  ║
╚══════════════════════════════════════════════════╝

Security Considerations#

Token Generation#

  • Use crypto.randomBytes(32) for secure random generation
  • Hash tokens before comparing? No - tokens are stored as-is (like API keys)
  • Tokens are secrets - never log full tokens

Token Storage#

  • Store tokens as document IDs in MongoDB (no hashing needed)
  • Index on _id for O(1) lookup
  • Add TTL index for automatic cleanup of old revoked tokens

Rate Limiting#

  • Max 10 active tokens per user
  • Rate limit token generation: 5 requests/hour per user
  • Rate limit API calls: 1000 requests/hour per token

Token Revocation#

  • Soft delete (set revoked: true)
  • Allow user to view revoked tokens for audit log
  • Cleanup old revoked tokens after 90 days (TTL index)

Scope Management#

  • All tokens start with ["publish"] scope
  • Future: Add granular scopes like ["read", "publish:pieces", "publish:kidlisp"]
  • Validate scopes in each endpoint

User Experience Flow#

Happy Path#

1. User visits aesthetic.computer
2. Clicks profile → "API Tokens" or "Settings"
3. Sees empty state: "No API tokens yet"
4. Clicks "Generate New Token"
5. Modal appears: "Name this token (e.g., Claude Desktop)"
6. User enters "Claude Desktop" and clicks "Generate"
7. Success modal shows token ONE TIME:

   ┌─────────────────────────────────────────────┐
   │  ✅ Token Generated                         │
   ├─────────────────────────────────────────────┤
   │                                             │
   │  Token: ac_live_8k3jf9d2l4m6n8p0q2r4s6...  │
   │         [Copy to Clipboard]                 │
   │                                             │
   │  ⚠️  This token will only be shown once.    │
   │     Copy it now and store it securely!      │
   │                                             │
   │  Add to your MCP client:                    │
   │                                             │
   │  {                                          │
   │    "mcpServers": {                          │
   │      "aesthetic-computer": {                │
   │        "command": "npx",                    │
   │        "args": ["-y", "@aesthetic.compu...  │
   │        "env": {                             │
   │          "AC_TOKEN": "ac_live_8k3jf9d2..."  │
   │        }                                    │
   │      }                                      │
   │    }                                        │
   │  }                                          │
   │                                             │
   │  [I've Saved It] [Download Config]          │
   └─────────────────────────────────────────────┘

8. User copies token
9. Token appears in list (masked)
10. User adds to MCP client config
11. Publishing now associates with user account

Error Cases#

Token Limit Reached:

❌ Token limit reached (10/10)
   You must revoke an existing token before creating a new one.

Unauthorized Revoke Attempt:

❌ Permission denied
   This token belongs to a different user.

Token Already Revoked:

⚠️  Token already revoked
   This token was revoked on Feb 10, 2026.

Implementation Checklist#

Phase 1: Backend (Estimated: 4-6 hours)#

  • Update authorization.mjs with API token validation
  • Create token generation utility (crypto randomness)
  • Create api-token-generate.mjs Netlify function
  • Create api-token-list.mjs Netlify function
  • Create api-token-revoke.mjs Netlify function
  • Add MongoDB indexes to api_tokens collection
  • Add rate limiting middleware
  • Update netlify.toml with new routes
  • Write unit tests for token validation

Phase 2: Frontend (Estimated: 6-8 hours)#

  • Create @api-tokens piece or integrate into settings
  • Implement token list UI
  • Implement token generation modal
  • Implement one-time token display with copy button
  • Implement token revocation with confirmation
  • Add empty state UI
  • Add loading states and error handling
  • Add usage instructions and documentation links

Phase 3: Documentation (Estimated: 2 hours)#

  • Update MCP README with token generation instructions
  • Update website docs with token management guide
  • Add troubleshooting section
  • Create video walkthrough (optional)

Phase 4: Testing & Launch (Estimated: 2-3 hours)#

  • Test token generation flow
  • Test token validation in MCP publishing
  • Test token revocation
  • Test rate limiting
  • Test concurrent token usage
  • Deploy to production
  • Monitor logs for errors
  • Announce feature to users

Total Estimated Time: 14-19 hours


Alternative Approaches Considered#

Option A: Extend Auth0 Token Expiry#

Pros:

  • No new infrastructure
  • Reuses existing auth flow

Cons:

  • Auth0 token limits (max ~30 days)
  • Can't revoke individual tokens
  • More expensive (Auth0 pricing)
  • Less user control

Verdict: ❌ Not recommended


Option B: Simple Token Page (Auth0 tokens)#

Pros:

  • Very quick to implement (1-2 hours)
  • No database changes needed

Cons:

  • Tokens still expire after 24 hours
  • Users must re-authenticate frequently
  • Poor UX for MCP clients

Verdict: ⚠️ Good for MVP, but not long-term solution

Implementation:

// GET /api/my-token
export async function handler(event) {
  const user = await authorize(event.headers);
  if (!user) return respond(401, { error: "Unauthorized" });

  // Return the Auth0 token that was just validated
  const token = event.headers.authorization?.replace("Bearer ", "");
  return respond(200, { token, expires: "24 hours" });
}

Option C: OAuth2 Device Flow#

Pros:

  • Industry standard
  • Good for CLI tools
  • Handles refresh tokens

Cons:

  • Complex implementation
  • Overkill for simple use case
  • Still requires user interaction

Verdict: ❌ Over-engineered


Migration Strategy#

Backward Compatibility#

The proposed solution is 100% backward compatible:

  1. Existing Auth0 tokens continue to work
  2. No changes to existing API contracts
  3. New token format is distinct (ac_ prefix)
  4. Anonymous publishing still works without any token

Rollout Plan#

Week 1: Soft Launch

  • Deploy backend changes
  • Create token management UI
  • Announce to beta testers only
  • Monitor for issues

Week 2: Documentation

  • Update all MCP documentation
  • Create video tutorials
  • Add in-app help tooltips

Week 3: Public Launch

  • Announce via social media
  • Post in Discord/community channels
  • Update registry metadata if needed

Week 4: Monitoring

  • Track token generation rate
  • Monitor API performance impact
  • Gather user feedback
  • Iterate on UX

Success Metrics#

Key Performance Indicators (KPIs)#

Metric Target How to Measure
Token generation rate 100+ tokens/week MongoDB query count
Token usage rate 80%+ tokens used within 7 days Check lastUsed field
Revocation rate <5% tokens revoked within first month Track revocations
Support tickets <10 token-related tickets/month Support system
MCP publishing auth rate 30%+ of publishes authenticated Compare anon vs auth

Success Criteria#

  • Users can generate tokens without support help
  • Token validation adds <50ms latency to API calls
  • Zero security incidents related to tokens
  • Positive user feedback on token management UX
  • Increased rate of authenticated (non-anonymous) publishing

Open Questions#

  1. Token expiry: Should tokens expire after inactivity (e.g., 1 year unused)?

    • Recommendation: Yes, expire after 1 year of inactivity. Send email warning at 11 months.
  2. Token naming: Should we enforce unique token names per user?

    • Recommendation: No, allow duplicates. Users might want "Claude Desktop" on multiple machines.
  3. Token export: Should users be able to export token list (without secrets)?

    • Recommendation: Yes, add "Export to CSV" for audit logs.
  4. Token transfer: Should tokens be transferable between accounts?

    • Recommendation: No, security risk. Users must generate new tokens.
  5. Notification: Should users get notified when their token is used from new IP?

    • Recommendation: Phase 2 feature. Not critical for MVP.

Appendix: Example Code Snippets#

Token Generation (Cryptographic)#

import crypto from 'crypto';

export function generateApiToken() {
  const randomBytes = crypto.randomBytes(32);
  const base62 = randomBytes.toString('base64')
    .replace(/\+/g, '')
    .replace(/\//g, '')
    .replace(/=/g, '')
    .slice(0, 32);

  return `ac_live_${base62}`;
}

// Example output: ac_live_8k3jf9d2l4m6n8p0q2r4s6t8v0w2x4y6

Token Validation (Fast Path)#

export async function validateApiToken(token) {
  // Early return for invalid format
  if (!token || !token.startsWith('ac_')) {
    return undefined;
  }

  const database = await connect();
  const collection = database.db.collection('api_tokens');

  // Single query with projection (only fetch needed fields)
  const tokenDoc = await collection.findOne(
    { _id: token, revoked: false },
    { projection: { user: 1, name: 1, scopes: 1 } }
  );

  if (!tokenDoc) {
    await database.disconnect();
    return undefined;
  }

  // Fire-and-forget update (don't await)
  collection.updateOne(
    { _id: token },
    { $set: { lastUsed: new Date() } }
  ).catch(() => {}); // Silently fail

  await database.disconnect();

  return {
    sub: tokenDoc.user,
    email_verified: true,
    source: 'api_token',
    token_name: tokenDoc.name,
    scopes: tokenDoc.scopes || ['publish'],
  };
}

Rate Limiting Middleware#

import * as KeyValue from "./kv.mjs";

export async function checkRateLimit(userId, action, limit, window) {
  const key = `ratelimit:${action}:${userId}`;
  await KeyValue.connect();

  const count = await KeyValue.get(key) || 0;

  if (count >= limit) {
    await KeyValue.disconnect();
    return { allowed: false, remaining: 0 };
  }

  // Increment counter
  await KeyValue.incr(key);
  await KeyValue.expire(key, window); // TTL in seconds

  await KeyValue.disconnect();

  return { allowed: true, remaining: limit - count - 1 };
}

// Usage:
const rateLimit = await checkRateLimit(user.sub, 'token_generate', 5, 3600); // 5 per hour
if (!rateLimit.allowed) {
  return respond(429, { error: "Rate limit exceeded. Try again later." });
}

Conclusion#

The 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.

Recommended Next Steps:

  1. Review and approve this design document
  2. Create implementation tickets
  3. Begin Phase 1 (Backend) development
  4. Iterate based on beta tester feedback

Estimated Launch Date: 2-3 weeks from approval


Questions or Feedback? Contact: @jeffrey on aesthetic.computer GitHub Issues: https://github.com/whistlegraph/aesthetic-computer/issues