MCP Authentication: OAuth 2.1, Service Tokens, and API Keys Guide

TL;DR — MCP authentication: OAuth 2.1 for HTTP, env vars for stdio, no token passthrough. MCP Spec: "Authorization is OPTIONAL. HTTP transports SHOULD conform to OAuth 2.1. STDIO transports SHOULD NOT follow this — retrieve credentials from environment. Protected server requires valid Authorization header with Bearer token." MCP Docs: "Follows OAuth 2.1 conventions. Strongly recommended when server accesses sensitive data, performs actions on behalf of users, or exposes dangerous tools." Descope: "June 2025 spec prohibits token passthrough to upstream APIs. Use existing IdP metadata via .well-known/oauth-authorization-server rather than hosting own." TrueFoundry: "Three methods: env vars for stdio, static HTTP headers for remote, OAuth 2.1 with PKCE for remote. OAuth available since Cursor v1.0." Medium: "Protecting MCP server with OAuth 2.1 using Go and Keycloak. Based on MCP Spec 2025-06-18 Authorization." Learn more with MCP guide, MCP transports, MCP security, and build MCP server.

MCP Spec defines the authorization framework: "Authorization is OPTIONAL for MCP implementations. When supported: Implementations using an HTTP-based transport SHOULD conform to this specification. Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment. A protected MCP server requires valid Authorization header with Bearer token."

MCP Docs explains the design: "Its design doesn't focus on one specific authorization or identity system, but rather follows the conventions outlined for OAuth 2.1. While authorization for MCP servers is optional, it is strongly recommended when your server accesses sensitive data, performs actions on behalf of users, or exposes potentially dangerous tools."

MCP Authentication Architecture

