diff --git a/src/server/auth/clients.ts b/src/server/auth/clients.ts index a42935aa..30e4085d 100644 --- a/src/server/auth/clients.ts +++ b/src/server/auth/clients.ts @@ -12,7 +12,7 @@ export interface OAuthRegisteredClientsStore { /** * Registers a new client with the server. The client ID and secret will be automatically generated by the library. A modified version of the client information can be returned to reflect specific values enforced by the server. * - * NOTE: Implements must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field. + * NOTE: Implementations must ensure that client secrets, if present, are expired in accordance with the `client_secret_expires_at` field. * * If unimplemented, dynamic client registration is unsupported. */ diff --git a/src/server/auth/handlers/token.ts b/src/server/auth/handlers/token.ts index 553f86ac..56a3a475 100644 --- a/src/server/auth/handlers/token.ts +++ b/src/server/auth/handlers/token.ts @@ -3,14 +3,13 @@ import express, { RequestHandler } from "express"; import { OAuthServerProvider } from "../provider.js"; import cors from "cors"; import { verifyChallenge } from "pkce-challenge"; +import { authenticateClient } from "../middleware/clientAuth.js"; export type TokenHandlerOptions = { provider: OAuthServerProvider; }; const TokenRequestSchema = z.object({ - client_id: z.string(), - client_secret: z.string().optional(), grant_type: z.string(), }); @@ -32,10 +31,13 @@ export function tokenHandler({ provider }: TokenHandlerOptions): RequestHandler // Configure CORS to allow any origin, to make accessible to web-based MCP clients router.use(cors()); + // Authenticate and extract client details + router.use(authenticateClient({ clientsStore: provider.clientsStore })); + router.post("/", async (req, res) => { - let client_id, client_secret, grant_type; + let grant_type; try { - ({ client_id, client_secret, grant_type } = TokenRequestSchema.parse(req.body)); + ({ grant_type } = TokenRequestSchema.parse(req.body)); } catch (error) { res.status(400).json({ error: "invalid_request", @@ -44,24 +46,6 @@ export function tokenHandler({ provider }: TokenHandlerOptions): RequestHandler return; } - const client = await provider.clientsStore.getClient(client_id); - if (!client) { - // TODO: Return 401 with WWW-Authenticate if Authorization header was used - res.status(400).json({ - error: "invalid_client", - error_description: "Invalid client_id", - }); - return; - } - - if (client.client_secret !== client_secret) { - res.status(400).json({ - error: "invalid_client", - error_description: "Invalid client_secret", - }); - return; - } - switch (grant_type) { case "authorization_code": { let grant; diff --git a/src/server/auth/middleware/clientAuth.ts b/src/server/auth/middleware/clientAuth.ts new file mode 100644 index 00000000..12759c2f --- /dev/null +++ b/src/server/auth/middleware/clientAuth.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { RequestHandler } from "express"; +import { OAuthRegisteredClientsStore } from "../clients.js"; +import { OAuthClientInformationFull } from "../../../shared/auth.js"; + +export type ClientAuthenticationMiddlewareOptions = { + /** + * A store used to read information about registered OAuth clients. + */ + clientsStore: OAuthRegisteredClientsStore; +} + +const ClientAuthenticatedRequestSchema = z.object({ + client_id: z.string(), + client_secret: z.string().optional(), +}) + +declare global { + namespace Express { + interface Request { + /** + * The authenticated client for this request, if the `authenticateClient` middleware was used. + */ + client?: OAuthClientInformationFull; + } + } +} + +export function authenticateClient({ clientsStore }: ClientAuthenticationMiddlewareOptions): RequestHandler { + return async (req, res, next) => { + let client_id, client_secret; + try { + const result = ClientAuthenticatedRequestSchema.parse(req.body); + client_id = result.client_id; + client_secret = result.client_secret; + } catch (error) { + res.status(400).json({ + error: "invalid_request", + error_description: String(error), + }); + return; + } + + const client = await clientsStore.getClient(client_id); + if (!client) { + // TODO: Return 401 with WWW-Authenticate if Authorization header was used + res.status(400).json({ + error: "invalid_client", + error_description: "Invalid client_id", + }); + return; + } + + if (client.client_secret !== client_secret) { + res.status(400).json({ + error: "invalid_client", + error_description: "Invalid client_secret", + }); + return; + } + + req.client = client; + next(); + } +} \ No newline at end of file diff --git a/src/server/auth/provider.ts b/src/server/auth/provider.ts index c9cf4362..67de4bc1 100644 --- a/src/server/auth/provider.ts +++ b/src/server/auth/provider.ts @@ -33,6 +33,7 @@ export interface OAuthServerProvider { /** * Returns the `codeChallenge` that was used when the indicated authorization began. */ + // TODO: Add `client` to this and below methods challengeForAuthorizationCode(authorizationCode: string): Promise; /**