Authentication and Security
- Implement API key authentication for a remote HTTP MCP server
- Understand OAuth 2.1 PKCE flow as the MCP specification standard for production servers
- Apply the core security rules: HTTPS, no hardcoded secrets, input validation, and minimal tool scope
- Recognise and mitigate the confused deputy problem in MCP servers that proxy other APIs
Why Auth Matters
A stdio server is secured by the operating system — only processes on the same machine can reach it. An HTTP server is different. The moment you deploy to the cloud, your endpoint is reachable by anyone who knows the URL. Without authentication, that means anyone can call your tools, consume your API credits, or — if your tools write data — corrupt or exfiltrate it.
Authentication for MCP servers isn't optional for production deployments. It's the first line of defence, and you need to choose the right tier for your use case.
Three Auth Tiers
Match the complexity of your auth to the scope of your deployment:
Tier 1 — API Keys (Simplest)
For personal tools or internal team servers, a shared API key is sufficient and takes minutes to add. Store the key in an environment variable on the server, and require the client to send it as an Authorization: Bearer header.
app.post("/mcp", (req, res) => {
const authHeader = req.headers.authorization;
if (!authHeader || authHeader !== `Bearer ${process.env.MCP_API_KEY}`) {
return res.status(401).json({ error: "Unauthorized" });
}
return transport.handlePostRequest(req, res);
});
Apply the same check to your GET and DELETE routes. In Claude Desktop's config, pass the key through a custom header or embed it in the URL — whichever the client supports. Keep the key out of source control by loading it from .env with dotenv or your platform's secret manager.
Tier 2 — OAuth 2.1 (Production Standard)
The MCP specification mandates OAuth 2.1 for production remote servers. This is the right choice when you're shipping a server that others will use — not just your own team. OAuth 2.1 is a tightened successor to OAuth 2.0 with several important changes:
- PKCE is mandatory. The implicit grant flow is removed. Every client must use Proof Key for Code Exchange, which prevents authorisation code interception attacks.
- Your server acts as the OAuth resource server. It validates incoming Bearer tokens on every request. The authorisation server (which issues the tokens) can be a dedicated service like Auth0, or one you build yourself.
- Claude.ai and major MCP clients handle the auth flow automatically. When a user adds an integration that requires OAuth, the client opens the authorisation URL, receives a token, and attaches it to subsequent requests — you don't have to build a custom client-side flow.
- Resource indicators (RFC 8707) bind each token to a specific server URI. A token issued for
https://server-a.com/mcpcan't be replayed againsthttps://server-b.com/mcp. This prevents cross-server token theft attacks.
Your server must expose a metadata discovery endpoint at /.well-known/oauth-authorization-server so clients can find the authorisation URL and token endpoint automatically. Most OAuth libraries generate this document for you.
Tier 3 — mTLS (Enterprise)
Mutual TLS authenticates both sides of a connection at the transport layer — the client presents a certificate as well as the server. It's standard in zero-trust enterprise networks and rarely needed outside of that context. If your organisation already requires mTLS for internal services, you can layer it in front of your MCP server with a reverse proxy (Nginx, Envoy, or a service mesh like Istio) without changing your application code.
Security Rules for Every MCP Server
Authentication is necessary but not sufficient. Apply these rules regardless of which auth tier you choose:
Use HTTPS in Production
Never serve MCP over plain HTTP in production. All three routes (POST, GET, DELETE) carry sensitive data. Let's Encrypt makes TLS free; Railway, Render, and Fly.io provision certificates automatically.
Store Secrets in Environment Variables
Never hardcode API keys, database credentials, or tokens in source code. Use dotenv locally and your platform's secret manager in production. Rotate secrets on a schedule and immediately after any suspected exposure.
Treat Tool Parameters as Untrusted Input
The MCP schema validates parameter types, but it doesn't sanitise values. A string parameter that gets interpolated into a SQL query can carry a SQL injection. A path parameter can attempt directory traversal. Validate and sanitise every parameter that flows into downstream systems:
// Example: safe path construction
import path from "path";
function safeReadFile(userPath: string): string {
const base = "/data/allowed";
const resolved = path.resolve(base, userPath);
if (!resolved.startsWith(base)) {
throw new Error("Path traversal attempt blocked");
}
return fs.readFileSync(resolved, "utf8");
}
Apply Least-Privilege Credentials
A tool that reads a database shouldn't use credentials that can also write or delete. Create scoped credentials for each tool's actual needs. If a tool only needs to query one table, give it a read-only connection to that table only. A compromised tool call can then do no more damage than the task it was designed for.
Never Log Sensitive Data
Request headers can contain Bearer tokens. Tool arguments can contain passwords or personal data. Use structured logging that explicitly allowlists the fields you log, rather than logging entire request objects. A common mistake is adding console.log(req.body) during debugging and forgetting to remove it before deploying.
The Confused Deputy Problem
An MCP server that proxies other APIs can be manipulated by a carefully crafted model prompt into taking actions the user didn't intend. This is the confused deputy problem: the server has authority over a downstream API, but it's executing instructions from a model that may have been fed adversarial content.
Mitigations include:
- Require explicit confirmation for destructive or irreversible operations (delete, send, publish).
- Scope tools narrowly — a "send email" tool shouldn't also be able to delete emails.
- Log all tool calls with enough context to audit them after the fact.
- For high-stakes actions, consider a human-in-the-loop approval step before execution.
Stdio Servers: Don't Overlook Local Security
Stdio servers don't need network auth, but they can still leak secrets. If a tool reads environment variables and returns them to the model, sensitive values could appear in Claude's context window and potentially in logs. Be deliberate about what your tools expose — return only what the caller actually needs.
What to Try Next
- Add API key validation to the HTTP server you built in Lesson 13. Test it with
curl -H "Authorization: Bearer wrong-key"and confirm you get a 401. - Review one of your existing tool handlers and audit the parameter handling — identify any string that flows into a filesystem path, shell command, or database query without validation.
- Read the OAuth 2.1 metadata discovery spec and set up a
/.well-known/oauth-authorization-serverendpoint using an existing library likeoauth2-serveror a managed provider like Auth0. - In the next lesson you'll package and publish your server so others can install it with a single command.
- For personal or internal servers, API key auth is sufficient: check Authorization: Bearer <key> against an env var before handling any request
- The MCP specification requires OAuth 2.1 with PKCE for production remote servers — Claude.ai and other major clients implement this flow automatically
- Resource indicators (RFC 8707) bind tokens to a specific server URI, preventing a stolen token from being replayed against a different server
- Never log the request body, headers, or tool arguments if they might contain secrets — use structured logging that explicitly omits sensitive fields
- Treat every tool parameter as untrusted input: validate, sanitise, and apply least-privilege credentials so a compromised tool call cannot escalate beyond its intended scope