2026-03-30 09:53:45 +08:00
|
|
|
import { Request, Response } from 'express';
|
2026-04-05 00:12:14 +08:00
|
|
|
import { randomUUID } from 'crypto';
|
2026-03-30 09:53:45 +08:00
|
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
|
|
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp';
|
|
|
|
|
import { User } from '../types';
|
2026-04-05 00:12:14 +08:00
|
|
|
import { verifyMcpToken, verifyJwtToken } from '../services/authService';
|
2026-04-10 04:25:58 +08:00
|
|
|
import { getUserByAccessToken } from '../services/oauthService';
|
2026-04-05 00:12:14 +08:00
|
|
|
import { isAddonEnabled } from '../services/adminService';
|
2026-04-10 04:25:58 +08:00
|
|
|
import { ADDON_IDS } from '../addons';
|
2026-03-30 09:53:45 +08:00
|
|
|
import { registerResources } from './resources';
|
|
|
|
|
import { registerTools } from './tools';
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
import { McpSession, sessions, revokeUserSessions, revokeUserSessionsForClient } from './sessionManager';
|
2026-03-30 09:53:45 +08:00
|
|
|
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
export { revokeUserSessions, revokeUserSessionsForClient };
|
2026-03-30 09:53:45 +08:00
|
|
|
|
|
|
|
|
const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
|
2026-04-06 06:08:17 +08:00
|
|
|
const sessionParsed = Number.parseInt(process.env.MCP_MAX_SESSION_PER_USER ?? "");
|
|
|
|
|
const MAX_SESSIONS_PER_USER = Number.isFinite(sessionParsed) && sessionParsed > 0 ? sessionParsed : 5;
|
2026-03-30 12:59:24 +08:00
|
|
|
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
|
2026-04-03 21:43:58 +08:00
|
|
|
const parsed = Number.parseInt(process.env.MCP_RATE_LIMIT ?? "");
|
|
|
|
|
const RATE_LIMIT_MAX = Number.isFinite(parsed) && parsed > 0 ? parsed : 60; // requests per minute per user
|
2026-03-30 12:59:24 +08:00
|
|
|
|
|
|
|
|
interface RateLimitEntry {
|
|
|
|
|
count: number;
|
|
|
|
|
windowStart: number;
|
|
|
|
|
}
|
|
|
|
|
const rateLimitMap = new Map<number, RateLimitEntry>();
|
|
|
|
|
|
|
|
|
|
function isRateLimited(userId: number): boolean {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const entry = rateLimitMap.get(userId);
|
|
|
|
|
if (!entry || now - entry.windowStart > RATE_LIMIT_WINDOW_MS) {
|
|
|
|
|
rateLimitMap.set(userId, { count: 1, windowStart: now });
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
entry.count += 1;
|
|
|
|
|
return entry.count > RATE_LIMIT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countSessionsForUser(userId: number): number {
|
|
|
|
|
const cutoff = Date.now() - SESSION_TTL_MS;
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (const session of sessions.values()) {
|
|
|
|
|
if (session.userId === userId && session.lastActivity >= cutoff) count++;
|
|
|
|
|
}
|
|
|
|
|
return count;
|
|
|
|
|
}
|
2026-03-30 09:53:45 +08:00
|
|
|
|
|
|
|
|
const sessionSweepInterval = setInterval(() => {
|
|
|
|
|
const cutoff = Date.now() - SESSION_TTL_MS;
|
2026-04-06 21:21:55 +08:00
|
|
|
let cleaned = 0;
|
2026-03-30 09:53:45 +08:00
|
|
|
for (const [sid, session] of sessions) {
|
|
|
|
|
if (session.lastActivity < cutoff) {
|
2026-03-30 12:59:24 +08:00
|
|
|
try { session.server.close(); } catch { /* ignore */ }
|
2026-03-30 09:53:45 +08:00
|
|
|
try { session.transport.close(); } catch { /* ignore */ }
|
|
|
|
|
sessions.delete(sid);
|
2026-04-06 21:21:55 +08:00
|
|
|
cleaned++;
|
2026-03-30 09:53:45 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-30 12:59:24 +08:00
|
|
|
const rateCutoff = Date.now() - RATE_LIMIT_WINDOW_MS;
|
|
|
|
|
for (const [uid, entry] of rateLimitMap) {
|
|
|
|
|
if (entry.windowStart < rateCutoff) rateLimitMap.delete(uid);
|
|
|
|
|
}
|
2026-04-06 21:21:55 +08:00
|
|
|
if (cleaned > 0 || sessions.size > 0) {
|
|
|
|
|
console.log(`[MCP] Session sweep: cleaned ${cleaned}, active ${sessions.size}`);
|
|
|
|
|
}
|
2026-03-30 09:53:45 +08:00
|
|
|
}, 10 * 60 * 1000); // sweep every 10 minutes
|
|
|
|
|
|
|
|
|
|
// Prevent the interval from keeping the process alive if nothing else is running
|
|
|
|
|
sessionSweepInterval.unref();
|
|
|
|
|
|
2026-04-10 04:25:58 +08:00
|
|
|
interface VerifyTokenResult {
|
|
|
|
|
user: User;
|
|
|
|
|
/** null = full access (static token or JWT); string[] = OAuth 2.1 scoped access */
|
|
|
|
|
scopes: string[] | null;
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
/** OAuth client_id when authenticated via OAuth 2.1; null otherwise */
|
|
|
|
|
clientId: string | null;
|
2026-04-10 04:25:58 +08:00
|
|
|
isStaticToken: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function verifyToken(authHeader: string | undefined): VerifyTokenResult | null {
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
if (!authHeader) return null;
|
|
|
|
|
// M8: strictly require "Bearer" scheme (RFC 6750)
|
|
|
|
|
const spaceIdx = authHeader.indexOf(' ');
|
|
|
|
|
if (spaceIdx === -1) return null;
|
|
|
|
|
const scheme = authHeader.slice(0, spaceIdx);
|
|
|
|
|
const token = authHeader.slice(spaceIdx + 1);
|
|
|
|
|
if (scheme.toLowerCase() !== 'bearer' || !token) return null;
|
2026-03-30 09:53:45 +08:00
|
|
|
|
2026-04-10 04:25:58 +08:00
|
|
|
// OAuth 2.1 access token (trekoa_...)
|
|
|
|
|
if (token.startsWith('trekoa_')) {
|
|
|
|
|
const result = getUserByAccessToken(token);
|
|
|
|
|
if (!result) return null;
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
return { user: result.user, scopes: result.scopes, clientId: result.clientId, isStaticToken: false };
|
2026-04-10 04:25:58 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Long-lived static MCP token (trek_...) — full access + deprecation notice
|
2026-03-30 09:53:45 +08:00
|
|
|
if (token.startsWith('trek_')) {
|
2026-04-10 04:25:58 +08:00
|
|
|
const user = verifyMcpToken(token);
|
|
|
|
|
if (!user) return null;
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
return { user, scopes: null, clientId: null, isStaticToken: true };
|
2026-03-30 09:53:45 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-10 04:25:58 +08:00
|
|
|
// Short-lived JWT (TREK web session used directly) — full access, no notice
|
|
|
|
|
const user = verifyJwtToken(token);
|
|
|
|
|
if (!user) return null;
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
return { user, scopes: null, clientId: null, isStaticToken: false };
|
2026-03-30 09:53:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function mcpHandler(req: Request, res: Response): Promise<void> {
|
2026-04-10 04:25:58 +08:00
|
|
|
if (!isAddonEnabled(ADDON_IDS.MCP)) {
|
2026-03-30 09:53:45 +08:00
|
|
|
res.status(403).json({ error: 'MCP is not enabled' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 04:25:58 +08:00
|
|
|
const tokenResult = verifyToken(req.headers['authorization']);
|
|
|
|
|
if (!tokenResult) {
|
2026-03-30 09:53:45 +08:00
|
|
|
res.status(401).json({ error: 'Access token required' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
const { user, scopes, clientId, isStaticToken } = tokenResult;
|
2026-03-30 09:53:45 +08:00
|
|
|
|
2026-03-30 12:59:24 +08:00
|
|
|
if (isRateLimited(user.id)) {
|
|
|
|
|
res.status(429).json({ error: 'Too many requests. Please slow down.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 09:53:45 +08:00
|
|
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
|
|
|
|
|
|
|
|
// Resume an existing session
|
|
|
|
|
if (sessionId) {
|
|
|
|
|
const session = sessions.get(sessionId);
|
|
|
|
|
if (!session) {
|
|
|
|
|
res.status(404).json({ error: 'Session not found' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (session.userId !== user.id) {
|
|
|
|
|
res.status(403).json({ error: 'Session belongs to a different user' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
session.lastActivity = Date.now();
|
2026-04-06 21:21:55 +08:00
|
|
|
try {
|
|
|
|
|
await session.transport.handleRequest(req, res, req.body);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[MCP] transport.handleRequest error:', err);
|
|
|
|
|
if (!res.headersSent) {
|
2026-04-09 18:56:05 +08:00
|
|
|
res.status(500).json({ error: 'Internal MCP error' });
|
2026-04-06 21:21:55 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-30 09:53:45 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only POST can initialize a new session
|
|
|
|
|
if (req.method !== 'POST') {
|
|
|
|
|
res.status(400).json({ error: 'Missing mcp-session-id header' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 12:59:24 +08:00
|
|
|
if (countSessionsForUser(user.id) >= MAX_SESSIONS_PER_USER) {
|
|
|
|
|
res.status(429).json({ error: 'Session limit reached. Close an existing session before opening a new one.' });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 09:53:45 +08:00
|
|
|
// Create a new per-user MCP server and session
|
MCP: add tool annotations, prompts, mimeType, and capabilities
- Add tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) to all 40+ tools
- Register 3 MCP prompts: trip-summary, packing-list, budget-overview
- Add explicit mimeType: application/json to all resource registrations
- Announce capabilities with listChanged on resources, tools, prompts
- Update server name to 'TREK MCP' in MCP initialization
2026-04-06 16:43:31 +08:00
|
|
|
const server = new McpServer({
|
|
|
|
|
name: 'TREK MCP',
|
|
|
|
|
version: '1.0.0',
|
|
|
|
|
capabilities: {
|
|
|
|
|
resources: { listChanged: true },
|
|
|
|
|
tools: { listChanged: true },
|
|
|
|
|
prompts: { listChanged: true },
|
|
|
|
|
},
|
|
|
|
|
});
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
registerResources(server, user.id, scopes);
|
2026-04-10 04:25:58 +08:00
|
|
|
registerTools(server, user.id, scopes, isStaticToken);
|
2026-03-30 09:53:45 +08:00
|
|
|
|
|
|
|
|
const transport = new StreamableHTTPServerTransport({
|
|
|
|
|
sessionIdGenerator: () => randomUUID(),
|
|
|
|
|
onsessioninitialized: (sid) => {
|
security(oauth): harden OAuth 2.1/MCP implementation (Critical + High + Medium findings)
Address 14 security findings from internal review of the OAuth 2.1 + MCP layer:
Critical:
- C1: Scope-gate all MCP resources (trips, budget, packing, collab, atlas, vacay, etc.)
- C2: Wire token/session revocation into active MCP session lifecycle per (user, client_id)
- C3: Refresh-token replay detection via parent_token_id chain + cascade revoke on replay
High:
- H1: Validate PKCE code_challenge (43-char base64url) and code_verifier (43–128 chars) format
- H2: Rate-limit /oauth/token (30/min), /authorize/validate (30/min), /oauth/revoke (10/min)
- H3: Strip client metadata from unauthenticated /authorize/validate responses (oracle prevention)
- H4: Constant-time secret comparison via crypto.timingSafeEqual (prevents timing attacks)
- H5: Collapse all invalid_grant cases to a single generic message; log specifics server-side
Medium:
- M1: Set Cache-Control: no-store + Pragma: no-cache on token endpoint responses
- M2: Return 404 (not 200/403) on discovery + revoke endpoints when MCP addon is disabled
- M4: Audit-log all OAuth lifecycle events (create, consent, issue, refresh, revoke, replay)
- M5: Union consent scopes on re-authorization instead of replacing existing grants
- M7: Require httpOnly cookie auth (not Bearer JWT) on all state-mutating OAuth endpoints
- M8: Strict Bearer scheme check in MCP token verification
Refactoring:
- Extract MCP session management (sessions Map, revokeUserSessions, revokeUserSessionsForClient)
into mcp/sessionManager.ts to break the circular dependency between oauthService and mcp/index
- Extract verifyJwtAndLoadUser helper in auth middleware, shared by authenticate and new
requireCookieAuth middleware
Tests:
- Fix all existing integration tests broken by the security hardening (OAUTH-019 to OAUTH-032)
- Add 13 new integration tests covering M1, M2, H1, H3, H5, M5, M7, C3
- Add 14 new unit tests covering C2, C3, H1, H3, M5 behaviors in oauthService
2026-04-10 08:03:12 +08:00
|
|
|
sessions.set(sid, { server, transport, userId: user.id, scopes, clientId, isStaticToken, lastActivity: Date.now() });
|
2026-04-10 04:25:58 +08:00
|
|
|
const authMethod = isStaticToken ? 'static-token' : scopes ? `oauth(${scopes.join(',')})` : 'jwt';
|
|
|
|
|
console.log(`[MCP] Session ${sid} created for user ${user.id} [${authMethod}]. Active sessions: ${sessions.size}`);
|
2026-03-30 09:53:45 +08:00
|
|
|
},
|
|
|
|
|
onsessionclosed: (sid) => {
|
|
|
|
|
sessions.delete(sid);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-06 21:21:55 +08:00
|
|
|
try {
|
|
|
|
|
await server.connect(transport);
|
|
|
|
|
await transport.handleRequest(req, res, req.body);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[MCP] transport.handleRequest error:', err);
|
|
|
|
|
if (!res.headersSent) {
|
|
|
|
|
res.status(500).json({ error: 'Internal MCP error', detail: String(err) });
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-30 09:53:45 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Close all active MCP sessions (call during graceful shutdown). */
|
|
|
|
|
export function closeMcpSessions(): void {
|
|
|
|
|
clearInterval(sessionSweepInterval);
|
|
|
|
|
for (const [, session] of sessions) {
|
2026-03-30 12:59:24 +08:00
|
|
|
try { session.server.close(); } catch { /* ignore */ }
|
2026-03-30 09:53:45 +08:00
|
|
|
try { session.transport.close(); } catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
sessions.clear();
|
2026-03-30 12:59:24 +08:00
|
|
|
rateLimitMap.clear();
|
2026-03-30 09:53:45 +08:00
|
|
|
}
|