UNPKG

@modelcontextprotocol/sdk

Version:

Model Context Protocol implementation for TypeScript

143 lines 6.6 kB
import { z } from "zod"; import express from "express"; import { rateLimit } from "express-rate-limit"; import { allowedMethods } from "../middleware/allowedMethods.js"; import { InvalidRequestError, InvalidClientError, InvalidScopeError, ServerError, TooManyRequestsError, OAuthError } from "../errors.js"; // Parameters that must be validated in order to issue redirects. const ClientAuthorizationParamsSchema = z.object({ client_id: z.string(), redirect_uri: z.string().optional().refine((value) => value === undefined || URL.canParse(value), { message: "redirect_uri must be a valid URL" }), }); // Parameters that must be validated for a successful authorization request. Failure can be reported to the redirect URI. const RequestAuthorizationParamsSchema = z.object({ response_type: z.literal("code"), code_challenge: z.string(), code_challenge_method: z.literal("S256"), scope: z.string().optional(), state: z.string().optional(), resource: z.string().url().optional(), }); export function authorizationHandler({ provider, rateLimit: rateLimitConfig }) { // Create a router to apply middleware const router = express.Router(); router.use(allowedMethods(["GET", "POST"])); router.use(express.urlencoded({ extended: false })); // Apply rate limiting unless explicitly disabled if (rateLimitConfig !== false) { router.use(rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per windowMs standardHeaders: true, legacyHeaders: false, message: new TooManyRequestsError('You have exceeded the rate limit for authorization requests').toResponseObject(), ...rateLimitConfig })); } router.all("/", async (req, res) => { var _a; res.setHeader('Cache-Control', 'no-store'); // In the authorization flow, errors are split into two categories: // 1. Pre-redirect errors (direct response with 400) // 2. Post-redirect errors (redirect with error parameters) // Phase 1: Validate client_id and redirect_uri. Any errors here must be direct responses. let client_id, redirect_uri, client; try { const result = ClientAuthorizationParamsSchema.safeParse(req.method === 'POST' ? req.body : req.query); if (!result.success) { throw new InvalidRequestError(result.error.message); } client_id = result.data.client_id; redirect_uri = result.data.redirect_uri; client = await provider.clientsStore.getClient(client_id); if (!client) { throw new InvalidClientError("Invalid client_id"); } if (redirect_uri !== undefined) { if (!client.redirect_uris.includes(redirect_uri)) { throw new InvalidRequestError("Unregistered redirect_uri"); } } else if (client.redirect_uris.length === 1) { redirect_uri = client.redirect_uris[0]; } else { throw new InvalidRequestError("redirect_uri must be specified when client has multiple registered URIs"); } } catch (error) { // Pre-redirect errors - return direct response // // These don't need to be JSON encoded, as they'll be displayed in a user // agent, but OTOH they all represent exceptional situations (arguably, // "programmer error"), so presenting a nice HTML page doesn't help the // user anyway. if (error instanceof OAuthError) { const status = error instanceof ServerError ? 500 : 400; res.status(status).json(error.toResponseObject()); } else { const serverError = new ServerError("Internal Server Error"); res.status(500).json(serverError.toResponseObject()); } return; } // Phase 2: Validate other parameters. Any errors here should go into redirect responses. let state; try { // Parse and validate authorization parameters const parseResult = RequestAuthorizationParamsSchema.safeParse(req.method === 'POST' ? req.body : req.query); if (!parseResult.success) { throw new InvalidRequestError(parseResult.error.message); } const { scope, code_challenge, resource } = parseResult.data; state = parseResult.data.state; // Validate scopes let requestedScopes = []; if (scope !== undefined) { requestedScopes = scope.split(" "); const allowedScopes = new Set((_a = client.scope) === null || _a === void 0 ? void 0 : _a.split(" ")); // Check each requested scope against allowed scopes for (const scope of requestedScopes) { if (!allowedScopes.has(scope)) { throw new InvalidScopeError(`Client was not registered with scope ${scope}`); } } } // All validation passed, proceed with authorization await provider.authorize(client, { state, scopes: requestedScopes, redirectUri: redirect_uri, codeChallenge: code_challenge, resource: resource ? new URL(resource) : undefined, }, res); } catch (error) { // Post-redirect errors - redirect with error parameters if (error instanceof OAuthError) { res.redirect(302, createErrorRedirect(redirect_uri, error, state)); } else { const serverError = new ServerError("Internal Server Error"); res.redirect(302, createErrorRedirect(redirect_uri, serverError, state)); } } }); return router; } /** * Helper function to create redirect URL with error parameters */ function createErrorRedirect(redirectUri, error, state) { const errorUrl = new URL(redirectUri); errorUrl.searchParams.set("error", error.errorCode); errorUrl.searchParams.set("error_description", error.message); if (error.errorUri) { errorUrl.searchParams.set("error_uri", error.errorUri); } if (state) { errorUrl.searchParams.set("state", state); } return errorUrl.href; } //# sourceMappingURL=authorize.js.map