Skip to content

Commit 4605b9a

Browse files
authored
feat: Allow verifyToken to throw errors (#63)
1 parent 1a5d56c commit 4605b9a

File tree

2 files changed

+63
-16
lines changed

2 files changed

+63
-16
lines changed

src/next/auth-wrapper.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
1+
import {AuthInfo} from "@modelcontextprotocol/sdk/server/auth/types.js";
22
import {
33
InvalidTokenError,
44
InsufficientScopeError,
55
ServerError,
66
} from "@modelcontextprotocol/sdk/server/auth/errors.js";
7-
import { withAuthContext } from "./auth-context";
7+
import {withAuthContext} from "./auth-context";
88

99
declare global {
1010
interface Request {
@@ -29,16 +29,32 @@ export function withMcpAuth(
2929
} = {}
3030
) {
3131
return async (req: Request) => {
32-
try {
33-
const authHeader = req.headers.get("Authorization");
34-
const [type, token] = authHeader?.split(" ") || [];
32+
const origin = new URL(req.url).origin;
33+
const resourceMetadataUrl = `${origin}${resourceMetadataPath}`;
3534

36-
// Only support bearer token as per the MCP spec
37-
// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-6-1-token-requirements
38-
const bearerToken = type?.toLowerCase() === "bearer" ? token : undefined;
35+
const authHeader = req.headers.get("Authorization");
36+
const [type, token] = authHeader?.split(" ") || [];
3937

40-
const authInfo = await verifyToken(req, bearerToken);
38+
// Only support bearer token as per the MCP spec
39+
// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#2-6-1-token-requirements
40+
const bearerToken = type?.toLowerCase() === "bearer" ? token : undefined;
4141

42+
let authInfo: AuthInfo | undefined;
43+
try {
44+
authInfo = await verifyToken(req, bearerToken);
45+
} catch (error) {
46+
console.error("Unexpected error authenticating bearer token:", error);
47+
const publicError = new InvalidTokenError("Invalid token");
48+
return new Response(JSON.stringify(publicError.toResponseObject()), {
49+
status: 401,
50+
headers: {
51+
"WWW-Authenticate": `Bearer error="${publicError.errorCode}", error_description="${publicError.message}", resource_metadata="${resourceMetadataUrl}"`,
52+
"Content-Type": "application/json",
53+
},
54+
});
55+
}
56+
57+
try {
4258
if (required && !authInfo) {
4359
throw new InvalidTokenError("No authorization provided");
4460
}
@@ -50,7 +66,7 @@ export function withMcpAuth(
5066
// Check if token has the required scopes (if any)
5167
if (requiredScopes?.length) {
5268
const hasAllScopes = requiredScopes.every((scope) =>
53-
authInfo.scopes.includes(scope)
69+
authInfo!.scopes.includes(scope)
5470
);
5571

5672
if (!hasAllScopes) {
@@ -68,9 +84,6 @@ export function withMcpAuth(
6884

6985
return withAuthContext(authInfo, () => handler(req));
7086
} catch (error) {
71-
const origin = new URL(req.url).origin;
72-
const resourceMetadataUrl = `${origin}${resourceMetadataPath}`;
73-
7487
if (error instanceof InvalidTokenError) {
7588
return new Response(JSON.stringify(error.toResponseObject()), {
7689
status: 401,

tests/e2e.test.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
1+
import {describe, it, expect, beforeEach, afterEach, vi, Mock} from "vitest";
22
import { z } from "zod";
33
import {
44
createServer,
@@ -16,6 +16,7 @@ describe("e2e", () => {
1616
let server: Server;
1717
let endpoint: string;
1818
let client: Client;
19+
let verifyTokenMock: Mock;
1920

2021
beforeEach(async () => {
2122
const _mcpHandler = createMcpHandler(
@@ -55,7 +56,7 @@ describe("e2e", () => {
5556
}
5657
);
5758

58-
const mcpHandler = withMcpAuth(_mcpHandler, (req) => {
59+
verifyTokenMock = vi.fn((req) => {
5960
const header = req.headers.get("Authorization");
6061
if (header?.startsWith("Bearer ")) {
6162
const token = header.slice(7).trim();
@@ -66,7 +67,9 @@ describe("e2e", () => {
6667
});
6768
}
6869
return undefined;
69-
});
70+
})
71+
72+
const mcpHandler = withMcpAuth(_mcpHandler, verifyTokenMock);
7073

7174
server = createServer(nodeToWebHandler(mcpHandler));
7275
await new Promise<void>((resolve) => {
@@ -173,6 +176,37 @@ describe("e2e", () => {
173176
"Tool echo: Are you there? for ACCESS_TOKEN"
174177
);
175178
});
179+
180+
it("should return an invalid token error when verifyToken fails", async () => {
181+
const authenticatedTransport = new StreamableHTTPClientTransport(
182+
new URL(`${endpoint}/mcp`),
183+
{
184+
requestInit: {
185+
headers: {
186+
Authorization: `Bearer ACCESS_TOKEN`,
187+
},
188+
},
189+
}
190+
);
191+
const authenticatedClient = new Client(
192+
{
193+
name: "example-client",
194+
version: "1.0.0",
195+
},
196+
{
197+
capabilities: {
198+
prompts: {},
199+
resources: {},
200+
tools: {},
201+
},
202+
}
203+
);
204+
verifyTokenMock.mockImplementation(() => {
205+
throw new Error('JWT signature failed, or something')
206+
})
207+
208+
expect(() => authenticatedClient.connect(authenticatedTransport)).rejects.toThrow('Invalid token')
209+
});
176210
});
177211

178212
function nodeToWebHandler(

0 commit comments

Comments
 (0)