From 230b1655efb9dadc8a9d565ecf64abecda74ddbb Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Fri, 30 May 2025 14:35:33 -0700 Subject: [PATCH 1/5] Enhance authentication handling in withMcpAuth --- src/next/auth-wrapper.ts | 110 ++++++++++++++++++++++++------------ src/next/mcp-api-handler.ts | 6 +- 2 files changed, 77 insertions(+), 39 deletions(-) diff --git a/src/next/auth-wrapper.ts b/src/next/auth-wrapper.ts index 75e835d..f8709c5 100644 --- a/src/next/auth-wrapper.ts +++ b/src/next/auth-wrapper.ts @@ -1,4 +1,5 @@ import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types"; +import { InvalidTokenError, InsufficientScopeError, ServerError } from "@modelcontextprotocol/sdk/server/auth/errors"; import { withAuthContext } from "./auth-context"; export function withMcpAuth( @@ -9,57 +10,92 @@ export function withMcpAuth( ) => AuthInfo | undefined | Promise, { required = false, - oauthResourcePath = "/.well-known/oauth-protected-resource", + resourceMetadataPath = "/.well-known/oauth-protected-resource", + requiredScopes, }: { required?: boolean; - oauthResourcePath?: string; + resourceMetadataPath?: string; + requiredScopes?: string[]; } = {} ) { return async (req: Request) => { - const origin = new URL(req.url).origin; + try { + const authHeader = req.headers.get("Authorization"); + const [type, token] = authHeader?.split(" ") || []; - const authHeader = req.headers.get("Authorization"); - const [type, token] = authHeader?.split(" ") || []; + // Only support bearer token as per the MCP spec + // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-6-1-token-requirements + const bearerToken = type?.toLowerCase() === "bearer" ? token : undefined; - // Only support bearer token as per the MCP spec - // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-6-1-token-requirements - const bearerToken = type?.toLowerCase() === "bearer" ? token : undefined; + const authInfo = await verifyToken(req, bearerToken); + + if (required && !authInfo) { + throw new InvalidTokenError("No authorization provided"); + } - const authInfo = await verifyToken(req, bearerToken); + if (!authInfo) { + return handler(req); + } - if (required && !authInfo) { - return new Response( - JSON.stringify({ - error: "unauthorized_client", - error_description: "No authorization provided", - }), - { - status: 401, - headers: { - "WWW-Authenticate": `Bearer resource_metadata=${origin}${oauthResourcePath}`, - }, + // Check if token has the required scopes (if any) + if (requiredScopes?.length) { + const hasAllScopes = requiredScopes.every(scope => + authInfo.scopes.includes(scope) + ); + + if (!hasAllScopes) { + throw new InsufficientScopeError("Insufficient scope"); } - ); - } + } - if (!authInfo) { - return handler(req); - } + // Check if the token is expired + if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) { + throw new InvalidTokenError("Token has expired"); + } + + // Set auth info on the request object after successful verification + (req as any).auth = authInfo; + + return withAuthContext(authInfo, () => handler(req)); + } catch (error) { + const origin = new URL(req.url).origin; + const resourceMetadataUrl = `${origin}${resourceMetadataPath}`; - if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) { - return new Response( - JSON.stringify({ - error: "invalid_token", - error_description: "Authorization expired", - }), - { + if (error instanceof InvalidTokenError) { + return new Response(JSON.stringify(error.toResponseObject()), { status: 401, headers: { - "WWW-Authenticate": `Bearer error="invalid_token", error_description="Authorization expired", resource_metadata=${origin}${oauthResourcePath}`, - }, - } - ); + "WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`, + "Content-Type": "application/json" + } + }); + } else if (error instanceof InsufficientScopeError) { + return new Response(JSON.stringify(error.toResponseObject()), { + status: 403, + headers: { + "WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`, + "Content-Type": "application/json" + } + }); + } else if (error instanceof ServerError) { + return new Response(JSON.stringify(error.toResponseObject()), { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + } else { + console.error("Unexpected error authenticating bearer token:", error); + const serverError = new ServerError("Internal Server Error"); + return new Response(JSON.stringify(serverError.toResponseObject()), { + status: 500, + headers: { + "Content-Type": "application/json" + } + }); + } } - return withAuthContext(authInfo, () => handler(req)); }; } + + diff --git a/src/next/mcp-api-handler.ts b/src/next/mcp-api-handler.ts index 0219073..34419c2 100644 --- a/src/next/mcp-api-handler.ts +++ b/src/next/mcp-api-handler.ts @@ -322,7 +322,9 @@ export function initializeMcpApiHandler( url: req.url, headers: Object.fromEntries(req.headers), body: bodyContent, + auth: (req as any).auth, // Use the auth info that should already be set by withMcpAuth }); + // Create a response that will emit events const wrappedRes = new EventEmittingResponse( @@ -666,7 +668,7 @@ interface FakeIncomingMessageOptions { // Create a fake IncomingMessage function createFakeIncomingMessage( options: FakeIncomingMessageOptions = {} -): IncomingMessage { +): IncomingMessage & { auth?: AuthInfo } { const { method = "GET", url = "/", @@ -696,7 +698,7 @@ function createFakeIncomingMessage( } // Create the IncomingMessage instance - const req = new IncomingMessage(socket); + const req = new IncomingMessage(socket) as IncomingMessage & { auth?: AuthInfo }; // Set the properties req.method = method; From 07da52644a814554bcc2946b31cee31aa3d388d9 Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Fri, 30 May 2025 15:08:25 -0700 Subject: [PATCH 2/5] Global req object customization, better error handling --- src/next/auth-wrapper.ts | 8 +++++++- src/next/mcp-api-handler.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/next/auth-wrapper.ts b/src/next/auth-wrapper.ts index f8709c5..307d24e 100644 --- a/src/next/auth-wrapper.ts +++ b/src/next/auth-wrapper.ts @@ -2,6 +2,12 @@ import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types"; import { InvalidTokenError, InsufficientScopeError, ServerError } from "@modelcontextprotocol/sdk/server/auth/errors"; import { withAuthContext } from "./auth-context"; +declare global { + interface Request { + auth?: AuthInfo; + } +} + export function withMcpAuth( handler: (req: Request) => Response | Promise, verifyToken: ( @@ -54,7 +60,7 @@ export function withMcpAuth( } // Set auth info on the request object after successful verification - (req as any).auth = authInfo; + req.auth = authInfo; return withAuthContext(authInfo, () => handler(req)); } catch (error) { diff --git a/src/next/mcp-api-handler.ts b/src/next/mcp-api-handler.ts index 34419c2..f6976bb 100644 --- a/src/next/mcp-api-handler.ts +++ b/src/next/mcp-api-handler.ts @@ -322,7 +322,7 @@ export function initializeMcpApiHandler( url: req.url, headers: Object.fromEntries(req.headers), body: bodyContent, - auth: (req as any).auth, // Use the auth info that should already be set by withMcpAuth + auth: req.auth, // Use the auth info that should already be set by withMcpAuth }); From 06d9a24054fc169959f864960f2d7b2447baba4d Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Sat, 31 May 2025 16:49:43 -0700 Subject: [PATCH 3/5] Export AuthInfo for convenience --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 1353dc5..e620efd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export { default as createMcpHandler } from "./next"; export { withMcpAuth as experimental_withMcpAuth } from "./next/auth-wrapper"; +export type { AuthInfo } from "./next/auth-wrapper"; From 853cd435c39cf1305c976e4e9bf1c8809fcf5ad6 Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Mon, 2 Jun 2025 16:02:24 -0700 Subject: [PATCH 4/5] Remove AuthInfo export --- src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index e620efd..1353dc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,4 +2,3 @@ export { default as createMcpHandler } from "./next"; export { withMcpAuth as experimental_withMcpAuth } from "./next/auth-wrapper"; -export type { AuthInfo } from "./next/auth-wrapper"; From 2a03f5f84e3bd2936f0c6d99e97af23dd7d41a67 Mon Sep 17 00:00:00 2001 From: Allen Zhou <46854522+allenzhou101@users.noreply.github.com> Date: Wed, 4 Jun 2025 12:01:11 -0700 Subject: [PATCH 5/5] Create eight-bananas-serve.md --- .changeset/eight-bananas-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/eight-bananas-serve.md diff --git a/.changeset/eight-bananas-serve.md b/.changeset/eight-bananas-serve.md new file mode 100644 index 0000000..e888f43 --- /dev/null +++ b/.changeset/eight-bananas-serve.md @@ -0,0 +1,5 @@ +--- +"@vercel/mcp-adapter": patch +--- + +Add more robust error handling to auth logic