Extract client authentication into reusable middleware

This commit is contained in:
Justin Spahr-Summers 2025-02-18 09:39:52 +00:00
commit 19f2ac7018
No known key found for this signature in database
4 changed files with 73 additions and 23 deletions

View file

@ -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.
*/

View file

@ -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;

View file

@ -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();
}
}

View file

@ -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<string>;
/**