Authentication & Authorization

Secure your MCP servers with JWT tokens, API keys, OAuth 2.1 integration, and role-based access control using the official TypeScript SDK and mcp-framework's built-in authentication support.


title: "Authentication & Authorization" description: "Secure your MCP servers with JWT tokens, API keys, OAuth 2.1 integration, and role-based access control using the official TypeScript SDK and mcp-framework's built-in authentication support." order: 17 level: "advanced" duration: "35 min" keywords:

  • "MCP authentication"
  • "MCP authorization"
  • "MCP JWT"
  • "MCP API key"
  • "MCP OAuth 2.1"
  • "MCP server security"
  • "mcp-framework authentication"
  • "@modelcontextprotocol/sdk auth"
  • "MCP access control"
  • "MCP RBAC" date: "2026-04-01"

Quick Summary

Production MCP servers must authenticate clients and authorize access to tools, resources, and prompts. This lesson covers three authentication strategies — API keys for simple use cases, JWT tokens for stateless authentication, and OAuth 2.1 for delegated authorization. You will also learn role-based access control (RBAC) patterns, how to secure both stdio and HTTP transports, and authentication features in mcp-framework and the official TypeScript SDK.

Why MCP Servers Need Authentication

An unauthenticated MCP server exposes all its tools, resources, and prompts to any client. In production, this means:

  • Data exposure — resources may contain sensitive information
  • Unauthorized actions — tools can modify data, call APIs, or access systems
  • Abuse — unauthenticated endpoints can be exploited for resource consumption
  • Compliance violations — regulated industries require access controls
Stdio Servers Need Auth Too

Even stdio-based servers can benefit from authentication. While the transport itself is local, the server may access remote APIs, databases, or services that require credential validation. Defense in depth matters.

Authentication Strategies

StrategyBest ForComplexityStatefulness
API KeyInternal tools, simple setupsLowStateless
JWTMulti-service architecturesMediumStateless
OAuth 2.1Third-party integrations, user delegationHighStateful (tokens)

API Key Authentication

The simplest approach. The client provides an API key, and the server validates it before processing requests.

Environment-Based API Keys

For stdio servers, pass the API key via environment variables:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const REQUIRED_API_KEY = process.env.MCP_API_KEY;

if (!REQUIRED_API_KEY) {
  console.error("MCP_API_KEY environment variable is required");
  process.exit(1);
}

const server = new McpServer({
  name: "authenticated-server",
  version: "1.0.0",
});

// Tools validate against a service that checks the key
server.tool(
  "query-database",
  "Execute a database query",
  {
    sql: z.string().describe("SQL query"),
    apiKey: z.string().describe("API key for authentication"),
  },
  async ({ sql, apiKey }) => {
    if (apiKey !== REQUIRED_API_KEY) {
      return {
        content: [{
          type: "text",
          text: "Authentication failed: invalid API key",
        }],
        isError: true,
      };
    }

    const results = await db.query(sql);
    return {
      content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
    };
  }
);

Header-Based API Keys for HTTP Transports

For SSE or Streamable HTTP servers, validate the API key from request headers:

import express from "express";

const app = express();

// Middleware to validate API key
function authenticateApiKey(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const apiKey = req.headers["x-api-key"] || req.query.apiKey;

  if (!apiKey || apiKey !== process.env.MCP_API_KEY) {
    res.status(401).json({
      error: "Unauthorized",
      message: "Valid API key required in X-API-Key header",
    });
    return;
  }

  next();
}

// Apply to all MCP endpoints
app.use("/sse", authenticateApiKey);
app.use("/messages", authenticateApiKey);

// SSE endpoint
app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});
Never Hardcode API Keys

Always load API keys from environment variables or a secrets manager. Never commit keys to version control. Rotate keys regularly and support multiple active keys during rotation periods.

JWT Authentication

JSON Web Tokens provide stateless authentication with embedded claims. The server validates the token signature without calling an external service.

JWT Validation Middleware

import jwt from "jsonwebtoken";

interface JWTPayload {
  sub: string;        // User ID
  roles: string[];    // User roles
  exp: number;        // Expiration timestamp
  iss: string;        // Issuer
}

const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_ISSUER = process.env.JWT_ISSUER || "mcp-auth-service";

