example-remote-server/docs/session-ownership.md
2025-10-05 23:51:06 -04:00

4.5 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:

Embedded OAuth: Reads McpInstallation from Redis, extracts installation.userId

External OAuth: Calls auth server's /introspect endpoint, receives sub field

Both result in AuthInfo.extra.userId being populated.

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