Simplified the repository to have a single OAuth pattern with separate auth and MCP servers at the top level. Preserved embedded OAuth documentation as an alternative pattern.
4.6 KiB
Session Ownership System
Overview
The MCP server implements multi-user session isolation to ensure users can only access their own MCP sessions. This is accomplished using user identifiers and Redis-backed ownership tracking.
How It Works
Each MCP session is owned by a specific user. Session ownership is stored in Redis and validated on each request.
User ID Assignment
Production: OAuth providers assign unique user identifiers via the sub (subject) claim - a standard OAuth/OpenID Connect field.
User ID sources:
- Provider's user database: Users created in Auth0/Okta (example:
auth0|507f1f77bcf86cd799439011) - Social/federated identities: Provider links to Google, Facebook (example:
google-oauth2|112233445566778899) - Enterprise connections: SAML, LDAP, Active Directory (example:
samlp|ad|john.doe@company.com)
This implementation: Mock upstream IDP returns HTML with JavaScript that generates a random UUID in the browser, stores it in localStorage.mcpUserId, and sends it to the server as a query parameter. This simulates user authentication for testing - different browsers/tabs get different UUIDs, allowing multi-user testing.
Token Validation
When the MCP server validates an access token:
- MCP server calls the auth server's
/introspectendpoint with the token - Auth server validates the token and returns user information including the
subfield (user ID) - MCP server populates
AuthInfo.extra.userIdwith this user identifier
This separation allows the auth server to be replaced with any OAuth provider that supports token introspection.
Session Creation
const userId = req.auth?.extra?.userId;
const sessionId = randomUUID();
await redisClient.set(`session:${sessionId}:owner`, userId);
Session Access Validation
const owner = await redisClient.get(`session:${sessionId}:owner`);
if (owner !== userId) {
throw new Error('Session not owned by user');
}
Redis Key Structure
Session Ownership
session:{sessionId}:owner → userId
Example: session:550e8400-...:owner → auth0|507f1f77bcf86cd799439011
Session Liveness
Sessions are considered "live" when an MCP server process is actively handling them. This is tracked via Redis pub/sub subscription counts:
mcp:shttp:toserver:{sessionId} → pub/sub channel
Check if live: PUBSUB NUMSUB mcp:shttp:toserver:{sessionId} returns > 0
When an MCP server starts handling a session, it subscribes to the channel. When it shuts down (gracefully or via crash), Redis automatically removes the subscription.
Security
- Session isolation: Users can only access sessions they own
- Ownership persistence: Survives across requests and server restarts
- Token-based validation: User ID extracted from validated OAuth token
- Access control: All operations (GET, POST, DELETE) validate ownership
Implementation
Core Functions (src/services/redisTransport.ts)
export async function setSessionOwner(sessionId: string, userId: string): Promise<void>
export async function isSessionOwnedBy(sessionId: string, userId: string): Promise<boolean>
export async function isLive(sessionId: string): Promise<boolean>
export async function shutdownSession(sessionId: string): Promise<void>
Request Flow
sequenceDiagram
participant Client
participant MCP as MCP Server
participant Redis
Client->>MCP: POST /mcp (initialize)
MCP->>MCP: Extract userId from OAuth token
MCP->>MCP: Generate sessionId
MCP->>Redis: SET session:{sessionId}:owner = userId
MCP->>Client: Return sessionId
Client->>MCP: POST /mcp (with sessionId)
MCP->>MCP: Extract userId from OAuth token
MCP->>Redis: GET session:{sessionId}:owner
Redis-->>MCP: userId
MCP->>MCP: Verify userId matches token
MCP->>Client: Process request
Configuration
Environment Variables
REDIS_URL=redis://localhost:6379
BASE_URI=http://localhost:3232
Redis Monitoring
# List all session owners
redis-cli KEYS "session:*:owner"
# Check specific session ownership
redis-cli GET "session:{sessionId}:owner"
# Check if session is live (actively being handled)
redis-cli PUBSUB NUMSUB "mcp:shttp:toserver:{sessionId}"
# Monitor session operations
redis-cli MONITOR | grep "session:"
Testing
npm test -- --testNamePattern="User Session Isolation"
npm test -- --testNamePattern="session ownership"
References
- RFC 7662: Token Introspection - Standard for
subclaim - MCP Authorization Spec