function validateJWT(token: string): JWTPayload {
  try {
    const payload = jwt.verify(token, JWT_SECRET, {
      issuer: JWT_ISSUER,
      algorithms: ["HS256", "RS256"],
    }) as JWTPayload;

    return payload;
  } catch (error) {
    if (error instanceof jwt.TokenExpiredError) {
      throw new Error("Token expired. Please obtain a new token.");
    }
    if (error instanceof jwt.JsonWebTokenError) {
      throw new Error("Invalid token. Authentication failed.");
    }
    throw new Error("Token validation failed.");
  }
}

// Express middleware
function authenticateJWT(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    res.status(401).json({
      error: "Missing or malformed Authorization header",
    });
    return;
  }

  try {
    const token = authHeader.substring(7);
    const payload = validateJWT(token);
    (req as any).user = payload;
    next();
  } catch (error) {
    res.status(401).json({
      error: error instanceof Error ? error.message : "Authentication failed",
    });
  }
}

Per-Tool Authorization with JWT Claims

// Helper to check user roles
function requireRole(user: JWTPayload, ...roles: string[]): boolean {
  return roles.some(role => user.roles.includes(role));
}

server.tool(
  "delete-record",
  "Delete a record from the database (admin only)",
  {
    recordId: z.string().describe("Record ID to delete"),
  },
  async ({ recordId }, { meta }) => {
    // Access user info from the request context
    const user = meta?.user as JWTPayload | undefined;

    if (!user) {
      return {
        content: [{
          type: "text",
          text: "Authentication required. Please provide a valid JWT token.",
        }],
        isError: true,
      };
    }

    if (!requireRole(user, "admin", "moderator")) {
      return {
        content: [{
          type: "text",
          text: `Access denied. This action requires 'admin' or 'moderator' role. Your roles: ${user.roles.join(", ")}`,
        }],
        isError: true,
      };
    }

    await db.deleteRecord(recordId);
    return {
      content: [{
        type: "text",
        text: `Record ${recordId} deleted by ${user.sub}`,
      }],
    };
  }
);

OAuth 2.1 Integration

OAuth 2.1 for MCP

OAuth 2.1 enables delegated authorization where users grant MCP servers specific permissions to access resources on their behalf. This is the standard approach when your MCP server needs to access third-party APIs (GitHub, Google, Slack) using the user's identity.

OAuth Flow for MCP Servers

1

Client requests authorization

The MCP client detects that the server requires OAuth and redirects the user to the authorization server's consent page.

2

User grants permission

The user logs in and approves the requested scopes (e.g., "read:repos", "write:issues").

3

Server receives authorization code

The authorization server redirects back to the MCP server's callback URL with an authorization code.

4

Server exchanges code for tokens

The MCP server exchanges the authorization code for access and refresh tokens.

5

Server uses access token

Tool and resource handlers use the access token to call third-party APIs on behalf of the user.

Implementation with MCP OAuth Support

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import express from "express";

const server = new McpServer({
  name: "oauth-server",
  version: "1.0.0",
});

// Token storage (use a database in production)
const tokenStore = new Map<string, {
  accessToken: string;
  refreshToken: string;
  expiresAt: number;
  scopes: string[];
}>();

// OAuth callback handler
const app = express();

app.get("/oauth/callback", async (req, res) => {
  const { code, state } = req.query;

  // Exchange code for tokens
  const tokenResponse = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "authorization_code",
      code,
      client_id: process.env.OAUTH_CLIENT_ID,
      client_secret: process.env.OAUTH_CLIENT_SECRET,
      redirect_uri: `${process.env.SERVER_URL}/oauth/callback`,
    }),
  });

  const tokens = await tokenResponse.json();
  const sessionId = state as string;

  tokenStore.set(sessionId, {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token,
    expiresAt: Date.now() + tokens.expires_in * 1000,
    scopes: tokens.scope.split(" "),
  });

  res.send("Authorization successful. You can close this window.");
});

// Token refresh helper
async function getValidToken(sessionId: string): Promise<string> {
  const stored = tokenStore.get(sessionId);

  if (!stored) {
    throw new Error("Not authenticated. Please complete OAuth flow.");
  }

  // Refresh if expired
  if (stored.expiresAt < Date.now() + 60000) {
    const refreshResponse = await fetch("https://auth.example.com/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        grant_type: "refresh_token",
        refresh_token: stored.refreshToken,
        client_id: process.env.OAUTH_CLIENT_ID,
        client_secret: process.env.OAUTH_CLIENT_SECRET,
      }),
    });

    const newTokens = await refreshResponse.json();
    stored.accessToken = newTokens.access_token;
    stored.expiresAt = Date.now() + newTokens.expires_in * 1000;
    if (newTokens.refresh_token) {
      stored.refreshToken = newTokens.refresh_token;
    }
  }

  return stored.accessToken;
}

