Introduction #
In the previous blog, we explored the fundamentals of the Model Context Protocol (MCP), including its architecture, key components, and how they interact to enable dynamic, agent-driven workflows.
Observations #
If you followed the proof of concept in the previous post, two critical observations should have likely stood out:
-
Multiple Moving Parts : MCP comprises numerous interconnected components - models, tools, agents etc. , all operating in a highly dynamic environment. This complexity inherently increases the attack surface, making it more susceptible to misconfigurations, vulnerabilities, and unintended interactions that adversaries can exploit.
-
Lack of Authentication and Authorization Mechanisms : MCP does support authentication and authorization 1, typically through OAuth or similar mechanisms. However, if these controls are misconfigured, inconsistently enforced, or poorly integrated, they can create significant gaps in the security posture.
Landscape #
This figure showcases threats across the MCP Ecosystem.

Given the identified attack surface, we can look at Threat Modelling Framework called MAESTRO
(Multi-Agent Environment, Security, Threat, Risk, and Outcome) introdcued by Ken Huang and OWASP GenAI Security Project’s Agentic Security Initiative (ASI) , you can read more about it here.

This should give you an idea of how to identify and categorize threats across the lifecycle of an MCP ecosystem. In the next sections, we’ll shift our focus to the OAuth
segment and explore its role in securing the system.
MCP Authorization #
Key Spec Highlights (2025‑03‑26)
-
Comprehensive OAuth 2.1-Based Authorization Framework
The spec now defines a full OAuth 2.1 flow, covering both confidential and public clients, standardizing how authorization is handled across MCP. -
MCP Servers as OAuth Resource Servers
MCP servers are officially classified as OAuth resource servers, requiring them to both validate tokens and protect resources accordingly. -
Dynamic Client Registration (RFC 7591)
Clients can now automatically register with MCP authorization servers, reducing manual configuration and improving scalability. -
OAuth Server Metadata Discovery (RFC 8414)
Clients must discover server endpoints and supported features via metadata - enabling dynamic discovery ofauthorize
,token
, and registration URLs. -
PKCE Enforcement
PKCE is now mandatory for authorization code grants, ensuring public clients implementcode_challenge
andcode_verifier
to mitigate interception risks. -
Resource Indicators (RFC 8707)
OAuth flows must include aresource
parameter to bind access tokens to specific MCP servers - preventing token misuse across endpoints. -
JSON-RPC Batching Support
Although not strictly auth, this update also included support for batch requests over HTTP - contextually related to the new transport and auth flows.
For a Proof of Concept we are going to Integrate our MCP Server with Entra ID server as the Authority , prequisites
- MCP Client: VScode with CoPilot
- Resource Server: MCP Server hosted it locally/docker etc.
- Authority server : Entra ID
NOTE: As VS Code has built-in authentication support for GitHub and Microsoft Entra 2 , there are a few thing to note:
- First-Party App Constraints: VS Code acts as a first-party application, meaning it is developed and managed by Microsoft. First-party apps often have special privileges and do not require explicit app registration in Entra ID for basic operations.
- Predefined Trust: As a first-party app, VS Code is inherently trusted by Entra ID, allowing it to interact with Entra ID endpoints without requiring a separate app registration.
- Dynamic Configuration: Instead of relying on app registration, VS Code dynamically fetches OpenID Connect metadata from Entra ID, ensuring compatibility and flexibility across tenants.
Building up from previous post, I just changed the @mcp.tool()
to be an echo message as our main focus is on oAuth.
@mcp.tool()
def echo(message: str) -> str:
"""Echoes back the received message."""
return f"Echo: {message}"
We will use three import statments,
from mcp.server.auth.provider import TokenVerifier
from mcp.server.auth.settings import AuthSettings
from dataclasses import dataclass
Dataclasses to passaround and use token details in authenticationa and authorization logic. TokenVerifier is a base class (or interface) that defines how token verification should work in the FastMCP framework. AuthSettings is a configuration class used to define authentication and authorization settings for your FastMCP server
MCP would be intialized with Auth now,
@dataclass
class TokenInfo:
sub: str
scopes: list[str]
expires_at: int
client_id: str
raw_token: str
def fetch_openid_config(tenant_id: —str):
# Fetches the OpenID Connect configuration from the well-known endpoint for the given tenant.
# Returns a dictionary with the configuration.
openid_config_url = f"https://login.microsoftonline.com/{tenant_id}/v2.0/.well-known/openid-configuration"
response = requests.get(openid_config_url)
response.raise_for_status()
return response.json()
# Fetch and use OpenID Connect config
OPENID_CONFIG = fetch_openid_config(TENANT_ID)
OAUTH_METADATA = {
"issuer": OPENID_CONFIG["issuer"],
"authorization_endpoint": OPENID_CONFIG["authorization_endpoint"],
"token_endpoint": OPENID_CONFIG["token_endpoint"]
}
# Custom token verifier for FastMCP server.
# Decodes and inspects JWT tokens received from clients.
# Validates that the token contains required OpenID Connect scopes.
# Returns TokenInfo if valid, raises ValueError if not.
class MyTokenVerifier(TokenVerifier):
def __init__(self):
# Initialize the verifier by fetching JWKS from the OpenID config.
self.jwks_uri = OPENID_CONFIG["jwks_uri"]
try:
self.jwks = self._get_jwks()
except Exception as e:
self.jwks = {'keys': []}
def _get_jwks(self):
# Fetch JWKS (public keys) from Entra ID for token signature validation.
try:
LOG.info(f"SERVER -> OAUTH_SERVER: Fetching JWKS from Entra ID: {self.jwks_uri}")
response = requests.get(self.jwks_uri)
response.raise_for_status()
jwks_data = response.json()
return jwks_data
except Exception as e:
raise
async def verify_token(self, token: str) -> TokenInfo:
# Verifies the provided JWT access token:
# Decodes the token to inspect claims and validates the signature.
# Checks for required OpenID Connect scopes.
# Returns TokenInfo if valid, raises ValueError otherwise.
try:
# Decode token with signature verification
unverified_claims = jwt.decode(token, options={"verify_signature": False})
audience = unverified_claims.get('aud')
scopes = set(unverified_claims.get('scp', '').split(' '))
# Validate standard OpenID Connect scopes
STANDARD_SCOPES = {"openid", "profile", "email"}
if STANDARD_SCOPES.intersection(scopes):
return TokenInfo(
sub=unverified_claims.get('sub', ''),
scopes=list(scopes),
expires_at=unverified_claims.get('exp', 0),
client_id=unverified_claims.get('appid', ''),
raw_token=token
)
raise ValueError("Invalid token: Missing required scopes")
except Exception as e:
raise ValueError(f"Token validation failed: {str(e)}")
—
# Initialize FastMCP as Resource Server
mcp = FastMCP(
auth=AuthSettings(
issuer_url=OAUTH_METADATA["issuer"],
oauth_metadata=OAUTH_METADATA,
resource_server_url="http://localhost:8000"
),
token_verifier=MyTokenVerifier()
)
When a user tries to access the MCP server from VS Code 3, the client first sends a request without any authentication token. The MCP server responds with a 401 Unauthorized status and a WWW-Authenticate: Bearer challenge, indicating that authentication is required. In response, VS Code initiates the OAuth flow with Entra ID 4, prompting the user to log in and granting the necessary permissions. Once the user successfully authenticates, Entra ID issues a Bearer token to VS Code. The client then sends a new request to the MCP server, this time including the Authorization: Bearer
Hope this flow will make it more clearer - this was generated using GPT using the Client and Server logs from my code.
sequenceDiagram participant User as User participant VSCode as VS Code (MCP Client) participant MCP as MCP Server participant EntraID as Entra ID Note over MCP: Server Startup MCP->>EntraID: Fetch JWKS (JSON Web Key Set) EntraID-->>MCP: JWKS Response Note over MCP: Server Ready Note over User, VSCode: User OAuth Flow User->>VSCode: Open VS Code and Initialize MCP Client VSCode->>MCP: POST /mcp MCP-->>VSCode: 401 Unauthorized VSCode->>EntraID: Redirect to Entra ID Login EntraID->>User: Prompt for Login and Consent User-->>EntraID: Provide Credentials and Consent EntraID-->>VSCode: Authorization Code VSCode->>EntraID: Exchange Code for Token EntraID-->>VSCode: Access Token Note over VSCode, MCP: Authentication Request VSCode->>MCP: POST /mcp MCP->>EntraID: Token Verification EntraID-->>MCP: Token Issued MCP-->>VSCode: Accept Token Note over VSCode, MCP: Tool Execution VSCode->>MCP: Call Tool (Echo) MCP-->>VSCode: Response: Echo: Yodie Gang!!
Finally here’s Oauth in Action.
I am still researching on how to force Entra app api/custom scopes to work with Vscode (being discussed here https://github.com/microsoft/vscode/issues/249663 ) - will update this post once I figure it out.
Whats next #
I would like to do a follow up on this post using other offerings like,
- mcp-remote
- ContextForge MCP Gateway and,
- Enterprise Solutions like Azure API Managment - stay tuned!