Fix test suite for refactored shttp and redisTransport
- 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>
This commit is contained in:
parent
93bc76fb04
commit
88f683ef12
1 changed files with 9953 additions and 94 deletions
392
docs/user-id-system.md
Normal file
392
docs/user-id-system.md
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
# 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
|
||||
|
||||
```typescript
|
||||
// Stored in browser localStorage
|
||||
{
|
||||
"mcpUserId": "550e8400-e29b-41d4-a716-446655440000" // UUID v4
|
||||
}
|
||||
```
|
||||
|
||||
### User ID Generation Flow
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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:
|
||||
|
||||
```javascript
|
||||
// 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:
|
||||
|
||||
```mermaid
|
||||
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
|
||||
- `numsub` reflects the actual state without requiring explicit cleanup
|
||||
|
||||
### Session Ownership Functions
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
1. **Session Isolation**: Users can only access sessions they own
|
||||
2. **Identity Verification**: User ID is validated from authenticated token
|
||||
3. **Ownership Persistence**: Session ownership is stored in Redis
|
||||
4. **Access Control**: All session operations validate ownership
|
||||
5. **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
|
||||
|
||||
```typescript
|
||||
// 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
|
||||
|
||||
1. **Redis Efficiency**: O(1) lookups for session ownership
|
||||
2. **Session Reuse**: Existing sessions are reused when ownership matches
|
||||
3. **Cleanup**: Automatic cleanup prevents resource leaks
|
||||
4. **Caching**: Session ownership is cached in Redis
|
||||
|
||||
---
|
||||
|
||||
## 7. Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
Loading…
Reference in a new issue