- Update tests to handle async session initialization - Fix session ID retrieval from SSE/JSON responses - Update DELETE request tests to match new behavior - Remove references to deprecated startServerListeningToRedis - Add proper waits for async operations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
11 KiB
User ID System Documentation
Overview
The MCP server implements a comprehensive user identification and session ownership system that ensures secure multi-user access to MCP resources. This system integrates localStorage-based user management, OAuth authentication flows, and Redis-backed session isolation.
Architecture Components
1. User ID Management (localStorage)
2. OAuth Authorization Flow
3. Redis Session Ownership
4. Session Access Validation
1. User ID Management (localStorage)
The fake upstream authentication system uses browser localStorage to manage user identities for testing and development purposes.
localStorage Schema
// Stored in browser localStorage
{
"mcpUserId": "550e8400-e29b-41d4-a716-446655440000" // UUID v4
}
User ID Generation Flow
sequenceDiagram
participant Browser
participant LocalStorage
participant AuthPage as Fake Auth Page
Browser->>AuthPage: Load /fakeupstreamauth/authorize
AuthPage->>LocalStorage: Check mcpUserId
alt User ID exists
LocalStorage-->>AuthPage: Return existing UUID
AuthPage->>Browser: Display existing ID
else User ID missing
AuthPage->>AuthPage: Generate new UUID v4
AuthPage->>LocalStorage: Store new mcpUserId
AuthPage->>Browser: Display new ID
end
Note over AuthPage: User can edit or regenerate ID
AuthPage->>LocalStorage: Update mcpUserId (if changed)
User ID Operations
| Operation | Description | Implementation |
|---|---|---|
| Generate | Create new UUID v4 | generateUUID() function |
| Retrieve | Get existing or create new | getUserId() function |
| Update | Edit existing ID | editUserId() function |
| Persist | Store in localStorage | localStorage.setItem('mcpUserId', userId) |
2. OAuth Authorization Flow
The OAuth flow integrates user IDs from localStorage into the MCP authorization process.
Complete OAuth Flow with User ID
sequenceDiagram
participant Client
participant MCPServer as MCP Server
participant AuthPage as Auth Page
participant FakeAuth as Fake Upstream Auth
participant LocalStorage
participant Redis
Client->>MCPServer: Request authorization
MCPServer->>AuthPage: Redirect to auth page
AuthPage->>Client: Show MCP authorization page
Client->>FakeAuth: Click "Continue to Authentication"
FakeAuth->>LocalStorage: Get/Create userId
LocalStorage-->>FakeAuth: Return userId
FakeAuth->>Client: Show userId management UI
Client->>FakeAuth: Complete authentication
FakeAuth->>MCPServer: Redirect with code + userId
MCPServer->>Redis: Store userId in McpInstallation
MCPServer->>Client: Return access token
Note over Redis: McpInstallation.userId = userId
OAuth Data Flow
graph TD
A[Browser localStorage] -->|userId| B[Fake Auth Page]
B -->|userId in query params| C[Authorization Callback]
C -->|userId| D[McpInstallation Object]
D -->|access_token| E[Redis Storage]
E -->|AuthInfo.extra.userId| F[Session Ownership]
Authorization Code Exchange
The userId is embedded in the authorization flow:
// In fake auth page
function authorize() {
const userId = getUserId(); // From localStorage
const url = new URL(redirectUri);
url.searchParams.set('userId', userId);
url.searchParams.set('code', 'fakecode');
window.location.href = url.toString();
}
3. Redis Session Ownership
Redis stores session ownership information using a structured key system.
Redis Key Structure
session:{sessionId}:owner → userId # Session ownership
mcp:shttp:toserver:{sessionId} → [pub/sub channel] # Client→Server messages (also indicates liveness)
mcp:shttp:toclient:{sessionId}:{requestId} → [pub/sub channel] # Server→Client responses
mcp:control:{sessionId} → [pub/sub channel] # Control messages
Redis Operations
| Operation | Key Pattern | Value | Purpose |
|---|---|---|---|
| Set Owner | session:{sessionId}:owner |
userId |
Store session owner |
| Get Owner | session:{sessionId}:owner |
userId |
Retrieve session owner |
| Check Live | mcp:shttp:toserver:{sessionId} |
numsub > 0 |
Check if session active via pub/sub subscribers |
Session Liveness Mechanism
Session liveness is determined by pub/sub subscription count rather than explicit keys:
graph TD
A[MCP Server Starts] --> B[Subscribe to mcp:shttp:toserver:sessionId]
B --> C[numsub = 1 → Session is LIVE]
C --> D[Session Processing]
D --> E[MCP Server Shutdown]
E --> F[Unsubscribe from channel]
F --> G[numsub = 0 → Session is DEAD]
H[isLive() function] --> I[Check numsub count]
I --> J{numsub > 0?}
J -->|Yes| K[Session is Live]
J -->|No| L[Session is Dead]
Why this works:
- When an MCP server starts, it subscribes to
mcp:shttp:toserver:{sessionId} - When it shuts down (gracefully or crashes), Redis automatically removes the subscription
numsubreflects the actual state without requiring explicit cleanup
Session Ownership Functions
// Core ownership functions
export async function setSessionOwner(sessionId: string, userId: string): Promise<void>
export async function getSessionOwner(sessionId: string): Promise<string | null>
export async function validateSessionOwnership(sessionId: string, userId: string): Promise<boolean>
export async function isSessionOwnedBy(sessionId: string, userId: string): Promise<boolean>
export async function isLive(sessionId: string): Promise<boolean> // Uses numsub count
4. Session Access Validation
Session access is validated at multiple points in the request lifecycle.
Session Validation Flow
sequenceDiagram
participant Client
participant Handler as shttp Handler
participant Auth as Auth Middleware
participant Redis
Client->>Handler: MCP Request with session-id
Handler->>Auth: Extract userId from token
Auth-->>Handler: Return userId
alt New Session (Initialize)
Handler->>Handler: Generate new sessionId
Handler->>Redis: setSessionOwner(sessionId, userId)
Handler->>Handler: Start MCP server (subscribes to channel)
Note over Handler: Session becomes "live" via pub/sub subscription
Handler->>Client: Return with new session
else Existing Session
Handler->>Redis: isSessionOwnedBy(sessionId, userId)
Redis-->>Handler: Return ownership status
alt Session Owned by User
Handler->>Client: Process request
else Session Not Owned
Handler->>Client: 400 Bad Request
end
end
DELETE Request Validation
sequenceDiagram
participant Client
participant Handler as shttp Handler
participant Redis
Client->>Handler: DELETE /mcp (session-id: xyz)
Handler->>Handler: Extract userId from auth
Handler->>Redis: isSessionOwnedBy(sessionId, userId)
alt Session Owned by User
Redis-->>Handler: true
Handler->>Redis: shutdownSession(sessionId)
Handler->>Client: 200 OK (Session terminated)
else Session Not Owned
Redis-->>Handler: false
Handler->>Client: 404 Not Found (Session not found or access denied)
end
Request Authorization Matrix
| Request Type | Session ID | User ID | Authorization Check |
|---|---|---|---|
| Initialize | None | Required | Create new session |
| Existing Session | Required | Required | isSessionOwnedBy() |
| DELETE Session | Required | Required | isSessionOwnedBy() |
5. Security Model
Multi-User Isolation
graph TB
subgraph "User A"
A1[localStorage: userA-id]
A2[Session: session-A]
A3[Redis: session:A:owner → userA]
end
subgraph "User B"
B1[localStorage: userB-id]
B2[Session: session-B]
B3[Redis: session:B:owner → userB]
end
subgraph "Redis Isolation"
R1[session:A:owner → userA-id]
R2[session:B:owner → userB-id]
R3[Ownership Validation]
end
A3 --> R1
B3 --> R2
R1 --> R3
R2 --> R3
Security Guarantees
- Session Isolation: Users can only access sessions they own
- Identity Verification: User ID is validated from authenticated token
- Ownership Persistence: Session ownership is stored in Redis
- Access Control: All session operations validate ownership
- Secure Cleanup: DELETE operations verify ownership before termination
Attack Prevention
| Attack Vector | Prevention | Implementation |
|---|---|---|
| Session Hijacking | Ownership validation | isSessionOwnedBy() check |
| Cross-User Access | User ID verification | Extract userId from AuthInfo |
| Session Spoofing | Token validation | Bearer token middleware |
| Unauthorized DELETE | Ownership check | Validate before shutdown |
6. Implementation Details
Error Handling
// Session access errors
if (!userId) {
return 401; // Unauthorized: User ID required
}
if (!await isSessionOwnedBy(sessionId, userId)) {
return 400; // Bad Request: Session access denied
}
Testing Strategy
The system includes comprehensive tests for:
- User session isolation: Users cannot access other users' sessions
- DELETE request validation: Only owners can delete sessions
- Redis cleanup: Proper cleanup of ownership data
- Auth flow integration: User ID propagation through OAuth
Performance Considerations
- Redis Efficiency: O(1) lookups for session ownership
- Session Reuse: Existing sessions are reused when ownership matches
- Cleanup: Automatic cleanup prevents resource leaks
- Caching: Session ownership is cached in Redis
7. Configuration
Environment Variables
# Redis configuration for session storage
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=optional
# Base URI for OAuth redirects
BASE_URI=http://localhost:3000
Development Testing
# Run multi-user tests
npm test -- --testNamePattern="User Session Isolation"
# Test session ownership
npm test -- --testNamePattern="session ownership"
# Full integration test
npm test
8. Monitoring and Debugging
Redis Key Monitoring
# Monitor session ownership keys
redis-cli KEYS "session:*:owner"
# Watch session ownership operations
redis-cli MONITOR | grep "session:"
# Check active (live) sessions via pub/sub
redis-cli PUBSUB CHANNELS "mcp:shttp:toserver:*"
redis-cli PUBSUB NUMSUB "mcp:shttp:toserver:*"
Debugging Commands
# Check session ownership
redis-cli GET "session:550e8400-e29b-41d4-a716-446655440000:owner"
# List all session owners
redis-cli KEYS "session:*:owner"
# Check if specific session is live
redis-cli PUBSUB NUMSUB "mcp:shttp:toserver:550e8400-e29b-41d4-a716-446655440000"
# Monitor pub/sub activity
redis-cli MONITOR
This system provides robust multi-user session management with strong security guarantees and comprehensive testing coverage.