Skip to content

Commit dc73fb8

Browse files
committed
Enhance authentication handling in withMcpAuth
1 parent 6682278 commit dc73fb8

File tree

2 files changed

+95
-31
lines changed

2 files changed

+95
-31
lines changed

src/next/auth-wrapper.ts

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,96 @@
1+
import { InvalidTokenError, InsufficientScopeError, ServerError } from '@modelcontextprotocol/sdk/server/auth/errors';
2+
import { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types';
3+
4+
export interface McpAuthOptions {
5+
/**
6+
* Optional, scopes that the token must have.
7+
*/
8+
requiredScopes?: string[];
9+
10+
/**
11+
* Optional, resource metadata path to include in WWW-Authenticate header.
12+
*/
13+
resourceMetadataPath?: string;
14+
}
15+
116
export function withMcpAuth(
217
handler: (req: Request) => Promise<Response>,
3-
verifyToken: (req: Request, token: string) => Promise<boolean>,
4-
oauthResourcePath = "/.well-known/oauth-protected-resource"
18+
verifyToken: (req: Request, token: string) => Promise<AuthInfo>,
19+
options: McpAuthOptions = {
20+
resourceMetadataPath: "/.well-known/oauth-protected-resource"
21+
}
522
) {
623
return async (req: Request) => {
7-
const origin = new URL(req.url).origin;
8-
9-
if (!req.headers.get("Authorization")) {
10-
return new Response(null, {
11-
status: 401,
12-
headers: {
13-
"WWW-Authenticate": `Bearer resource_metadata=${origin}${oauthResourcePath}`,
14-
},
15-
});
16-
}
24+
try {
25+
if (!req.headers.get("Authorization")) {
26+
throw new InvalidTokenError("Missing Authorization header");
27+
}
1728

18-
const authHeader = req.headers.get("Authorization");
19-
const token = authHeader?.split(" ")[1];
29+
const authHeader = req.headers.get("Authorization");
30+
const [type, token] = authHeader?.split(" ") || [];
2031

21-
if (!token) {
22-
throw new Error(
23-
`Invalid authorization header value, expected Bearer <token>, received ${authHeader}`
24-
);
25-
}
32+
if (type?.toLowerCase() !== "bearer" || !token) {
33+
throw new InvalidTokenError("Invalid Authorization header format, expected 'Bearer TOKEN'");
34+
}
2635

27-
const isAuthenticated = await verifyToken(req, token);
36+
const authInfo = await verifyToken(req, token);
2837

29-
if (!isAuthenticated) {
30-
return new Response(JSON.stringify({ error: "Unauthorized" }), {
31-
status: 401,
32-
headers: {
33-
"WWW-Authenticate": `Bearer resource_metadata=${origin}${oauthResourcePath}`,
34-
},
35-
});
36-
}
38+
// Check if token has the required scopes (if any)
39+
if (options.requiredScopes?.length) {
40+
const hasAllScopes = options.requiredScopes.every(scope =>
41+
authInfo.scopes.includes(scope)
42+
);
43+
44+
if (!hasAllScopes) {
45+
throw new InsufficientScopeError("Insufficient scope");
46+
}
47+
}
48+
49+
// Check if the token is expired
50+
if (authInfo.expiresAt && authInfo.expiresAt < Date.now() / 1000) {
51+
throw new InvalidTokenError("Token has expired");
52+
}
3753

38-
return handler(req);
54+
// Set auth info on the request object after successful verification
55+
(req as any).auth = authInfo;
56+
57+
return handler(req);
58+
} catch (error) {
59+
const origin = new URL(req.url).origin;
60+
const resourceMetadataUrl = options.resourceMetadataPath || `${origin}/.well-known/oauth-protected-resource`;
61+
if (error instanceof InvalidTokenError) {
62+
return new Response(JSON.stringify(error.toResponseObject()), {
63+
status: 401,
64+
headers: {
65+
"WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`,
66+
"Content-Type": "application/json"
67+
}
68+
});
69+
} else if (error instanceof InsufficientScopeError) {
70+
return new Response(JSON.stringify(error.toResponseObject()), {
71+
status: 403,
72+
headers: {
73+
"WWW-Authenticate": `Bearer error="${error.errorCode}", error_description="${error.message}", resource_metadata="${resourceMetadataUrl}"`,
74+
"Content-Type": "application/json"
75+
}
76+
});
77+
} else if (error instanceof ServerError) {
78+
return new Response(JSON.stringify(error.toResponseObject()), {
79+
status: 500,
80+
headers: {
81+
"Content-Type": "application/json"
82+
}
83+
});
84+
} else {
85+
console.error("Unexpected error authenticating bearer token:", error);
86+
const serverError = new ServerError("Internal Server Error");
87+
return new Response(JSON.stringify(serverError.toResponseObject()), {
88+
status: 500,
89+
headers: {
90+
"Content-Type": "application/json"
91+
}
92+
});
93+
}
94+
}
3995
};
4096
}

src/next/mcp-api-handler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
} from "../lib/log-helper";
2121
import { createEvent } from "../lib/log-helper";
2222
import { EventEmittingResponse } from "../lib/event-emitter.js";
23+
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types";
2324

2425
interface SerializedRequest {
2526
requestId: string;
@@ -310,7 +311,9 @@ export function initializeMcpApiHandler(
310311
url: req.url,
311312
headers: Object.fromEntries(req.headers),
312313
body: bodyContent,
314+
auth: (req as any).auth, // Use the auth info that should already be set by withMcpAuth
313315
});
316+
314317

315318
// Create a response that will emit events
316319
const wrappedRes = new EventEmittingResponse(
@@ -618,19 +621,21 @@ interface FakeIncomingMessageOptions {
618621
url?: string;
619622
headers?: IncomingHttpHeaders;
620623
body?: BodyType;
624+
auth?: AuthInfo;
621625
socket?: Socket;
622626
}
623627

624628
// Create a fake IncomingMessage
625629
function createFakeIncomingMessage(
626630
options: FakeIncomingMessageOptions = {}
627-
): IncomingMessage {
631+
): IncomingMessage & { auth?: AuthInfo } {
628632
const {
629633
method = "GET",
630634
url = "/",
631635
headers = {},
632636
body = null,
633637
socket = new Socket(),
638+
auth,
634639
} = options;
635640

636641
// Create a readable stream that will be used as the base for IncomingMessage
@@ -654,12 +659,15 @@ function createFakeIncomingMessage(
654659
}
655660

656661
// Create the IncomingMessage instance
657-
const req = new IncomingMessage(socket);
662+
const req = new IncomingMessage(socket) as IncomingMessage & { auth?: AuthInfo };
658663

659664
// Set the properties
660665
req.method = method;
661666
req.url = url;
662667
req.headers = headers;
668+
if (auth) {
669+
req.auth = auth;
670+
}
663671

664672
// Copy over the stream methods
665673
req.push = readable.push.bind(readable);

0 commit comments

Comments
 (0)