Designing OAuth 2.1 Architecture for HTTP-Based MCP
Build zero-trust MCP server security using OAuth 2.1. Learn dedicated authorization servers, RFC 8707 Resource Indicators, and PRM token validation.

Designing an OAuth 2.1 Architecture for Secure HTTP-Based Model Context Protocol Support
The rapid adoption of the Model Context Protocol (MCP) has transformed how generative AI models and autonomous agents interface with enterprise systems of record. By providing a standardized protocol for context retrieval and tool execution, MCP enables large language models (LLMs) to query databases, invoke SaaS APIs, execute terminal scripts, and parse file systems seamlessly.
When an MCP server operates locally via STDIO transport, security boundaries are anchored to the host operating system: the process runs in user space, inheriting the credentials and file permissions of the local workstation user.
However, as enterprises scale agentic workflows across cloud-native environments, MCP servers are increasingly deployed as remote, network-accessible microservices over HTTP transports (Server-Sent Events / SSE and streamable HTTP). The moment an MCP server is exposed to a network, its security posture shifts dramatically.
An unauthenticated or loosely authenticated HTTP MCP server transforms from a helpful developer utility into an unmonitored execution proxy that can be abused for unauthorized data extraction, lateral network movement, and prompt injection attacks.
To establish enterprise-grade MCP server security, the AI engineering community has converged on profiling OAuth 2.1 as the definitive authorization protocol for HTTP-based MCP deployments. OAuth 2.1 removes legacy, high-risk grant types, mandates Proof Key for Code Exchange (PKCE) for all clients, and leverages modern identity specifications—including Protected Resource Metadata (RFC 9728), Authorization Server Metadata (RFC 8414), and Resource Indicators (RFC 8707).
This article provides a comprehensive architectural blueprint for designing, implementing, and enforcing an OAuth 2.1 authorization framework for HTTP-based MCP servers, highlighting how Aegis Security delivers zero-bypass runtime protection over the entire model context pipeline.
The Three-Role Topology: Decoupling Resource Servers from Dedicated Authorization Servers
A fundamental failure mode in early MCP server implementations was collapsing the authorization server and the resource server into a single software component. Forcing every MCP server developer to implement custom login endpoints, user management, consent screens, and token-issuance logic leads to brittle security controls, unpatched crypto libraries, and maintenance debt.
Modern MCP security architectures enforce a strict separation of concerns across three distinct roles defined by OAuth 2.1:
The MCP Client (OAuth Client)
The host application driving the interaction (e.g., VS Code, Claude Code, Cursor, or a custom agent orchestrator). The client acts as an OAuth 2.1 public client. It initiates the discovery handshake, conducts the authorization code flow with PKCE, stores temporary access tokens, and attaches the bearer credential to outgoing HTTP requests to the MCP server.
The MCP Server (OAuth Resource Server)
The remote service exposing tools, prompts, and data resources to the agent. The MCP server acts strictly as an OAuth Resource Server. It does not handle user authentication, render login forms, or issue tokens. Instead, it exposes metadata pointing to trusted dedicated authorization servers, validates incoming JWT/opaque tokens, enforces scope bounds per tool call, and verifies audience parameters.
The Dedicated Authorization Server (Identity Provider)
An enterprise-grade identity platform (e.g., Keycloak, Okta, Microsoft Entra ID, or Auth0) tasked with authenticating human operators or non-human workloads. The authorization server manages user directories, renders consent forms, handles multi-factor authentication (MFA), issues cryptographically signed JWTs or opaque tokens, and exposes token introspection endpoints.
The Step-by-Step OAuth 2.1 Handshake & Metadata Discovery Loop
Because AI agents and MCP clients interact dynamically with various tools across distributed infrastructure, hardcoding static client secrets or authorization endpoints into client binaries is non-viable. The MCP authorization specification implements a fully dynamic, standards-based discovery loop built on Protected Resource Metadata token validation and standard OIDC/OAuth discovery.
Step 1: Initial Handshake & Challenge (HTTP 401)
When an MCP client issues an initial unauthenticated request to a protected remote MCP server, the server rejects the connection with an HTTP 401 Unauthorized status code. The response includes a WWW-Authenticate header containing the resource_metadata parameter pointing to the server's RFC 9728 endpoint:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"
Step 2: Protected Resource Metadata Discovery (RFC 9728)
The client parses the header and issues an HTTP GET request to the provided resource_metadata URL. The MCP server responds with a JSON document defining its resource URI, supported scopes, and trusted authorization servers:
{
"resource": "https://mcp.example.com",
"authorization_servers": [
"https://auth.example.com/realms/master"
],
"scopes_supported": [
"mcp:tools:read",
"mcp:tools:execute"
],
"bearer_methods_supported": [
"header"
]
}
Step 3: Authorization Server Metadata Discovery (RFC 8414 / OIDC)
Having selected an authorization server from the PRM document, the client appends /.well-known/oauth-authorization-server or /.well-known/openid-configuration to the issuer base URL to fetch the authorization server's capabilities:
{
"issuer": "https://auth.example.com/realms/master",
"authorization_endpoint": "https://auth.example.com/realms/master/protocol/openid-connect/auth",
"token_endpoint": "https://auth.example.com/realms/master/protocol/openid-connect/token",
"introspection_endpoint": "https://auth.example.com/realms/master/protocol/openid-connect/token/introspect",
"registration_endpoint": "https://auth.example.com/realms/master/clients-registrations/openid-connect",
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"]
}