flowchart TD subgraph StdioAuth["stdio Authentication"] StdConfig["Client Config
env block in mcpServers JSON"] StdEnv["Environment Variables
API_KEY, GITHUB_TOKEN, etc."] StdServer["Server reads
os.environ.get('API_KEY')
no HTTP auth needed"] end subgraph HTTPAuth["Streamable HTTP Authentication"] Bearer["Bearer Token
Authorization: Bearer
in HTTP header"] APIKey["API Key
custom header or query
simpler than OAuth"] OAuth["OAuth 2.1 with PKCE
authorization code flow
automatic token refresh"] end subgraph OAuthFlow["OAuth 2.1 Flow"] Discovery[".well-known/
oauth-authorization-server
metadata endpoint"] AuthCode["Authorization Code
client redirects to IdP
user grants permission"] PKCE["PKCE
code_verifier + code_challenge
prevents authorization code interception"] Token["Token Exchange
code → access_token
+ refresh_token"] Validate["Token Validation
server validates bearer token
on every request"] end subgraph Rules["Authentication Rules"] NoPass["No Token Passthrough
server must NOT forward
client token to upstream APIs"] IdP["Use Existing IdP
advertise issuer metadata
don't host own OAuth server"] Separate["Separate Credentials
server has own credentials
for upstream services"] end StdConfig --> StdEnv --> StdServer Bearer --> OAuthFlow APIKey --> HTTPAuth OAuth --> OAuthFlow Discovery --> AuthCode --> PKCE --> Token --> Validate NoPass --> Rules IdP --> Rules Separate --> Rules

Authentication Method Comparison

Method Transport Complexity Security Best For
Environment Variables stdio Low Medium Local dev, single user
Static Bearer Token HTTP Low Medium Simple remote servers
API Key (header) HTTP Low Medium Internal services
OAuth 2.1 with PKCE HTTP High High Production, enterprise, multi-user

OAuth 2.1 Flow Steps

Step Action Participant Details
1 Discovery Client GET .well-known/oauth-authorization-server
2 PKCE Generation Client Generate code_verifier and code_challenge
3 Authorization User Redirect to IdP, user grants permission
4 Code Return IdP → Client Authorization code returned
5 Token Exchange Client → IdP Code + code_verifier → access_token
6 API Call Client → Server Bearer token in Authorization header
7 Validation Server Validate token on every request
8 Refresh Client Use refresh_token when access_token expires

Implementation

from dataclasses import dataclass
from typing import Optional
from enum import Enum

class AuthMethod(Enum):
    ENV_VARS = "env_vars"
    BEARER_TOKEN = "bearer_token"
    API_KEY = "api_key"
    OAUTH_21 = "oauth_21"

@dataclass
class MCPAuthenticationGuide:
    """MCP authentication implementation guide."""

    def get_stdio_auth(self) -> dict:
        """stdio transport authentication."""
        return {
            "method": "Environment variables",
            "spec": (
                "STDIO transport SHOULD NOT follow "
                "authorization specification. Instead "
                "retrieve credentials from environment."
            ),
            "how": [
                "Client config includes env block",
                "Client injects env vars when spawning "
                "server subprocess",
                "Server reads credentials from "
                "os.environ",
                "No HTTP auth headers needed",
            ],
            "config_example": (
                '{\n'
                '  "mcpServers": {\n'
                '    "github": {\n'
                '      "command": "docker",\n'
                '      "args": ["run", "-i", "--rm",\n'
                '        "-e", "GITHUB_TOKEN",\n'
                '        "ghcr.io/github/'
                'github-mcp-server"],\n'
                '      "env": {\n'
                '        "GITHUB_TOKEN":\n'
                '          "ghp_your_token"\n'
                '      }\n'
                '    }\n'
                '  }\n'
                '}'
            ),
            "server_code": (
                "import os\n"
                "token = os.environ.get(\n"
                "    'GITHUB_TOKEN')\n"
                "if not token:\n"
                "    raise ValueError(\n"
                "        'GITHUB_TOKEN not set')\n"
                "# Use token for API calls"
            ),
        }

    def get_bearer_token_auth(self) -> dict:
        """Bearer token authentication for HTTP."""
        return {
            "method": "Bearer token in Authorization header",
            "spec": (
                "Protected MCP server requires valid "
                "Authorization header with Bearer token. "
                "HTTP-based transports SHOULD conform "
                "to OAuth 2.1 conventions."
            ),
            "how": [
                "Client includes Authorization header",
                "Header format: "
                "'Authorization: Bearer <token>'",
                "Server validates token on every request",
                "Token can be OAuth access_token or "
                "static API token",
            ],
            "server_validation": (
                "from mcp.server.fastmcp import FastMCP\n"
                "\n"
                "mcp = FastMCP('protected-server')\n"
                "\n"
                "def validate_token(token: str) -> bool:\n"
                "    '''Validate bearer token.'''\n"
                "    # Validate against your IdP\n"
                "    # or token store\n"
                "    return token == VALID_TOKEN\n"
                "\n"
                "# For Streamable HTTP, middleware\n"
                "# validates Authorization header\n"
                "# before passing to MCP handler"
            ),
        }

    def get_oauth_21_flow(self) -> dict:
        """OAuth 2.1 with PKCE flow."""
        return {
            "standard": "OAuth 2.1 with PKCE",
            "spec_version": "MCP 2025-06-18 Authorization",
            "steps": [
                {
                    "step": 1,
                    "name": "Discovery",
                    "action": (
                        "Client GETs .well-known/"
                        "oauth-authorization-server "
                        "to discover OAuth endpoints"
                    ),
                },
                {
                    "step": 2,
                    "name": "PKCE Generation",
                    "action": (
                        "Client generates code_verifier "
                        "and code_challenge (S256). "
                        "Prevents authorization code "
                        "interception attacks."
                    ),
                },
                {
                    "step": 3,
                    "name": "Authorization Request",
                    "action": (
                        "Client redirects user to IdP "
                        "authorization endpoint with "
                        "code_challenge and state"
                    ),
                },
                {
                    "step": 4,
                    "name": "User Consent",
                    "action": (
                        "User grants or denies "
                        "permission at IdP"
                    ),
                },
                {
                    "step": 5,
                    "name": "Authorization Code",
                    "action": (
                        "IdP redirects back to client "
                        "with authorization code"
                    ),
                },
                {
                    "step": 6,
                    "name": "Token Exchange",
                    "action": (
                        "Client exchanges code + "
                        "code_verifier for "
                        "access_token and refresh_token"
                    ),
                },
                {
                    "step": 7,
                    "name": "API Call",
                    "action": (
                        "Client sends access_token as "
                        "Bearer token in Authorization "
                        "header on every MCP request"
                    ),
                },
                {
                    "step": 8,
                    "name": "Token Refresh",
                    "action": (
                        "When access_token expires, "
                        "client uses refresh_token "
                        "to get new access_token"
                    ),
                },
            ],
            "well_known": (
                "Server exposes .well-known/"
                "oauth-authorization-server endpoint "
                "with OAuth metadata: issuer, "
                "authorization_endpoint, "
                "token_endpoint, "
                "registration_endpoint, "
                "scopes_supported, "
                "response_types_supported, "
                "grant_types_supported, "
                "token_endpoint_auth_methods_supported"
            ),
        }

    def get_token_passthrough_rule(self) -> dict:
        """Token passthrough prohibition."""
        return {
            "rule": (
                "MCP servers MUST NOT pass through "
                "client tokens to upstream APIs"
            ),
            "spec": (
                "June 2025 spec explicitly prohibits "
                "MCP servers from passing through "
                "tokens to upstream APIs"
            ),
            "why": [
                "Prevents token confusion attacks",
                "Prevents privilege escalation",
                "Ensures server has controlled access "
                "to upstream services",
                "Maintains security boundaries",
            ],
            "correct_behavior": [
                "Server receives client bearer token "
                "for MCP authentication",
                "Server validates client token",
                "Server uses its OWN credentials for "
                "upstream API calls",
                "Server may exchange client token for "
                "a different token via token exchange",
                "Server has separate API keys/tokens "
                "for upstream services",
            ],
            "incorrect_behavior": [
                "Server forwards client bearer token "
                "to GitHub API",
                "Server passes client OAuth token to "
                "database connection",
                "Server uses client token for any "
                "upstream service call",
            ],
        }

    def get_idp_integration(self) -> dict:
        """Existing IdP integration."""
        return {
            "recommendation": (
                "If you have an existing IdP "
                "(Identity Provider), configure your "
                "MCP server to advertise that issuer's "
                "metadata rather than hosting its own"
            ),
            "how": [
                "Populate .well-known/"
                "oauth-authorization-server with "
                "existing IdP metadata",
                "Point issuer to existing IdP "
                "(Keycloak, Auth0, Okta, Azure AD)",
                "Server validates tokens against "
                "existing IdP's JWKS endpoint",
                "No need to host own OAuth server",
            ],
            "providers": [
                "Keycloak — open source, self-hosted",
                "Auth0 — managed, easy setup",
                "Okta — enterprise, SSO integration",
                "Azure AD — Microsoft ecosystem",
                "Google Identity — Google ecosystem",
            ],
        }

    def get_cursor_auth_methods(self) -> dict:
        """Cursor's three authentication methods."""
        return {
            "method_1_env_vars": {
                "transport": "stdio",
                "description": (
                    "Environment variables injected "
                    "into local stdio server processes"
                ),
                "config": (
                    '"env": {"API_KEY": "value"} '
                    "in mcpServers config"
                ),
                "best_for": "Local development, single user",
            },
            "method_2_static_headers": {
                "transport": "Streamable HTTP",
                "description": (
                    "Static HTTP headers with Bearer "
                    "tokens or API keys for remote servers"
                ),
                "best_for": "Simple remote servers",
            },
            "method_3_oauth": {
                "transport": "Streamable HTTP",
                "description": (
                    "OAuth 2.1 with PKCE for remote "
                    "servers running MCP authorization "
                    "specification"
                ),
                "available_since": "Cursor v1.0",
                "best_for": (
                    "Enterprise, multi-user, "
                    "production remote servers"
                ),
            },
        }

    def get_security_best_practices(self) -> dict:
        """Authentication security best practices."""
        return {
            "token_storage": [
                "Store tokens in environment variables, "
                "not in code",
                "Never hardcode tokens in config files "
                "committed to version control",
                "Use secret management for production "
                "(Vault, AWS Secrets Manager)",
            ],
            "token_validation": [
                "Validate bearer token on every request",
                "Check token expiration",
                "Verify token issuer and audience",
                "Use JWKS endpoint for signature "
                "verification",
            ],
            "scope_management": [
                "Request minimal scopes (least privilege)",
                "Define custom scopes for MCP tools",
                "Validate scopes before tool execution",
                "Different scopes for read vs write tools",
            ],
            "token_lifecycle": [
                "Short-lived access tokens (15-60 min)",
                "Long-lived refresh tokens",
                "Automatic token refresh",
                "Token revocation on logout",
            ],
        }

MCP Authentication Checklist

  • [ ] MCP authorization is OPTIONAL but strongly recommended for sensitive data access
  • [ ] HTTP-based transports SHOULD conform to OAuth 2.1 conventions
  • [ ] STDIO transports SHOULD NOT use HTTP auth — retrieve credentials from environment
  • [ ] Protected MCP server requires valid Authorization header with Bearer token
  • [ ] MCP follows OAuth 2.1 conventions, not one specific identity system
  • [ ] Authorization strongly recommended when server accesses sensitive data
  • [ ] Authorization strongly recommended when server performs actions on behalf of users
  • [ ] Authorization strongly recommended when server exposes potentially dangerous tools
  • [ ] stdio auth: set env vars in client config (e.g., claude_desktop_config.json env block)
  • [ ] stdio auth: server reads credentials from os.environ.get('API_KEY')
  • [ ] stdio auth: client injects env vars when spawning server subprocess
  • [ ] stdio auth: no HTTP auth headers needed
  • [ ] HTTP bearer token: Authorization: Bearer in HTTP header
  • [ ] HTTP bearer token: server validates token on every request
  • [ ] HTTP API key: custom header or query parameter, simpler than OAuth
  • [ ] OAuth 2.1 with PKCE: authorization code flow for production
  • [ ] OAuth 2.1: PKCE prevents authorization code interception attacks
  • [ ] OAuth 2.1: client generates code_verifier and code_challenge (S256)
  • [ ] OAuth 2.1: token exchange — code + code_verifier → access_token + refresh_token
  • [ ] OAuth 2.1: automatic token refresh using refresh_token
  • [ ] Server exposes .well-known/oauth-authorization-server metadata endpoint
  • [ ] .well-known includes: issuer, authorization_endpoint, token_endpoint, scopes_supported
  • [ ] June 2025 spec PROHIBITS token passthrough — server must NOT forward client token to upstream APIs
  • [ ] Server must use its OWN credentials for upstream API calls
  • [ ] Server may exchange client token via token exchange for different upstream token
  • [ ] Token passthrough prohibition prevents token confusion and privilege escalation
  • [ ] Use existing IdP (Keycloak, Auth0, Okta, Azure AD) rather than hosting own OAuth server
  • [ ] Configure .well-known to advertise existing IdP issuer metadata
  • [ ] Server validates tokens against existing IdP's JWKS endpoint
  • [ ] Cursor three methods: env vars (stdio), static headers (HTTP), OAuth 2.1 with PKCE (HTTP)
  • [ ] Cursor OAuth support available since v1.0
  • [ ] Store tokens in environment variables, never in code
  • [ ] Never hardcode tokens in config files committed to version control
  • [ ] Use secret management for production (Vault, AWS Secrets Manager)
  • [ ] Validate bearer token on every request — check expiration, issuer, audience
  • [ ] Use JWKS endpoint for signature verification
  • [ ] Request minimal scopes (least privilege)
  • [ ] Define custom scopes for MCP tools
  • [ ] Validate scopes before tool execution
  • [ ] Different scopes for read vs write tools
  • [ ] Short-lived access tokens (15-60 min)
  • [ ] Long-lived refresh tokens with automatic refresh
  • [ ] Token revocation on logout
  • [ ] Keycloak: open source, self-hosted OAuth provider
  • [ ] Auth0: managed, easy setup OAuth provider
  • [ ] Okta: enterprise, SSO integration
  • [ ] Read MCP developer guide for protocol
  • [ ] Read MCP transports for transport details
  • [ ] Read MCP security for security risks
  • [ ] Read build MCP server for server creation
  • [ ] Read Claude Desktop MCP for desktop config
  • [ ] Test: stdio server reads credentials from environment variables
  • [ ] Test: HTTP server validates bearer token on every request
  • [ ] Test: OAuth 2.1 PKCE flow completes successfully
  • [ ] Test: token refresh works when access_token expires
  • [ ] Test: server does NOT pass through client token to upstream APIs
  • [ ] Test: .well-known endpoint returns correct OAuth metadata
  • [ ] Test: invalid tokens are rejected with appropriate error
  • [ ] Document auth method, OAuth provider, scopes, and token lifecycle

FAQ

How does MCP authentication work?

MCP uses OAuth 2.1 conventions for HTTP-based transports and environment credentials for stdio. Authorization is optional but strongly recommended when servers access sensitive data. MCP Spec: "Authorization is OPTIONAL for MCP implementations. When supported: Implementations using an HTTP-based transport SHOULD conform to this specification. Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment. A protected MCP server requires valid Authorization header with Bearer token." MCP Docs: "Design does not focus on one specific authorization or identity system, but rather follows conventions outlined for OAuth 2.1. Authorization is strongly recommended when your server accesses sensitive data, performs actions on behalf of users, or exposes potentially dangerous tools." HTTP: OAuth 2.1 with PKCE, bearer tokens in Authorization header. stdio: environment variables for credentials.

How do you implement OAuth 2.1 authentication for an MCP server?

Configure .well-known/oauth-authorization-server metadata, implement OAuth 2.1 with PKCE flow, and validate bearer tokens on every request. Descope: "The June 2025 spec explicitly prohibits MCP servers from passing through tokens to upstream APIs. If you have an existing IdP (Identity Provider), configure your MCP server to advertise that issuer metadata rather than hosting its own. This can be done by populating the .well-known/oauth-authorization-server endpoint." Medium: "Protecting MCP server with OAuth 2.1 using Go and Keycloak. Based on MCP Specification 2025-06-18 Authorization. Implement MCP server without authentication first, then add OAuth 2.1 layer." Steps: (1) Set up OAuth provider (Keycloak, Auth0, Okta). (2) Configure .well-known/oauth-authorization-server. (3) Implement PKCE flow for clients. (4) Validate bearer tokens on every HTTP request. (5) Do NOT pass through tokens to upstream APIs. (6) Use existing IdP metadata when available.

How do you handle authentication for stdio MCP servers?

For stdio transports, retrieve credentials from environment variables instead of using HTTP authentication. MCP Spec: "Implementations using an STDIO transport SHOULD NOT follow this authorization specification, and instead retrieve credentials from the environment." TrueFoundry: "Cursor offers three auth methods: environment variables injected into local stdio server processes, static HTTP headers (Bearer tokens or API keys) for remote servers, and OAuth 2.1 with PKCE for remote servers." stdio auth: (1) Set env vars in MCP client config (e.g., claude_desktop_config.json env block). (2) Server reads credentials from os.environ. (3) No HTTP auth headers needed. (4) Client injects env vars when spawning server subprocess. (5) Example: {"env": {"API_KEY": "your-key"}} in config. (6) Server: api_key = os.environ.get('API_KEY').

What is the token passthrough prohibition in MCP?

The June 2025 MCP spec explicitly prohibits servers from passing through client tokens to upstream APIs — servers must exchange or use their own credentials. Descope: "The June 2025 spec explicitly prohibits MCP servers from passing through tokens to upstream APIs. If you have an existing IdP (Identity Provider), configure your MCP server to advertise that issuer metadata rather than hosting its own." This means: (1) Server receives client bearer token for authentication. (2) Server must NOT forward that same token to upstream APIs (GitHub, database, etc.). (3) Server must use its own credentials or exchange the token for a new one. (4) This prevents token confusion and privilege escalation. (5) Server should have separate credentials for upstream services. (6) Use existing IdP metadata endpoint rather than hosting own OAuth server when possible.

What are the three authentication methods for MCP in Cursor?

Cursor supports environment variables for stdio, static HTTP headers for remote, and OAuth 2.1 with PKCE for remote servers running MCP authorization spec. TrueFoundry: "Cursor offers three: environment variables injected into local stdio server processes, static HTTP headers (Bearer tokens or API keys) for remote servers, and OAuth 2.1 with PKCE for remote servers running the MCP authorization specification. OAuth support has been available since Cursor v1.0." Method 1 (stdio): env vars in config — {"env": {"API_KEY": "value"}}. Method 2 (HTTP static): Bearer token or API key in headers — configured per remote server. Method 3 (OAuth 2.1): PKCE flow for servers implementing MCP authorization spec — automatic token refresh, secure authorization code flow. Choose based on transport: stdio → env vars, simple HTTP → static headers, enterprise HTTP → OAuth 2.1.


Want a self-hosted AI company brain that does all of this out of the box?
Book a demo →