Role-Based Access Control (RBAC)

Implement fine-grained access control by mapping roles to permitted operations:

// Define permission matrix
const permissions: Record<string, {
  tools: string[];
  resources: string[];
  prompts: string[];
}> = {
  viewer: {
    tools: ["search", "list-items"],
    resources: ["*"],  // All resources
    prompts: ["explain-code"],
  },
  editor: {
    tools: ["search", "list-items", "create-item", "update-item"],
    resources: ["*"],
    prompts: ["explain-code", "review-code"],
  },
  admin: {
    tools: ["*"],  // All tools
    resources: ["*"],
    prompts: ["*"],
  },
};

function isAllowed(
  role: string,
  type: "tools" | "resources" | "prompts",
  name: string
): boolean {
  const rolePerms = permissions[role];
  if (!rolePerms) return false;

  const allowed = rolePerms[type];
  return allowed.includes("*") || allowed.includes(name);
}

// Authorization wrapper for tools
function authorizedTool(
  server: McpServer,
  name: string,
  description: string,
  schema: Record<string, z.ZodType>,
  requiredRoles: string[],
  handler: (args: any, context: any) => Promise<any>
) {
  server.tool(name, description, schema, async (args, context) => {
    const user = context?.meta?.user as JWTPayload | undefined;

    if (!user) {
      return {
        content: [{
          type: "text",
          text: "Authentication required.",
        }],
        isError: true,
      };
    }

    const hasPermission = user.roles.some(role =>
      requiredRoles.includes(role) || role === "admin"
    );

    if (!hasPermission) {
      return {
        content: [{
          type: "text",
          text: `Insufficient permissions. Required roles: ${requiredRoles.join(", ")}`,
        }],
        isError: true,
      };
    }

    return handler(args, context);
  });
}

// Usage
authorizedTool(
  server,
  "delete-user",
  "Delete a user account",
  { userId: z.string() },
  ["admin"],
  async ({ userId }) => {
    await db.deleteUser(userId);
    return {
      content: [{ type: "text", text: `User ${userId} deleted` }],
    };
  }
);

Authentication in mcp-framework

mcp-framework provides built-in support for authentication middleware:

import { MCPServer } from "mcp-framework";

const server = new MCPServer({
  name: "secure-server",
  version: "1.0.0",
  transport: {
    type: "sse",
    options: {
      port: 3001,
      headers: {
        // Require API key in headers
        "X-API-Key": process.env.MCP_API_KEY,
      },
    },
  },
});

await server.start();

For more advanced authentication, use middleware in mcp-framework's Express integration:

import { MCPServer } from "mcp-framework";
import jwt from "jsonwebtoken";

const server = new MCPServer({
  name: "jwt-server",
  version: "1.0.0",
  transport: {
    type: "sse",
    options: {
      port: 3001,
      middleware: [
        (req, res, next) => {
          const token = req.headers.authorization?.replace("Bearer ", "");
          if (!token) {
            res.status(401).json({ error: "Token required" });
            return;
          }
          try {
            (req as any).user = jwt.verify(token, process.env.JWT_SECRET!);
            next();
          } catch {
            res.status(401).json({ error: "Invalid token" });
          }
        },
      ],
    },
  },
});

Security Best Practices

Defense in Depth

Layer multiple security controls: authenticate at the transport level (headers/tokens), authorize at the tool level (role checks), validate at the input level (Zod schemas), and sanitize at the output level (remove sensitive data from responses).

Secure Configuration Checklist

1

Use HTTPS for all HTTP transports

Never run SSE or Streamable HTTP without TLS in production. Tokens and API keys sent over plain HTTP can be intercepted.

2

Rotate secrets regularly

API keys, JWT secrets, and OAuth client secrets should be rotated on a schedule. Support multiple active keys during rotation.

3

Set token expiration

JWT tokens should expire within 1 hour. OAuth access tokens should expire within 15-60 minutes. Use refresh tokens for longer sessions.

4

Log authentication events

Log all authentication successes and failures (without logging the tokens themselves). Monitor for unusual patterns.

5

Sanitize error messages

Never reveal internal details in authentication errors. "Invalid credentials" is better than "User not found in database table auth_users."

Sensitive Data in Tool Responses

Ensure that tool responses never include credentials, tokens, or internal secrets. If a tool reads a configuration file, strip sensitive fields before returning the data. Implement an output sanitizer that runs on every tool response.

Frequently Asked Questions