Skip to main content
Glama

Sentry MCP

Official
by getsentry
callback.test.ts7.51 kB
import { describe, it, expect, vi, beforeEach } from "vitest"; import { Hono } from "hono"; import oauthRoute from "./index"; import { signState, type OAuthState } from "./state"; import type { Env } from "../types"; // Mock the OAuth provider const mockOAuthProvider = { parseAuthRequest: vi.fn(), lookupClient: vi.fn(), completeAuthorization: vi.fn(), }; function createTestApp(env: Partial<Env> = {}) { const app = new Hono<{ Bindings: Env }>(); app.route("/oauth", oauthRoute); return app; } describe("oauth callback routes", () => { let app: ReturnType<typeof createTestApp>; let testEnv: Partial<Env>; beforeEach(() => { vi.clearAllMocks(); testEnv = { OAUTH_PROVIDER: mockOAuthProvider as unknown as Env["OAUTH_PROVIDER"], COOKIE_SECRET: "test-secret-key", SENTRY_CLIENT_ID: "test-client-id", SENTRY_CLIENT_SECRET: "test-client-secret", SENTRY_HOST: "sentry.io", }; app = createTestApp(testEnv); }); describe("GET /oauth/callback", () => { it("should reject callback with invalid state param", async () => { const request = new Request( `http://localhost/oauth/callback?code=test-code&state=%%%INVALID%%%`, { method: "GET" }, ); const response = await app.fetch(request, testEnv as Env); expect(response.status).toBe(400); const text = await response.text(); expect(text).toBe("Invalid state"); }); it("should reject callback without approved client cookie", async () => { // Build signed state matching what /oauth/authorize issues const now = Date.now(); const payload: OAuthState = { req: { clientId: "test-client", redirectUri: "https://example.com/callback", scope: ["read"], }, iat: now, exp: now + 10 * 60 * 1000, } as unknown as OAuthState; const signedState = await signState(payload, testEnv.COOKIE_SECRET!); const request = new Request( `http://localhost/oauth/callback?code=test-code&state=${signedState}`, { method: "GET", headers: {}, }, ); const response = await app.fetch(request, testEnv as Env); expect(response.status).toBe(403); const text = await response.text(); expect(text).toBe("Authorization failed: Client not approved"); }); it("should reject callback with invalid client approval cookie", async () => { const now = Date.now(); const payload: OAuthState = { req: { clientId: "test-client", redirectUri: "https://example.com/callback", scope: ["read"], }, iat: now, exp: now + 10 * 60 * 1000, } as unknown as OAuthState; const signedState = await signState(payload, testEnv.COOKIE_SECRET!); const request = new Request( `http://localhost/oauth/callback?code=test-code&state=${signedState}`, { method: "GET", headers: { Cookie: "mcp-approved-clients=invalid-cookie-value", }, }, ); const response = await app.fetch(request, testEnv as Env); expect(response.status).toBe(403); const text = await response.text(); expect(text).toBe("Authorization failed: Client not approved"); }); it("should reject callback with cookie for different client", async () => { // Ensure authorize POST accepts the redirectUri mockOAuthProvider.lookupClient.mockResolvedValueOnce({ clientId: "different-client", clientName: "Other Client", redirectUris: ["https://example.com/callback"], tokenEndpointAuthMethod: "client_secret_basic", }); const approvalFormData = new FormData(); approvalFormData.append( "state", btoa( JSON.stringify({ oauthReqInfo: { clientId: "different-client", redirectUri: "https://example.com/callback", scope: ["read"], }, }), ), ); const approvalRequest = new Request("http://localhost/oauth/authorize", { method: "POST", body: approvalFormData, }); const approvalResponse = await app.fetch(approvalRequest, testEnv as Env); expect(approvalResponse.status).toBe(302); const setCookie = approvalResponse.headers.get("Set-Cookie"); expect(setCookie).toBeTruthy(); // Build a signed state for a different client than the approved one const now = Date.now(); const payload: OAuthState = { req: { clientId: "test-client", redirectUri: "https://example.com/callback", scope: ["read"], }, iat: now, exp: now + 10 * 60 * 1000, } as unknown as OAuthState; const signedState = await signState(payload, testEnv.COOKIE_SECRET!); const request = new Request( `http://localhost/oauth/callback?code=test-code&state=${signedState}`, { method: "GET", headers: { Cookie: setCookie!.split(";")[0], }, }, ); const response = await app.fetch(request, testEnv as Env); expect(response.status).toBe(403); const text = await response.text(); expect(text).toBe("Authorization failed: Client not approved"); }); it("should reject callback when state signature is tampered", async () => { // Ensure client redirectUri is registered mockOAuthProvider.lookupClient.mockResolvedValueOnce({ clientId: "test-client", clientName: "Test Client", redirectUris: ["https://example.com/callback"], tokenEndpointAuthMethod: "client_secret_basic", }); // Prepare approval POST to generate a signed state const oauthReqInfo = { clientId: "test-client", redirectUri: "https://example.com/callback", scope: ["read"], }; const approvalFormData = new FormData(); approvalFormData.append( "state", btoa( JSON.stringify({ oauthReqInfo, }), ), ); const approvalRequest = new Request("http://localhost/oauth/authorize", { method: "POST", body: approvalFormData, }); const approvalResponse = await app.fetch(approvalRequest, testEnv as Env); expect(approvalResponse.status).toBe(302); const setCookie = approvalResponse.headers.get("Set-Cookie"); const location = approvalResponse.headers.get("location"); expect(location).toBeTruthy(); const redirectUrl = new URL(location!); const signedState = redirectUrl.searchParams.get("state")!; // Tamper with the signature portion (hex) without breaking payload parsing const [sig, b64] = signedState.split("."); const badSig = (sig[0] === "a" ? "b" : "a") + sig.slice(1); const tamperedState = `${badSig}.${b64}`; // Call callback with tampered state and valid approval cookie const callbackRequest = new Request( `http://localhost/oauth/callback?code=test-code&state=${tamperedState}`, { method: "GET", headers: { Cookie: setCookie!.split(";")[0], }, }, ); const response = await app.fetch(callbackRequest, testEnv as Env); expect(response.status).toBe(400); const text = await response.text(); expect(text).toBe("Invalid state"); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/getsentry/sentry-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server