Client Registration: Dynamic Client Registration (RFC 7591) vs. Client ID Metadata Documents
For an MCP client to interact with an authorization server, it must register its identity, redirect URIs, and allowed grant types. In standard web applications, this registration is performed statically by an administrator. In distributed agentic systems, pre-registering every IDE instance with every enterprise MCP authorization server is operationally impossible.
To support seamless scaling, the MCP authorization specification supports two primary registration patterns:
Client ID Metadata Documents (Preferred Strategy)
In this modern OAuth 2.1 pattern, the client uses a public, client-controlled HTTPS URL as its client_id (e.g., [https://cursor.sh/oauth/client-metadata.json](https://cursor.sh/oauth/client-metadata.json)). When the client initiates the authorization flow, the authorization server fetches the client's metadata document directly from that URL to verify allowed redirect URIs, client display names, and logos.
{
"client_id": "https://cursor.sh/oauth/client-metadata.json",
"client_name": "Cursor IDE Agent",
"redirect_uris": [
"http://127.0.0.1:54321/callback",
"cursor://anysphere.cursor-mcp/oauth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
This approach eliminates the need for prior registration endpoints, allowing any compliant client to connect to any MCP authorization server securely while guaranteeing domain ownership.
Dynamic Client Registration (RFC 7591)
For authorization servers that do not yet support Client ID Metadata Documents, clients use Dynamic Client Registration (DCR). The client issues a POST request to the authorization server's registration_endpoint:
POST /protocol/openid-connect/client-registrations/openid-connect HTTP/1.1
Host: auth.example.com
Content-Type: application/json
{
"client_name": "Claude Code CLI",
"redirect_uris": ["http://127.0.0.1:61234/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
The authorization server responds with a unique, generated client_id string that the client uses for subsequent authentication requests.
Audience Binding & Resource Indicators (RFC 8707): Eliminating the Confused Deputy
A severe vulnerability in un-profiled OAuth implementations is the Token Passthrough Anti-Pattern. If an agent obtains a generic access token intended for Server A and replays that same token to authenticate against Server B, Server B might accept the token if it trusts the same issuer.
This creates a classic Confused Deputy attack: an attacker who controls a low-trust MCP server can collect valid bearer tokens submitted by users and replay them against high-trust enterprise MCP servers handling financial records or infrastructure administration.
OAuth 2.1 resolves this threat by enforcing Resource Indicators (RFC 8707). During the initial /authorize and /token requests, the client explicitly includes a resource parameter specifying the target MCP server URL:
GET /protocol/openid-connect/auth?
response_type=code&
client_id=https://cursor.sh/oauth/client-metadata.json&
redirect_uri=http://127.0.0.1:54321/callback&
scope=mcp:tools:execute&
resource=https://mcp.example.com&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256 HTTP/1.1
Host: auth.example.com
When the authorization server issues the JWT access token, it embeds the requested resource directly inside the aud (audience) claim:
{
"iss": "https://auth.example.com/realms/master",
"sub": "user_33ed6c6b_prod",
"aud": "https://mcp.example.com",
"exp": 1755540817,
"iat": 1755540757,
"scope": "mcp:tools:read mcp:tools:execute",
"client_id": "https://cursor.sh/oauth/client-metadata.json"
}
When the MCP server validates incoming requests, it checks that the aud claim matches its own configured base URL. If a client presents a token minted for [https://mcp.other-vendor.com](https://mcp.other-vendor.com), the server drops the request immediately, neutralizing token replay attacks.

Full Implementation: TypeScript Express Middleware with Introspection and PRM Validation
The following production-ready TypeScript implementation demonstrates how to build an HTTP-based MCP server using @modelcontextprotocol/sdk. The server implements RFC 9728 Protected Resource Metadata routing, bearer middleware authentication, RFC 8707 audience validation, and token introspection via Keycloak.
import "dotenv/config";
import express from "express";
import { randomUUID } from "node:crypto";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import cors from "cors";
import {
mcpAuthMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
} from "@modelcontextprotocol/sdk/server/auth/router.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
import { checkResourceAllowed } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
const CONFIG = {
host: process.env.HOST || "localhost",
port: Number(process.env.PORT) || 3000,
auth: {
host: process.env.AUTH_HOST || "localhost",
port: Number(process.env.AUTH_PORT) || 8080,
realm: process.env.AUTH_REALM || "master",
clientId: process.env.OAUTH_CLIENT_ID || "mcp-resource-server",
clientSecret: process.env.OAUTH_CLIENT_SECRET || "",
},
};
const mcpServerUrl = new URL(`http://${CONFIG.host}:${CONFIG.port}`);
function createOAuthUrls() {
const authBaseUrl = new URL(
`http://${CONFIG.auth.host}:${CONFIG.auth.port}/realms/${CONFIG.auth.realm}/`
);
return {
issuer: authBaseUrl.toString(),
introspection_endpoint: new URL(
"protocol/openid-connect/token/introspect",
authBaseUrl
).toString(),
authorization_endpoint: new URL(
"protocol/openid-connect/auth",
authBaseUrl
).toString(),
token_endpoint: new URL(
"protocol/openid-connect/token",
authBaseUrl
).toString(),
};
}
const oauthMetadata: OAuthMetadata = {
...createOAuthUrls(),
response_types_supported: ["code"],
};
/**
* Token Verifier executing introspection and RFC 8707 Audience Validation
*/
const tokenVerifier = {
verifyAccessToken: async (token: string) => {
const endpoint = oauthMetadata.introspection_endpoint;
if (!endpoint) {
throw new Error("Missing OAuth introspection endpoint");
}
const params = new URLSearchParams({
token: token,
client_id: CONFIG.auth.clientId,
});
if (CONFIG.auth.clientSecret) {
params.set("client_secret", CONFIG.auth.clientSecret);
}
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: params.toString(),
});
if (!response.ok) {
throw new Error(`Token introspection failed with status ${response.status}`);
}
const data = await response.json();
if (data.active !== true) {
throw new Error("Token is inactive or revoked");
}
// Enforce RFC 8707 Resource Indicator / Audience Binding
if (!data.aud) {
throw new Error("Missing required audience (aud) claim in token");
}
const audiences: string[] = Array.isArray(data.aud) ? data.aud : [data.aud];
const isAudienceAllowed = audiences.some((aud) =>
checkResourceAllowed({
requestedResource: aud,
configuredResource: mcpServerUrl,
})
);
if (!isAudienceAllowed) {
throw new Error(
`Audience mismatch. Server expects ${mcpServerUrl.origin}, received: ${audiences.join(", ")}`
);
}
return {
token,
clientId: data.client_id,
scopes: data.scope ? data.scope.split(" ") : [],
expiresAt: data.exp,
userContext: data.sub,
};
},
};
const app = express();
app.use(express.json());
app.use(cors({ origin: "*", exposedHeaders: ["Mcp-Session-Id"] }));
// Mount RFC 9728 Protected Resource Metadata endpoint
app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: ["mcp:tools:read", "mcp:tools:execute"],
resourceName: "Enterprise Finance MCP Server",
})
);
// Enforce Bearer token middleware on tool execution routes
const authMiddleware = requireBearerAuth({
verifier: tokenVerifier,
requiredScopes: ["mcp:tools:execute"],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
});
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
function instantiateMcpTools() {
const server = new McpServer({
name: "enterprise-finance-mcp",
version: "1.0.0",
});
server.registerTool(
"query_ledger",
{
title: "Query General Ledger",
description: "Read-only access to transactional financial records",
inputSchema: {
account_id: z.string().describe("Target account identifier"),
},
},
async ({ account_id }) => ({
content: [{ type: "text", text: `Ledger data for account ${account_id}: Balance $1,250,000` }],
})
);
return server;
}
const mcpPostHandler = async (req: express.Request, res: express.Response) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (id) => {
transports[id] = transport;
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
const server = instantiateMcpTools();
await server.connect(transport);
} else {
res.status(400).json({ error: "Missing or invalid MCP session" });
return;
}
await transport.handleRequest(req, res, req.body);
};
app.post("/mcp", authMiddleware, mcpPostHandler);
app.listen(CONFIG.port, CONFIG.host, () => {
console.log(`🚀 Secure MCP Server active at ${mcpServerUrl.origin}`);
console.log(`🔐 PRM Metadata endpoint live at ${getOAuthProtectedResourceMetadataUrl(mcpServerUrl)}`);
});
Common Real-World Spec Inconsistencies & Client Defenses
Implementing OAuth 2.1 across diverse MCP clients (Cursor, Claude Code, Windsurf, custom agents) exposes several real-world specification edge cases that developers must handle defensively:
1. Loopback Address Mismatch (localhost vs. 127.0.0.1)
- The Issue: Many desktop MCP clients register redirect URIs using http://localhost:54321/callback during Dynamic Client Registration, but subsequently send [http://127.0.0.1:54321/callback](http://127.0.0.1:54321/callback) in authorization requests. Under strict OAuth 2.1 string matching rules, this causes immediate authorization failures.
- The Solution: Implement loopback expansion within your DCR proxy or authorization server. When a client registers a localhost URI, automatically register both localhost and 127.0.0.1 variants for that port.
2. Confidential vs. Public Client Misconfiguration
- The Issue: Certain IDE client implementations specify token_endpoint_auth_method: "client_secret_basic" during registration, signaling to the authorization server that they can securely manage client secrets. However, during token exchange, they omit the secret because they execute on an end-user workstation.
- The Solution: Force all client registrations handled via your MCP DCR proxy to register as public clients (token_endpoint_auth_method: "none"), ensuring that PKCE is strictly enforced without requiring static client secrets.
3. Opaque Token Introspection Latency
- The Issue: Identity providers like Clerk issue opaque access tokens (prefixed with oat_) rather than self-contained JWTs. Verifying an opaque token requires a synchronous, network-blocking call to the identity provider's /userinfo or /introspect endpoint on every single MCP tool call.
- The Solution: Implement local, high-performance in-memory caching with strict eviction lifetimes (e.g., 60 seconds) for introspected opaque tokens. Ensure that token revocation events clear the cache out-of-band.
The Aegis Runtime Control Plane: Enforcing Zero-Bypass MCP Protection
While OAuth 2.1 establishes robust identity validation and token issuance, authentication alone does not prevent runtime agent exploits.
If an authenticated agent receives an indirect prompt injection via an untrusted file or API response, it can use its valid OAuth token to execute destructive tool calls that technically fall within its granted scope but violate business intent.
Aegis Security fills this gap by deploying an inline, zero-bypass Runtime Enforcement Layer that intercepts every MCP tool call out-of-band.
In-Path Envoy Proxying with ext_authz
Aegis deploys stateless, high-performance Go sidecar proxies alongside your remote MCP servers. Using Envoy's ext_authz (External Authorization) filter, Aegis halts incoming HTTP tool execution requests before payload arguments touch system logic.
Semantic Schema Validation & Argument Filtering
Aegis evaluates tool call arguments against strict, version-controlled JSON validation schemas. If a prompt injection tricks an agent into appending shell pipe commands or path-traversal strings to a tool argument, Aegis's OPA engine catches the anomaly inline, executing real-time sanitization (sanitize) or blocking the request (deny).

Asynchronous Human-in-the-Loop Escalation
For high-impact tool invocations—such as executing database writes or triggering monetary transfers—Aegis executes an asynchronous suspension loop. The Data Plane pauses the active agent execution thread and triggers a backchannel authorization prompt to a human supervisor via Client-Initiated Backchannel Authentication (CIBA) or webhook notifications.
The agent thread remains frozen until an authorized human provides a cryptographically signed approval token, guaranteeing human oversight for critical operations.
Global Compliance & Auditing Integration
To satisfy enterprise compliance benchmarks (EU AI Act, SOC 2 Type II, ISO 27001, PCI DSS 4.0), every MCP authorization decision, token validation, and tool invocation must generate a tamper-proof audit trail.

Regulatory Compliance Mapping
Governance Standard | Core Posture Obligation | Aegis Platform Enforcement Mechanism |
EU AI Act (Articles 12 & 99) | Continuous logging, risk assessment, and immutable event tracing over the system lifecycle. | Immutable Capability Logging: Bundles and signs every OAuth handshake, tool call, and policy decision in WORM storage. |
OWASP Agentic AI Top 10 | Mitigating ASI01: Agent Goal Hijacking and ASI02: Insecure Tool Use. | Enforces strict JSON schema validation, short-lived tokens, and least-privilege tool scoping dynamically. |
SOC 2 (Type II Audits) | Maintain comprehensive audit trails, control logical perimeters, and capture infrastructure logs. | Captures trace-native telemetry via OpenTelemetry, isolating database records per tenant at the ORM layer. |
PCI DSS 4.0 | Continuous external attack surface mapping, strict network microsegmentation, and application protection. | In-Path Payload Sanitization: Utilizes automated content filters to redact sensitive cardholder data out-of-band. |
Aegis records every single token validation, context update, tool parameter argument, and proxy decision. The platform packages these traces into cryptographically signed snapshot files saved within write-once-read-many (WORM) object storage, delivering audit-ready verification packs for external compliance reviews.
Conclusion: Securing the Agentic Transport Layer
As autonomous AI agents move into production, exposing HTTP-based Model Context Protocol (MCP) servers without standardized authorization creates severe security vulnerabilities. Relying on static API keys, un-audienced bearer tokens, or custom, in-house auth logic leaves core systems vulnerable to token replay attacks, confused deputy exploits, and unmonitored data exfiltration.
OAuth 2.1 provides the definitive framework for securing remote MCP deployments. By enforcing mandatory PKCE, leveraging RFC 9728 Protected Resource Metadata discovery, decoupling authorization servers from resource hosts, and enforcing RFC 8707 Resource Indicators, enterprises can establish robust, scalable identity boundaries.
Pairing OAuth 2.1 identity verification with Aegis Security's zero-bypass runtime control plane provides complete defense-in-depth.
Aegis intercepts tool executions inline, validates parameters against declarative OPA schemas, enforces human-in-the-loop approvals for high-risk actions, and archives cryptographically signed audit logs automatically. Secure your transport layer, enforce least-privilege tool execution, and scale enterprise agentic AI with complete confidence.
Frequently Asked Questions (FAQ)
Q1: Is OAuth 2.1 required for local MCP servers running over STDIO?
No. Local MCP servers running over STDIO operate within the host OS process space, inheriting the local workstation user's permissions and environment variables. OAuth 2.1 is designed specifically for remote, network-accessible MCP servers operating over HTTP/SSE transports where client identity must be verified across network boundaries.
Q2: What is the purpose of Protected Resource Metadata (RFC 9728) in MCP authorization?
RFC 9728 allows an unauthenticated MCP client to discover how to authenticate with a remote MCP server. When the server returns an HTTP 401 response, it provides a pointer to its PRM document (/.well-known/oauth-protected-resource), which tells the client which dedicated authorization server to contact and which scopes are supported.
Q3: How do RFC 8707 Resource Indicators prevent Confused Deputy attacks?
RFC 8707 requires the client to specify the exact target MCP server URI during the OAuth authorization request. The authorization server embeds this URI in the token's aud (audience) claim. When the MCP server validates the token, it verifies that the aud claim matches its own URL, rejecting tokens that were minted for different servers.
Q4: Why is token passthrough explicitly banned in the MCP specification?
Token passthrough occurs when an MCP server takes an access token received from a client and replays it to authenticate against a downstream third-party API. This breaks audience binding, violates zero-trust principles, and exposes downstream systems to token theft. MCP servers must obtain their own scoped tokens (via token exchange or client credentials) when calling downstream services.
Q5: How does Aegis enforce runtime policy controls on top of OAuth 2.1 authentication?
While OAuth 2.1 verifies client identity and grants initial scopes, Aegis provides inline runtime enforcement at the tool-execution layer. Aegis sidecar proxies intercept individual tool calls out-of-band, parsing parameter arguments against OPA Rego policies to sanitize payloads or block prompt-injection attacks before execution.
Are your HTTP-based MCP servers operating over public or private networks without OAuth 2.1 audience binding and runtime inspection? Close the execution gap and protect your data perimeters with the Aegis AgenticOps Control Plane Core. Secure the action layer.
