572 lines
19 KiB
Markdown
572 lines
19 KiB
Markdown
# MCP (Model Context Protocol) Authorization Security
|
|
|
|
This reference provides comprehensive guidance for reviewing MCP server implementations with a focus on OAuth 2.1 authorization and security best practices.
|
|
|
|
## Overview
|
|
|
|
The Model Context Protocol (MCP) specification finalized OAuth 2.1-based authorization in June 2025. MCP servers represent high-value targets because they:
|
|
- Store authentication tokens for multiple services
|
|
- Execute actions across connected services
|
|
- Can be invoked by AI agents with varying levels of user oversight
|
|
- Potentially have access to sensitive data and operations
|
|
|
|
**Key Security Principle:** MCP servers must implement strict authorization controls to prevent unauthorized access and token misuse.
|
|
|
|
## MCP Authorization Requirements
|
|
|
|
### 1. OAuth 2.1 Compliance (MANDATORY)
|
|
|
|
The MCP specification requires OAuth 2.1 as the authorization foundation.
|
|
|
|
**Core Requirements:**
|
|
- MUST use OAuth 2.1 (not OAuth 2.0)
|
|
- MUST implement PKCE (Proof Key for Code Exchange) for all clients
|
|
- MUST implement Resource Indicators (RFC 8707)
|
|
- MUST validate tokens with correct audience claims
|
|
- MUST NOT use sessions for authentication
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ NON-COMPLIANT - OAuth 2.0 without PKCE
|
|
@app.route('/authorize')
|
|
def authorize():
|
|
redirect_uri = request.args.get('redirect_uri')
|
|
# Missing code_challenge and code_challenge_method
|
|
auth_code = generate_auth_code()
|
|
return redirect(f'{redirect_uri}?code={auth_code}')
|
|
|
|
# ✅ COMPLIANT - OAuth 2.1 with PKCE
|
|
@app.route('/authorize')
|
|
def authorize():
|
|
code_challenge = request.args.get('code_challenge')
|
|
code_challenge_method = request.args.get('code_challenge_method')
|
|
|
|
if not code_challenge or code_challenge_method != 'S256':
|
|
return {'error': 'invalid_request', 'error_description': 'PKCE required'}, 400
|
|
|
|
auth_code = generate_auth_code(code_challenge)
|
|
return redirect(f'{redirect_uri}?code={auth_code}')
|
|
```
|
|
|
|
**Severity:** CRITICAL if PKCE missing, HIGH if OAuth 2.0 instead of 2.1
|
|
|
|
### 2. Resource Indicators (RFC 8707) - MANDATORY
|
|
|
|
MCP clients MUST implement Resource Indicators to explicitly specify the target MCP server.
|
|
|
|
**Purpose:**
|
|
- Ensures tokens are audience-restricted to specific MCP servers
|
|
- Prevents token misuse across different MCP servers
|
|
- Enables proper multi-tenant security
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ MISSING RESOURCE INDICATOR
|
|
token_request = {
|
|
'grant_type': 'authorization_code',
|
|
'code': auth_code,
|
|
'redirect_uri': redirect_uri,
|
|
# Missing 'resource' parameter!
|
|
}
|
|
|
|
# ✅ CORRECT - WITH RESOURCE INDICATOR
|
|
token_request = {
|
|
'grant_type': 'authorization_code',
|
|
'code': auth_code,
|
|
'redirect_uri': redirect_uri,
|
|
'resource': 'https://mcp-server.example.com', # Explicit target
|
|
'code_verifier': code_verifier # PKCE
|
|
}
|
|
```
|
|
|
|
**Audience Validation:**
|
|
```python
|
|
# ✅ MCP SERVER MUST VALIDATE AUDIENCE
|
|
def validate_token(token):
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
public_key,
|
|
algorithms=['RS256'],
|
|
audience='https://mcp-server.example.com', # Must match resource
|
|
issuer='https://auth.example.com'
|
|
)
|
|
return payload
|
|
except jwt.InvalidAudienceError:
|
|
raise HTTPException(403, 'Token not intended for this MCP server')
|
|
```
|
|
|
|
**Severity:** CRITICAL if missing
|
|
|
|
### 3. Strict Token Acceptance Policy
|
|
|
|
**MCP servers MUST NOT accept tokens that were not explicitly issued for them.**
|
|
|
|
This is the core defense against confused deputy attacks.
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ VULNERABLE - ACCEPTS ANY TOKEN
|
|
def authorize_request(request):
|
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
# No audience validation!
|
|
payload = jwt.decode(token, public_key, algorithms=['RS256'])
|
|
return payload['user_id']
|
|
|
|
# ✅ SECURE - VALIDATES AUDIENCE
|
|
def authorize_request(request):
|
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
public_key,
|
|
algorithms=['RS256'],
|
|
audience=MCP_SERVER_IDENTIFIER, # This server's identifier
|
|
issuer=TRUSTED_ISSUER
|
|
)
|
|
except jwt.InvalidAudienceError:
|
|
raise Forbidden('Token not issued for this MCP server')
|
|
|
|
return payload
|
|
```
|
|
|
|
**Severity:** CRITICAL
|
|
|
|
### 4. Scope Validation and Insufficient Scope Response
|
|
|
|
MCP servers MUST validate that tokens have the required scopes for operations.
|
|
|
|
**Required Behavior:**
|
|
- When a token has insufficient scope, respond with HTTP 403 Forbidden
|
|
- Include `WWW-Authenticate` header with `error="insufficient_scope"`
|
|
- Optionally include `scope` parameter indicating required scopes
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ MISSING SCOPE VALIDATION
|
|
@app.route('/tools/invoke', methods=['POST'])
|
|
def invoke_tool(token_payload):
|
|
# No scope check!
|
|
result = execute_tool(request.json['tool_name'])
|
|
return result
|
|
|
|
# ✅ CORRECT SCOPE VALIDATION
|
|
@app.route('/tools/invoke', methods=['POST'])
|
|
def invoke_tool(token_payload):
|
|
required_scope = 'tools.invoke'
|
|
|
|
if required_scope not in token_payload.get('scope', '').split():
|
|
response = Response(
|
|
'Insufficient scope',
|
|
status=403,
|
|
headers={
|
|
'WWW-Authenticate': f'Bearer error="insufficient_scope", scope="{required_scope}"'
|
|
}
|
|
)
|
|
return response
|
|
|
|
result = execute_tool(request.json['tool_name'])
|
|
return result
|
|
```
|
|
|
|
**Severity:** HIGH
|
|
|
|
### 5. No Session-Based Authentication
|
|
|
|
**MCP servers MUST NOT use sessions for authentication.**
|
|
|
|
This is a specific requirement of the MCP specification.
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ VIOLATES MCP SPEC - USING SESSIONS
|
|
@app.route('/tools/list')
|
|
def list_tools():
|
|
if 'user_id' not in session:
|
|
return {'error': 'unauthorized'}, 401
|
|
# Using Flask session - NOT ALLOWED
|
|
|
|
# ✅ COMPLIANT - TOKEN-BASED
|
|
@app.route('/tools/list')
|
|
def list_tools():
|
|
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
|
payload = validate_mcp_token(token) # OAuth 2.1 token validation
|
|
return get_tools_for_user(payload['sub'])
|
|
```
|
|
|
|
**Severity:** CRITICAL - violates MCP specification
|
|
|
|
### 6. Secure Session ID Generation (if used for OAuth flow)
|
|
|
|
If the MCP server or authorization server uses session IDs during the OAuth flow (not for authentication):
|
|
|
|
**Requirements:**
|
|
- MUST use secure, non-deterministic session IDs
|
|
- MUST use cryptographically secure random number generators
|
|
- MUST have sufficient entropy to prevent guessing
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ WEAK SESSION ID
|
|
import random
|
|
session_id = str(random.randint(1000, 9999)) # Predictable!
|
|
|
|
# ✅ SECURE SESSION ID
|
|
import secrets
|
|
session_id = secrets.token_urlsafe(32) # Cryptographically secure
|
|
```
|
|
|
|
**Severity:** HIGH
|
|
|
|
## Token Forwarding to MCP Servers
|
|
|
|
### The Critical Security Issue
|
|
|
|
A common anti-pattern is forwarding user authentication tokens from an inference server (or other service) to an MCP server.
|
|
|
|
**Example of Vulnerable Flow:**
|
|
```
|
|
User → Inference Server (with user JWT)
|
|
↓
|
|
Inference Server → MCP Server (forwarding user JWT) ❌ INSECURE
|
|
```
|
|
|
|
**Why This Is Dangerous:**
|
|
|
|
1. **Audience Mismatch**: User JWT has `aud` claim for inference server, not MCP server
|
|
2. **Confused Deputy**: MCP server cannot verify inference server is authorized to delegate
|
|
3. **No Scope Restriction**: MCP server receives user's full permissions, not downscoped
|
|
4. **Violates MCP Spec**: MCP servers must only accept tokens issued for them
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ❌ CRITICAL VULNERABILITY
|
|
class InferenceServer:
|
|
def call_mcp_tool(self, user_token, tool_name, params):
|
|
# Forwarding user token directly to MCP server!
|
|
response = requests.post(
|
|
f'{MCP_SERVER_URL}/tools/invoke',
|
|
headers={'Authorization': f'Bearer {user_token}'}, # WRONG!
|
|
json={'tool': tool_name, 'params': params}
|
|
)
|
|
return response.json()
|
|
```
|
|
|
|
**Severity:** CRITICAL - Must be blocked before merge
|
|
|
|
### The Correct Pattern: Token Exchange
|
|
|
|
**Secure Flow:**
|
|
```
|
|
User → Inference Server (with user JWT)
|
|
↓ (validate user JWT)
|
|
Inference Server → Auth Server (token exchange request)
|
|
↓ (receives MCP-scoped token)
|
|
Inference Server → MCP Server (with MCP-specific token) ✓ SECURE
|
|
```
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ✅ CORRECT - TOKEN EXCHANGE
|
|
class InferenceServer:
|
|
def call_mcp_tool(self, user_token, tool_name, params):
|
|
# Step 1: Validate user token
|
|
user_claims = self.validate_user_token(user_token)
|
|
|
|
# Step 2: Exchange for MCP-specific token
|
|
mcp_token = self.exchange_token_for_mcp(
|
|
user_token=user_token,
|
|
mcp_server='https://mcp-server.example.com',
|
|
required_scopes=['tools.invoke']
|
|
)
|
|
|
|
# Step 3: Use MCP-specific token
|
|
response = requests.post(
|
|
f'{MCP_SERVER_URL}/tools/invoke',
|
|
headers={'Authorization': f'Bearer {mcp_token}'}, # Correct audience
|
|
json={'tool': tool_name, 'params': params}
|
|
)
|
|
return response.json()
|
|
|
|
def exchange_token_for_mcp(self, user_token, mcp_server, required_scopes):
|
|
"""Exchange user token for MCP-server-specific token"""
|
|
exchange_response = requests.post(
|
|
f'{AUTH_SERVER_URL}/token',
|
|
data={
|
|
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
|
'subject_token': user_token,
|
|
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
|
|
'resource': mcp_server, # RFC 8707 Resource Indicator
|
|
'scope': ' '.join(required_scopes) # Downscoped
|
|
}
|
|
)
|
|
|
|
if exchange_response.status_code != 200:
|
|
raise AuthorizationError('Token exchange failed')
|
|
|
|
return exchange_response.json()['access_token']
|
|
```
|
|
|
|
**Key Security Benefits:**
|
|
1. ✅ MCP token has correct `aud` claim for the MCP server
|
|
2. ✅ Token is downscoped to minimum needed permissions
|
|
3. ✅ Clear audit trail preserved (user → inference → MCP)
|
|
4. ✅ Complies with MCP OAuth 2.1 specification
|
|
5. ✅ Prevents confused deputy attacks
|
|
6. ✅ MCP server can properly validate the token
|
|
|
|
**Severity:** Implementation of token exchange is CRITICAL when missing
|
|
|
|
## MCP Server Architecture Patterns
|
|
|
|
### Pattern 1: Embedded Authorization Server
|
|
|
|
The MCP server includes its own auth system.
|
|
|
|
**Characteristics:**
|
|
- Handles login, user sessions, consent UI
|
|
- Issues and verifies tokens
|
|
- Acts as both authorization server and resource server
|
|
|
|
**Security Considerations:**
|
|
- Must implement full OAuth 2.1 spec
|
|
- Requires secure user credential management
|
|
- Needs proper consent UI for user authorization
|
|
- Session management during OAuth flow (not for MCP operations)
|
|
|
|
### Pattern 2: External Authorization Server (Recommended)
|
|
|
|
The MCP server delegates OAuth to a trusted external provider.
|
|
|
|
**Characteristics:**
|
|
- Authorization server handles authentication, user management, consent
|
|
- MCP server only validates tokens and enforces scopes
|
|
- Clear separation of concerns
|
|
|
|
**Security Benefits:**
|
|
- Leverages battle-tested auth infrastructure
|
|
- Reduces attack surface of MCP server
|
|
- Easier to audit and maintain
|
|
- Better compliance with OAuth 2.1 spec
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ✅ EXTERNAL AUTH SERVER PATTERN
|
|
class MCPServer:
|
|
def __init__(self, auth_server_url, mcp_server_id):
|
|
self.auth_server_url = auth_server_url
|
|
self.mcp_server_id = mcp_server_id
|
|
# Fetch public keys from auth server
|
|
self.public_keys = self.fetch_jwks()
|
|
|
|
def validate_token(self, token):
|
|
"""Validate token issued by external auth server"""
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
self.public_keys,
|
|
algorithms=['RS256'],
|
|
audience=self.mcp_server_id,
|
|
issuer=self.auth_server_url
|
|
)
|
|
return payload
|
|
except jwt.InvalidTokenError as e:
|
|
raise Unauthorized(str(e))
|
|
|
|
def fetch_jwks(self):
|
|
"""Fetch JSON Web Key Set from auth server"""
|
|
response = requests.get(f'{self.auth_server_url}/.well-known/jwks.json')
|
|
return response.json()
|
|
```
|
|
|
|
## Transport Security
|
|
|
|
### Mutual TLS (mTLS)
|
|
|
|
For production MCP deployments, especially in enterprise environments:
|
|
|
|
**Requirement:** Transport security SHOULD use mutual TLS (mTLS) for bidirectional verification.
|
|
|
|
**What to Look For in PRs:**
|
|
```python
|
|
# ✅ mTLS CONFIGURATION
|
|
import ssl
|
|
|
|
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
|
context.verify_mode = ssl.CERT_REQUIRED
|
|
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
|
|
context.load_verify_locations(cafile='client_ca.crt')
|
|
|
|
# Use context in server configuration
|
|
server = HTTPServer(('localhost', 8443), MCPRequestHandler)
|
|
server.socket = context.wrap_socket(server.socket, server_side=True)
|
|
```
|
|
|
|
**Severity:** MEDIUM for internal deployments, HIGH for external-facing servers
|
|
|
|
### HTTPS Mandatory
|
|
|
|
**All MCP communication MUST occur over HTTPS (or equivalent secure transport).**
|
|
|
|
**What to Look For in PRs:**
|
|
- HTTP URLs in production configurations (red flag)
|
|
- Missing TLS/SSL certificate validation
|
|
- Disabled certificate verification (`verify=False`)
|
|
|
|
**Severity:** CRITICAL if HTTP used in production
|
|
|
|
## Security Risks Specific to MCP
|
|
|
|
### 1. High-Value Target
|
|
|
|
MCP servers store tokens for multiple services. If compromised:
|
|
- Attacker gains access to all connected service tokens
|
|
- Can execute actions across all integrated services
|
|
- Potential for lateral movement across services
|
|
|
|
**Mitigation:**
|
|
- Encrypt tokens at rest
|
|
- Use short-lived tokens with refresh
|
|
- Implement rate limiting and anomaly detection
|
|
- Audit all token usage
|
|
|
|
### 2. Prompt Injection Attacks
|
|
|
|
AI agents can be manipulated through prompt injection to:
|
|
- Execute unintended MCP tool invocations
|
|
- Access unauthorized resources
|
|
- Exfiltrate data through tool responses
|
|
|
|
**What to Look For in PRs:**
|
|
- Input validation on tool parameters
|
|
- Scope restrictions preventing dangerous operations
|
|
- Audit logging of all tool invocations
|
|
- User confirmation for sensitive operations
|
|
|
|
**Severity:** HIGH to CRITICAL depending on MCP server capabilities
|
|
|
|
### 3. Cross-Prompt Injection
|
|
|
|
Malicious content embedded in documents or UI can override agent instructions.
|
|
|
|
**Mitigation:**
|
|
- Sanitize external content before processing
|
|
- Implement content security policies
|
|
- Separate user instructions from external content
|
|
- Require explicit user confirmation for sensitive actions
|
|
|
|
**Severity:** HIGH
|
|
|
|
### 4. Remote Code Execution Risk
|
|
|
|
If MCP tools can execute code or system commands, prompt injection could lead to RCE.
|
|
|
|
**What to Look For in PRs:**
|
|
- Code execution capabilities without sandboxing
|
|
- System command execution from tool parameters
|
|
- File system access without path validation
|
|
- Network access without destination whitelisting
|
|
|
|
**Severity:** CRITICAL
|
|
|
|
## Review Checklist for MCP Code
|
|
|
|
When reviewing PRs involving MCP server implementation:
|
|
|
|
### Critical Checks
|
|
- [ ] **OAuth 2.1 compliance**: Using OAuth 2.1 (not 2.0)
|
|
- [ ] **PKCE mandatory**: All authorization flows use PKCE with S256
|
|
- [ ] **Resource Indicators**: Token requests include explicit `resource` parameter
|
|
- [ ] **Audience validation**: Server validates `aud` claim matches its identifier
|
|
- [ ] **No token forwarding**: Server doesn't accept tokens issued for other services
|
|
- [ ] **Token exchange**: Uses token exchange for upstream service calls
|
|
- [ ] **No session auth**: Doesn't use sessions for MCP operation authentication
|
|
|
|
### High Priority Checks
|
|
- [ ] **Scope validation**: Validates required scopes and returns proper insufficient_scope errors
|
|
- [ ] **Issuer validation**: Validates `iss` claim against trusted issuers
|
|
- [ ] **Algorithm enforcement**: Explicit algorithm list (no `none`, no user-controlled)
|
|
- [ ] **Signature validation**: All tokens cryptographically validated
|
|
- [ ] **HTTPS/TLS**: All communication over secure transport
|
|
- [ ] **Input validation**: Tool parameters validated and sanitized
|
|
|
|
### Medium Priority Checks
|
|
- [ ] **Token storage**: Tokens encrypted at rest
|
|
- [ ] **Audit logging**: Tool invocations logged with user context
|
|
- [ ] **Rate limiting**: Prevents abuse of MCP endpoints
|
|
- [ ] **Least privilege**: Scopes are minimal and specific
|
|
- [ ] **mTLS consideration**: mTLS for production deployments
|
|
- [ ] **Secure session IDs**: If used in OAuth flow, cryptographically random
|
|
|
|
### MCP-Specific Security Patterns
|
|
|
|
**Inference Server + MCP Architecture:**
|
|
```
|
|
✅ SECURE PATTERN:
|
|
1. User authenticates to inference server
|
|
2. Inference server validates user token
|
|
3. Inference server exchanges token for MCP-specific token
|
|
4. Inference server invokes MCP with proper token
|
|
5. MCP server validates audience matches its identifier
|
|
|
|
❌ INSECURE PATTERN:
|
|
1. User authenticates to inference server
|
|
2. Inference server forwards user token to MCP
|
|
3. MCP server accepts token not issued for it
|
|
```
|
|
|
|
## Platform-Specific Considerations
|
|
|
|
### Windows 11 MCP Proxy
|
|
|
|
Windows 11 provides centralized MCP proxy for security:
|
|
|
|
**Features:**
|
|
- Proxy-mediated communication for all MCP interactions
|
|
- Centralized policy enforcement
|
|
- Consistent authentication and authorization
|
|
- Enhanced audit and monitoring
|
|
|
|
**What to Look For in PRs:**
|
|
- Windows platform should integrate with Windows 11 MCP proxy
|
|
- Proxy configuration and policy settings
|
|
- Proper certificate validation for proxy communication
|
|
|
|
**Severity:** MEDIUM for Windows deployments
|
|
|
|
## Key Standards and Specifications
|
|
|
|
- **MCP Authorization Spec** (June 2025): Official OAuth 2.1 authorization for MCP
|
|
- **OAuth 2.1**: Required authorization framework
|
|
- **RFC 7636**: PKCE - Mandatory for all MCP clients
|
|
- **RFC 8707**: Resource Indicators - Mandatory for MCP token requests
|
|
- **RFC 8693**: Token Exchange - Recommended for service-to-service delegation
|
|
- **RFC 9068**: JWT Profile for OAuth 2.0 Access Tokens
|
|
- **RFC 9700**: OAuth 2.0 Security Best Current Practice (January 2025)
|
|
|
|
## When to Escalate
|
|
|
|
Escalate to security team or mark as CRITICAL if you find:
|
|
|
|
1. **Token forwarding**: Inference/API servers forwarding user tokens to MCP servers
|
|
2. **Missing audience validation**: MCP server accepts tokens without validating `aud` claim
|
|
3. **No PKCE**: Authorization flow missing PKCE implementation
|
|
4. **No Resource Indicators**: Token requests missing `resource` parameter
|
|
5. **Session-based auth**: Using sessions for MCP operation authentication
|
|
6. **Prompt injection risks**: MCP tools with code execution and no sandboxing
|
|
7. **Missing OAuth 2.1**: Still using OAuth 2.0 instead of 2.1
|
|
8. **HTTP communication**: Production MCP server using HTTP instead of HTTPS
|
|
|
|
## Summary
|
|
|
|
MCP authorization security requires:
|
|
1. **OAuth 2.1** with PKCE (mandatory)
|
|
2. **Resource Indicators** (RFC 8707) for explicit audience targeting
|
|
3. **Strict token validation** (audience, issuer, signature, scopes)
|
|
4. **Token exchange** for service-to-service delegation (not forwarding)
|
|
5. **No session-based authentication** for MCP operations
|
|
6. **Secure transport** (HTTPS/TLS, preferably mTLS)
|
|
7. **Input validation** and prompt injection protection
|
|
|
|
The most common vulnerability is token forwarding from inference servers to MCP servers. This must be replaced with proper token exchange flows.
|