diff --git a/mcp.png b/mcp.png new file mode 100644 index 0000000..86c5266 Binary files /dev/null and b/mcp.png differ diff --git a/src/auth/provider.test.ts b/src/auth/provider.test.ts index 5f7c82b..bfdb9ec 100644 --- a/src/auth/provider.test.ts +++ b/src/auth/provider.test.ts @@ -53,6 +53,7 @@ function getMockAuthValues() { }, clientId: client.client_id, issuedAt: Date.now() / 1000, + userId: "test-user-id", }; return { @@ -279,6 +280,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "different-client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; await authService.saveRefreshToken(refreshToken, accessToken); @@ -314,6 +316,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; await authService.saveMcpInstallation(accessToken, mcpInstallation); @@ -351,6 +354,7 @@ describe("EverythingAuthProvider", () => { }, clientId: "client-id", issuedAt: twoDaysAgoInSeconds, // 2 days ago, with 1-day expiry + userId: "test-user-id", }; await authService.saveMcpInstallation(accessToken, mcpInstallation); @@ -377,6 +381,7 @@ describe("EverythingAuthProvider", () => { }, clientId: client.client_id, issuedAt: Date.now() / 1000, + userId: "test-user-id", }; // Save the installation diff --git a/src/auth/provider.ts b/src/auth/provider.ts index 53cb73e..3d77c61 100644 --- a/src/auth/provider.ts +++ b/src/auth/provider.ts @@ -72,22 +72,196 @@ export class EverythingAuthProvider implements OAuthServerProvider { // You can redirect to another page, or you can send an html response directly // res.redirect(new URL(`fakeupstreamauth/authorize?metadata=${authorizationCode}`, BASE_URI).href); + // Set permissive CSP for styling + res.setHeader('Content-Security-Policy', [ + "default-src 'self'", + "style-src 'self' 'unsafe-inline'", + "script-src 'self' 'unsafe-inline'", + "img-src 'self' data:", + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + "base-uri 'self'" + ].join('; ')); + res.send(` - + + - MCP Auth Page + + + MCP Server Authorization + -

MCP Server Auth Page

-

- This page is the authorization page presented by the MCP server, routing the user upstream. This is only - needed on 2025-03-26 Auth spec, where the MCP server acts as it's own authoriztion server. This page should - be present to avoid confused deputy attacks. -

-

- Click here - to continue to the upstream auth -

+
+
+ +
MCP
+
+ +

Authorization Required

+

This client wants to connect to your MCP server

+ +
+

Client Application

+
${client.client_id}
+
+ +
+

What happens next?

+

You'll be redirected to authenticate with the upstream provider. Once verified, you'll be granted access to this MCP server's resources.

+
+ + + Continue to Authentication + + +
+ Model Context Protocol (MCP) Server +
+
`); @@ -156,6 +330,7 @@ export class EverythingAuthProvider implements OAuthServerProvider { ...mcpInstallation, mcpTokens: newTokens, issuedAt: Date.now() / 1000, + userId: mcpInstallation.userId, // Preserve the user ID }); return newTokens; @@ -183,7 +358,10 @@ export class EverythingAuthProvider implements OAuthServerProvider { token, clientId: installation.clientId, scopes: ['mcp'], - expiresAt + expiresAt, + extra: { + userId: installation.userId + } }; } diff --git a/src/handlers/fakeauth.ts b/src/handlers/fakeauth.ts index d19e786..49f1f70 100644 --- a/src/handlers/fakeauth.ts +++ b/src/handlers/fakeauth.ts @@ -12,18 +12,245 @@ export async function handleFakeAuthorize(req: Request, res: Response) { // get the redirect_uri and state from the query params const { redirect_uri, state } = req.query; - // TODO, mint actual codes? + // Set a more permissive CSP for auth pages to allow inline styles and scripts + res.setHeader('Content-Security-Policy', [ + "default-src 'self'", + "style-src 'self' 'unsafe-inline'", // Allow inline styles for auth page styling + "script-src 'self' 'unsafe-inline'", // Allow inline scripts for auth page functionality + "object-src 'none'", + "frame-ancestors 'none'", + "form-action 'self'", + "base-uri 'self'" + ].join('; ')); res.send(` - + + - Fake Upstream Auth Provider! + + + Upstream Provider Authentication + -

Fake Auth

-

Fake auth page

-

Click here to authorize

-

Click here to fail authorization

+
+ +

Upstream Authentication

+

Please verify your identity with the upstream provider

+ +
+

Your User Identity

+
Loading...
+ +
+ + + +
+ Testing Multiple Users: Open this page in different browser windows or incognito tabs to simulate different users. Each will have their own unique User ID and separate MCP sessions. +
+
+ + `); @@ -36,6 +263,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { // The state returned from the upstream auth server is actually the authorization code state: mcpAuthorizationCode, code: upstreamAuthorizationCode, + userId, // User ID from the authorization flow } = req.query; // This is where you'd exchange the upstreamAuthorizationCode for access/refresh tokens @@ -63,6 +291,7 @@ export async function handleFakeAuthorizeRedirect(req: Request, res: Response) { mcpTokens, clientId: pendingAuth.clientId, issuedAt: Date.now() / 1000, + userId: (userId as string) || 'anonymous-user', // Include user ID from auth flow } // Store the upstream authorization data diff --git a/src/index.ts b/src/index.ts index 5f7aa32..37e94e9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,11 @@ const baseSecurityHeaders = (req: express.Request, res: express.Response, next: // simple logging middleware const logger = (req: express.Request, res: express.Response, next: express.NextFunction) => { + console.log(`${new Date().toISOString()} ${req.method} ${req.url}`); + if (req.url.includes('/mcp')) { + console.log(' Headers:', JSON.stringify(req.headers, null, 2)); + console.log(' Body:', JSON.stringify(req.body, null, 2)); + } next(); }; @@ -106,7 +111,12 @@ app.get("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP app.post("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP); app.delete("/mcp", cors(corsOptions), bearerAuth, authContext, handleStreamableHTTP); -// Upstream auth routes +// Static assets +app.get("/mcp-logo.png", (req, res) => { + res.sendFile("mcp.png", { root: "." }); +}); + +// Upstream auth routes app.get("/fakeupstreamauth/authorize", cors(corsOptions), handleFakeAuthorize); app.get("/fakeupstreamauth/callback", cors(corsOptions), handleFakeAuthorizeRedirect); diff --git a/src/redis.ts b/src/redis.ts index cc57f00..1316592 100644 --- a/src/redis.ts +++ b/src/redis.ts @@ -142,7 +142,7 @@ export class MockRedisClient implements RedisClient { options = { url: "redis://localhost:6379" }; private store = new Map(); private lists = new Map(); - private subscribers = new Map void)[]>(); + public subscribers = new Map void)[]>(); // Public for testing access private errorCallbacks = new Map void)[]>(); async get(key: string): Promise { diff --git a/src/services/auth.test.ts b/src/services/auth.test.ts index e79dd10..6a761bd 100644 --- a/src/services/auth.test.ts +++ b/src/services/auth.test.ts @@ -193,6 +193,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", } await saveMcpInstallation(accessToken, mcpInstallation); @@ -239,6 +240,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }); const getDel = jest.spyOn(mockRedis, 'getDel').mockImplementationOnce(() => { @@ -252,6 +254,7 @@ describe("auth utils", () => { }, clientId: "client-id", issuedAt: Date.now() / 1000, + userId: "test-user-id", }; const value = JSON.stringify(mcpInstallation); const iv = crypto.randomBytes(16);