MCP Server Middleware
AgenticDrop-in JWT verification and authorization for your MCP server. One import, two function calls — every tool request is verified against Astapa's JWKS endpoint with automatic key caching and rotation handling.
1Initialize the auth instance
Create the auth instance once at startup. It manages JWKS fetching, caching, and key rotation internally — you never touch keys directly.
import { createMcpAuth, extractBearerToken } from "@/lib/mcp-auth";
const mcpAuth = createMcpAuth({
jwksUrl: "https://auth.astapa.com/.well-known/jwks.json",
issuer: "auth.astapa.com",
audience: "your-mcp-server-id",
cacheTtlMs: 10 * 60 * 1000, // 10 minutes
});mcpAuth once in a shared module. It's stateless between requests but caches JWKS keys in memory for performance.2Verify and authorize requests
In your tool handler, extract the bearer token and run verification + authorization in a single call. The middleware checks the JWT signature, expiry, issuer, audience, scopes, and plan — all at once.
export async function POST(request: NextRequest) {
const token = extractBearerToken(
request.headers.get("authorization")
);
if (!token) {
return NextResponse.json(
{ error: "Missing Authorization header" },
{ status: 401 }
);
}
const result = await mcpAuth.verifyAndAuthorize(token, {
requiredScopes: ["tool:write"],
allowedPlans: ["pro", "enterprise"],
});
if (!result.authorized) {
const status = result.error.includes("Missing required scopes")
|| result.error.includes("not allowed")
? 403 : 401;
return NextResponse.json(
{ error: result.error },
{ status }
);
}
// Access verified claims
const { org_id, plan, scopes } = result.payload;
// Your tool logic here...
return NextResponse.json({ success: true });
}3Granular control (optional)
If you need to separate verification from authorization — for example, to verify first and then apply different authorization rules per tool — use the individual methods:
// Step 1: Verify the token (signature, expiry, issuer, audience)
const verifyResult = await mcpAuth.verify(token);
if (!verifyResult.valid) {
return NextResponse.json({ error: verifyResult.error }, { status: 401 });
}
// Step 2: Authorize against requirements
const authResult = mcpAuth.authorize(verifyResult.payload, {
requiredScopes: ["tool:read"],
allowedPlans: ["pro", "enterprise"],
});
if (!authResult.authorized) {
return NextResponse.json({ error: authResult.error }, { status: 403 });
}4API reference
createMcpAuth(config)
Creates an auth instance with three methods:
| Method | Returns | Description |
|---|---|---|
verify(token) | VerifyResult | Verify JWT signature, issuer, audience, and expiry |
authorize(payload, reqs) | AuthorizeResult | Check scopes and plan against requirements |
verifyAndAuthorize(token, reqs) | AuthResult | Combined verify + authorize in one call |
extractBearerToken(header)
Extracts the token string from an Authorization: Bearer ... header. Returns null if missing or malformed.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
jwksUrl | string | — | URL to the JWKS endpoint (required) |
issuer | string | — | Expected JWT iss claim (required) |
audience | string | — | Your MCP server's identifier (required) |
cacheTtlMs | number | 600000 | How long to cache JWKS keys (ms) |
5JWKS caching behavior
You don't manage keys. The middleware handles everything:
Keys cached by kid, returned instantly if TTL hasn't expired
Unknown kid triggers a JWKS refetch — handles rotation seamlessly
Minimum 5 seconds between refetches to prevent hammering the JWKS endpoint
Works out of the box. Override cacheTtlMs only if you need to
6Error handling
The middleware returns descriptive error strings you can forward directly to the client:
| Error | HTTP | What happened |
|---|---|---|
| Token expired | 401 | JWT exp is in the past — client needs to refresh |
| Invalid signature | 401 | RS256 verification failed — token was tampered with or from wrong issuer |
| Issuer mismatch | 401 | iss doesn't match your config |
| Audience mismatch | 401 | aud doesn't match — token was issued for a different server |
| Missing required scopes | 403 | Token is valid but lacks the scopes this tool requires |
| Plan not allowed | 403 | Token's plan isn't in the allowedPlans list |