Docs/MCP Auth/Middleware

MCP Server Middleware

Agentic

Drop-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.

mcp-auth.tstypescript
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
});
One instance, many requests
Create 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.

handler.tstypescript
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 });
}
401 vs 403
Return 401 for invalid/expired tokens (identity problem). Return 403 for valid tokens that lack required scopes or plan (permission problem).

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:

granular.tstypescript
// 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:

MethodReturnsDescription
verify(token)VerifyResultVerify JWT signature, issuer, audience, and expiry
authorize(payload, reqs)AuthorizeResultCheck scopes and plan against requirements
verifyAndAuthorize(token, reqs)AuthResultCombined verify + authorize in one call

extractBearerToken(header)

Extracts the token string from an Authorization: Bearer ... header. Returns null if missing or malformed.

Config options

OptionTypeDefaultDescription
jwksUrlstring—URL to the JWKS endpoint (required)
issuerstring—Expected JWT iss claim (required)
audiencestring—Your MCP server's identifier (required)
cacheTtlMsnumber600000How long to cache JWKS keys (ms)

5JWKS caching behavior

You don't manage keys. The middleware handles everything:

In-memory cache

Keys cached by kid, returned instantly if TTL hasn't expired

Auto key rotation

Unknown kid triggers a JWKS refetch — handles rotation seamlessly

Rate limiting

Minimum 5 seconds between refetches to prevent hammering the JWKS endpoint

Zero config

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:

ErrorHTTPWhat happened
Token expired401JWT exp is in the past — client needs to refresh
Invalid signature401RS256 verification failed — token was tampered with or from wrong issuer
Issuer mismatch401iss doesn't match your config
Audience mismatch401aud doesn't match — token was issued for a different server
Missing required scopes403Token is valid but lacks the scopes this tool requires
Plan not allowed403Token's plan isn't in the allowedPlans list
Don't leak internals
Forward the error string to the client as-is. Don't add stack traces or internal details to auth error responses.

Next steps

API Playground
Click "Try it" on any endpoint to get started.
MCP Server Middleware — Astapa Docs | Astapa