Skip to main content
Glama

ClinicalTrials.gov MCP Server

sessionStore.test.ts•11.6 kB
/** * @fileoverview Tests for SessionStore tenant isolation and security. * @module tests/mcp-server/transports/http/sessionStore.test */ import { beforeEach, describe, expect, it } from 'vitest'; import { SessionStore, type SessionIdentity, } from '@/mcp-server/transports/http/sessionStore.js'; /** * Helper to create valid 64-character hex session IDs for testing. * Format matches the output of generateSecureSessionId(). */ function createTestSessionId(suffix: string): string { // Convert suffix to hex if it's not already const hexSuffix = Buffer.from(suffix, 'utf8').toString('hex'); // Pad to 64 characters with zeros const paddedSuffix = hexSuffix.padStart(64, '0'); return paddedSuffix.slice(-64); } describe('SessionStore - Security & Tenant Isolation', () => { let store: SessionStore; const STALE_TIMEOUT = 30_000; // 30 seconds for testing // Valid session IDs for testing const SESSION_1 = createTestSessionId('1'); const SESSION_2 = createTestSessionId('2'); const SESSION_A = createTestSessionId('a'); const SESSION_B = createTestSessionId('b'); const SHARED_SESSION = createTestSessionId('shared'); beforeEach(() => { store = new SessionStore(STALE_TIMEOUT); }); describe('Basic Session Management', () => { it('should create a new session', () => { const session = store.getOrCreate(SESSION_1); expect(session.id).toBe(SESSION_1); expect(session.createdAt).toBeInstanceOf(Date); expect(session.lastAccessedAt).toBeInstanceOf(Date); }); it('should retrieve existing session', () => { const session1 = store.getOrCreate(SESSION_1); const session2 = store.getOrCreate(SESSION_1); expect(session1).toBe(session2); expect(session1.id).toBe(SESSION_1); }); it('should update lastAccessedAt on retrieval', async () => { const session1 = store.getOrCreate(SESSION_1); const firstAccess = session1.lastAccessedAt; await new Promise((resolve) => setTimeout(resolve, 10)); const session2 = store.getOrCreate(SESSION_1); expect(session2.lastAccessedAt.getTime()).toBeGreaterThan( firstAccess.getTime(), ); }); it('should validate existing session', () => { store.getOrCreate(SESSION_1); expect(store.isValidForIdentity(SESSION_1)).toBe(true); }); it('should reject non-existent session', () => { const invalidId = createTestSessionId('nonexistent'); expect(store.isValidForIdentity(invalidId)).toBe(false); }); it('should terminate a session', () => { store.getOrCreate(SESSION_1); expect(store.isValidForIdentity(SESSION_1)).toBe(true); store.terminate(SESSION_1); expect(store.isValidForIdentity(SESSION_1)).toBe(false); }); it('should return session count', () => { expect(store.getSessionCount()).toBe(0); store.getOrCreate(SESSION_1); expect(store.getSessionCount()).toBe(1); store.getOrCreate(SESSION_2); expect(store.getSessionCount()).toBe(2); }); }); describe('Identity Binding', () => { it('should bind identity on session creation', () => { const identity: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', subject: 'user@example.com', }; const session = store.getOrCreate(SESSION_1, identity); expect(session.tenantId).toBe('tenant-a'); expect(session.clientId).toBe('client-1'); expect(session.subject).toBe('user@example.com'); }); it('should create session without identity (backwards compatibility)', () => { const session = store.getOrCreate(SESSION_1); expect(session.tenantId).toBeUndefined(); expect(session.clientId).toBeUndefined(); expect(session.subject).toBeUndefined(); }); it('should lazy-bind identity on first authenticated request', () => { // Create session without identity const session1 = store.getOrCreate(SESSION_1); expect(session1.tenantId).toBeUndefined(); // Bind identity on subsequent request const identity: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; const session2 = store.getOrCreate(SESSION_1, identity); expect(session2.tenantId).toBe('tenant-a'); expect(session2.clientId).toBe('client-1'); expect(session1).toBe(session2); // Same session object }); it('should not rebind identity once set', () => { // Create with first identity const identity1: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; store.getOrCreate(SESSION_1, identity1); // Try to rebind with different identity (should not change) const identity2: SessionIdentity = { tenantId: 'tenant-b', clientId: 'client-2', }; const session = store.getOrCreate(SESSION_1, identity2); // Should still have original identity expect(session.tenantId).toBe('tenant-a'); expect(session.clientId).toBe('client-1'); }); }); describe('Tenant Isolation - Security', () => { it('should accept valid tenant for bound session', () => { const identity: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; store.getOrCreate(SESSION_1, identity); // Validate with same tenant expect(store.isValidForIdentity(SESSION_1, identity)).toBe(true); }); it('should REJECT session reuse across different tenants (CRITICAL)', () => { // User from tenant-a creates session const tenantA: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; store.getOrCreate(SESSION_1, tenantA); // Attacker from tenant-b tries to use same session const tenantB: SessionIdentity = { tenantId: 'tenant-b', clientId: 'client-2', }; // Should REJECT - this prevents session hijacking expect(store.isValidForIdentity(SESSION_1, tenantB)).toBe(false); }); it('should REJECT session reuse across different clients', () => { // User with client-1 creates session const client1: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; store.getOrCreate(SESSION_1, client1); // Different client from same tenant tries to use session const client2: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-2', }; // Should REJECT expect(store.isValidForIdentity(SESSION_1, client2)).toBe(false); }); it('should allow unbound session in no-auth mode', () => { // Create session without identity (no-auth mode) store.getOrCreate(SESSION_1); // Should accept requests without identity expect(store.isValidForIdentity(SESSION_1)).toBe(true); expect(store.isValidForIdentity(SESSION_1, undefined)).toBe(true); }); it('should REJECT authenticated request for unbound session', () => { // Session created without identity store.getOrCreate(SESSION_1); // Authenticated request with identity const identity: SessionIdentity = { tenantId: 'tenant-a', }; // Should allow (will trigger lazy binding) expect(store.isValidForIdentity(SESSION_1, identity)).toBe(true); }); it('should REJECT unauthenticated request for bound session', () => { // Session created with identity const identity: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-1', }; store.getOrCreate(SESSION_1, identity); // Unauthenticated request (no identity) expect(store.isValidForIdentity(SESSION_1)).toBe(false); expect(store.isValidForIdentity(SESSION_1, undefined)).toBe(false); }); }); describe('Staleness & Cleanup', () => { it('should invalidate stale sessions', async () => { // Use a very short timeout for faster test execution const shortStore = new SessionStore(50); // 50ms timeout shortStore.getOrCreate(SESSION_1); expect(shortStore.isValidForIdentity(SESSION_1)).toBe(true); // Wait for session to become stale await new Promise((resolve) => setTimeout(resolve, 100)); expect(shortStore.isValidForIdentity(SESSION_1)).toBe(false); }); it('should invalidate stale sessions with identity validation', async () => { const shortStore = new SessionStore(50); // 50ms timeout const identity: SessionIdentity = { tenantId: 'tenant-a', }; shortStore.getOrCreate(SESSION_1, identity); expect(shortStore.isValidForIdentity(SESSION_1, identity)).toBe(true); // Wait for session to become stale await new Promise((resolve) => setTimeout(resolve, 100)); expect(shortStore.isValidForIdentity(SESSION_1, identity)).toBe(false); }); }); describe('Multi-Tenant Scenarios', () => { it('should isolate sessions across multiple tenants', () => { // Tenant A creates session const tenantA: SessionIdentity = { tenantId: 'tenant-a', clientId: 'client-a', }; store.getOrCreate(SESSION_A, tenantA); // Tenant B creates different session const tenantB: SessionIdentity = { tenantId: 'tenant-b', clientId: 'client-b', }; store.getOrCreate(SESSION_B, tenantB); // Each tenant can access their own session expect(store.isValidForIdentity(SESSION_A, tenantA)).toBe(true); expect(store.isValidForIdentity(SESSION_B, tenantB)).toBe(true); // But NOT each other's sessions expect(store.isValidForIdentity(SESSION_A, tenantB)).toBe(false); expect(store.isValidForIdentity(SESSION_B, tenantA)).toBe(false); }); it('should handle same session ID across different tenants (edge case)', () => { // This shouldn't happen with crypto-strong IDs, but test defensive behavior // Tenant A creates session first const tenantA: SessionIdentity = { tenantId: 'tenant-a', }; store.getOrCreate(SHARED_SESSION, tenantA); // Tenant B tries to use same session ID const tenantB: SessionIdentity = { tenantId: 'tenant-b', }; // Tenant A should work expect(store.isValidForIdentity(SHARED_SESSION, tenantA)).toBe(true); // Tenant B should be REJECTED (session belongs to A) expect(store.isValidForIdentity(SHARED_SESSION, tenantB)).toBe(false); }); }); describe('Partial Identity Matching', () => { it('should validate when only tenantId is set', () => { const identity: SessionIdentity = { tenantId: 'tenant-a', // No clientId }; store.getOrCreate(SESSION_1, identity); // Should validate with same tenant expect( store.isValidForIdentity(SESSION_1, { tenantId: 'tenant-a' }), ).toBe(true); // Should reject different tenant expect( store.isValidForIdentity(SESSION_1, { tenantId: 'tenant-b' }), ).toBe(false); }); it('should validate when only clientId is set', () => { const identity: SessionIdentity = { clientId: 'client-1', // No tenantId }; store.getOrCreate(SESSION_1, identity); // Should validate with same client expect( store.isValidForIdentity(SESSION_1, { clientId: 'client-1' }), ).toBe(true); // Should reject different client expect( store.isValidForIdentity(SESSION_1, { clientId: 'client-2' }), ).toBe(false); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cyanheads/clinicaltrialsgov-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server