Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 79 additions & 37 deletions src/next/auth-wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
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<Response>,
verifyToken: (
Expand All @@ -9,57 +16,92 @@ export function withMcpAuth(
) => AuthInfo | undefined | Promise<AuthInfo | undefined>,
{
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.auth = authInfo;

if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) {
return new Response(
JSON.stringify({
error: "invalid_token",
error_description: "Authorization expired",
}),
{
return withAuthContext(authInfo, () => handler(req));
} catch (error) {
const origin = new URL(req.url).origin;
const resourceMetadataUrl = `${origin}${resourceMetadataPath}`;

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));
};
}


6 changes: 4 additions & 2 deletions src/next/mcp-api-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,9 @@ export function initializeMcpApiHandler(
url: req.url,
headers: Object.fromEntries(req.headers),
body: bodyContent,
auth: req.auth, // Use the auth info that should already be set by withMcpAuth
});


// Create a response that will emit events
const wrappedRes = new EventEmittingResponse(
Expand Down Expand Up @@ -666,7 +668,7 @@ interface FakeIncomingMessageOptions {
// Create a fake IncomingMessage
function createFakeIncomingMessage(
options: FakeIncomingMessageOptions = {}
): IncomingMessage {
): IncomingMessage & { auth?: AuthInfo } {
const {
method = "GET",
url = "/",
Expand Down Expand Up @@ -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;
Expand Down