Skip to content

Commit 1415c2d

Browse files
committed
Rate limiting on individual handlers
1 parent 4d02ed9 commit 1415c2d

File tree

5 files changed

+106
-9
lines changed

5 files changed

+106
-9
lines changed

src/server/auth/handlers/authorize.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ describe('Authorization Handler', () => {
8484
beforeEach(() => {
8585
app = express();
8686
options = { provider: mockProvider };
87-
app.get('/authorize', authorizationHandler(options));
88-
app.post('/authorize', authorizationHandler(options));
87+
const handler = authorizationHandler(options);
88+
app.use('/authorize', handler);
8989
});
9090

9191
describe('HTTP method validation', () => {
@@ -94,7 +94,7 @@ describe('Authorization Handler', () => {
9494
.put('/authorize')
9595
.query({ client_id: 'valid-client' });
9696

97-
expect(response.status).toBe(404); // Express filtering before reaching handler
97+
expect(response.status).toBe(405); // Method not allowed response from handler
9898
});
9999
});
100100

src/server/auth/handlers/authorize.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { RequestHandler } from "express";
22
import { z } from "zod";
3+
import express from "express";
34
import { OAuthServerProvider } from "../provider.js";
5+
import { rateLimit, Options as RateLimitOptions } from "express-rate-limit";
46

57
export type AuthorizationHandlerOptions = {
68
provider: OAuthServerProvider;
9+
/**
10+
* Rate limiting configuration for the authorization endpoint.
11+
* Set to false to disable rate limiting for this endpoint.
12+
*/
13+
rateLimit?: Partial<RateLimitOptions> | false;
714
};
815

916
// Parameters that must be validated in order to issue redirects.
@@ -21,8 +28,27 @@ const RequestAuthorizationParamsSchema = z.object({
2128
state: z.string().optional(),
2229
});
2330

24-
export function authorizationHandler({ provider }: AuthorizationHandlerOptions): RequestHandler {
25-
return async (req, res) => {
31+
export function authorizationHandler({ provider, rateLimit: rateLimitConfig }: AuthorizationHandlerOptions): RequestHandler {
32+
// Create a router to apply middleware
33+
const router = express.Router();
34+
35+
// Apply rate limiting unless explicitly disabled
36+
if (rateLimitConfig !== false) {
37+
router.use(rateLimit({
38+
windowMs: 15 * 60 * 1000, // 15 minutes
39+
max: 100, // 100 requests per windowMs
40+
standardHeaders: true,
41+
legacyHeaders: false,
42+
message: {
43+
error: 'too_many_requests',
44+
error_description: 'You have exceeded the rate limit for authorization requests'
45+
},
46+
...rateLimitConfig
47+
}));
48+
}
49+
50+
// Define the handler
51+
router.all("/", async (req, res) => {
2652
if (req.method !== "GET" && req.method !== "POST") {
2753
res.status(405).end("Method Not Allowed");
2854
return;
@@ -88,5 +114,7 @@ export function authorizationHandler({ provider }: AuthorizationHandlerOptions):
88114
redirectUri: redirect_uri,
89115
codeChallenge: params.code_challenge,
90116
}, res);
91-
};
117+
});
118+
119+
return router;
92120
}

src/server/auth/handlers/register.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { OAuthClientInformationFull, OAuthClientMetadataSchema } from "../../../
33
import crypto from 'node:crypto';
44
import cors from 'cors';
55
import { OAuthRegisteredClientsStore } from "../clients.js";
6+
import { rateLimit, Options as RateLimitOptions } from "express-rate-limit";
67

78
export type ClientRegistrationHandlerOptions = {
89
/**
@@ -16,11 +17,22 @@ export type ClientRegistrationHandlerOptions = {
1617
* If not set, defaults to 30 days.
1718
*/
1819
clientSecretExpirySeconds?: number;
20+
21+
/**
22+
* Rate limiting configuration for the client registration endpoint.
23+
* Set to false to disable rate limiting for this endpoint.
24+
* Registration endpoints are particularly sensitive to abuse and should be rate limited.
25+
*/
26+
rateLimit?: Partial<RateLimitOptions> | false;
1927
};
2028

2129
const DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS = 30 * 24 * 60 * 60; // 30 days
2230

23-
export function clientRegistrationHandler({ clientsStore, clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS }: ClientRegistrationHandlerOptions): RequestHandler {
31+
export function clientRegistrationHandler({
32+
clientsStore,
33+
clientSecretExpirySeconds = DEFAULT_CLIENT_SECRET_EXPIRY_SECONDS,
34+
rateLimit: rateLimitConfig
35+
}: ClientRegistrationHandlerOptions): RequestHandler {
2436
if (!clientsStore.registerClient) {
2537
throw new Error("Client registration store does not support registering clients");
2638
}
@@ -31,6 +43,21 @@ export function clientRegistrationHandler({ clientsStore, clientSecretExpirySeco
3143

3244
// Configure CORS to allow any origin, to make accessible to web-based MCP clients
3345
router.use(cors());
46+
47+
// Apply rate limiting unless explicitly disabled - stricter limits for registration
48+
if (rateLimitConfig !== false) {
49+
router.use(rateLimit({
50+
windowMs: 60 * 60 * 1000, // 1 hour
51+
max: 20, // 20 requests per hour - stricter as registration is sensitive
52+
standardHeaders: true,
53+
legacyHeaders: false,
54+
message: {
55+
error: 'too_many_requests',
56+
error_description: 'You have exceeded the rate limit for client registration requests'
57+
},
58+
...rateLimitConfig
59+
}));
60+
}
3461

3562
router.post("/", async (req, res) => {
3663
let clientMetadata;

src/server/auth/handlers/revoke.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@ import express, { RequestHandler } from "express";
33
import cors from "cors";
44
import { authenticateClient } from "../middleware/clientAuth.js";
55
import { OAuthTokenRevocationRequestSchema } from "../../../shared/auth.js";
6+
import { rateLimit, Options as RateLimitOptions } from "express-rate-limit";
67

78
export type RevocationHandlerOptions = {
89
provider: OAuthServerProvider;
10+
/**
11+
* Rate limiting configuration for the token revocation endpoint.
12+
* Set to false to disable rate limiting for this endpoint.
13+
*/
14+
rateLimit?: Partial<RateLimitOptions> | false;
915
};
1016

11-
export function revocationHandler({ provider }: RevocationHandlerOptions): RequestHandler {
17+
export function revocationHandler({ provider, rateLimit: rateLimitConfig }: RevocationHandlerOptions): RequestHandler {
1218
if (!provider.revokeToken) {
1319
throw new Error("Auth provider does not support revoking tokens");
1420
}
@@ -19,6 +25,21 @@ export function revocationHandler({ provider }: RevocationHandlerOptions): Reque
1925

2026
// Configure CORS to allow any origin, to make accessible to web-based MCP clients
2127
router.use(cors());
28+
29+
// Apply rate limiting unless explicitly disabled
30+
if (rateLimitConfig !== false) {
31+
router.use(rateLimit({
32+
windowMs: 15 * 60 * 1000, // 15 minutes
33+
max: 50, // 50 requests per windowMs
34+
standardHeaders: true,
35+
legacyHeaders: false,
36+
message: {
37+
error: 'too_many_requests',
38+
error_description: 'You have exceeded the rate limit for token revocation requests'
39+
},
40+
...rateLimitConfig
41+
}));
42+
}
2243

2344
// Authenticate and extract client details
2445
router.use(authenticateClient({ clientsStore: provider.clientsStore }));

src/server/auth/handlers/token.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import { OAuthServerProvider } from "../provider.js";
44
import cors from "cors";
55
import { verifyChallenge } from "pkce-challenge";
66
import { authenticateClient } from "../middleware/clientAuth.js";
7+
import { rateLimit, Options as RateLimitOptions } from "express-rate-limit";
78

89
export type TokenHandlerOptions = {
910
provider: OAuthServerProvider;
11+
/**
12+
* Rate limiting configuration for the token endpoint.
13+
* Set to false to disable rate limiting for this endpoint.
14+
*/
15+
rateLimit?: Partial<RateLimitOptions> | false;
1016
};
1117

1218
const TokenRequestSchema = z.object({
@@ -23,13 +29,28 @@ const RefreshTokenGrantSchema = z.object({
2329
scope: z.string().optional(),
2430
});
2531

26-
export function tokenHandler({ provider }: TokenHandlerOptions): RequestHandler {
32+
export function tokenHandler({ provider, rateLimit: rateLimitConfig }: TokenHandlerOptions): RequestHandler {
2733
// Nested router so we can configure middleware and restrict HTTP method
2834
const router = express.Router();
2935
router.use(express.urlencoded({ extended: false }));
3036

3137
// Configure CORS to allow any origin, to make accessible to web-based MCP clients
3238
router.use(cors());
39+
40+
// Apply rate limiting unless explicitly disabled
41+
if (rateLimitConfig !== false) {
42+
router.use(rateLimit({
43+
windowMs: 15 * 60 * 1000, // 15 minutes
44+
max: 50, // 50 requests per windowMs
45+
standardHeaders: true,
46+
legacyHeaders: false,
47+
message: {
48+
error: 'too_many_requests',
49+
error_description: 'You have exceeded the rate limit for token requests'
50+
},
51+
...rateLimitConfig
52+
}));
53+
}
3354

3455
// Authenticate and extract client details
3556
router.use(authenticateClient({ clientsStore: provider.clientsStore }));

0 commit comments

Comments
 (0)