Skip to main content

S in MCP stands for Security - Security Mechanism in MCP framework (oAuth)

·1338 words·7 mins· loading · loading ·
Security MCP AI
Author
PandaEatsBamboo
Dingbat
Table of Contents
MCP - This article is part of a series.
Part 2.1: This Article

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.

MCP - You can run but you can't hide
·1176 words·6 mins· loading · loading
Automation MCP Microsoft Azure LLM
Introduction # The growth of large‑language models has been exponential since Google introduced the transformer in June 2017.

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.

Alt Text
Security Landscape taken from Fig 1 https://arxiv.org/abs/2504.08623

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.

Alt Text
MAESTRO Framework

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)

  1. 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.

  2. 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.

  3. Dynamic Client Registration (RFC 7591)
    Clients can now automatically register with MCP authorization servers, reducing manual configuration and improving scalability.

  4. OAuth Server Metadata Discovery (RFC 8414)
    Clients must discover server endpoints and supported features via metadata - enabling dynamic discovery of authorize, token, and registration URLs.

  5. PKCE Enforcement
    PKCE is now mandatory for authorization code grants, ensuring public clients implement code_challenge and code_verifier to mitigate interception risks.

  6. Resource Indicators (RFC 8707)
    OAuth flows must include a resource parameter to bind access tokens to specific MCP servers - preventing token misuse across endpoints.

  7. 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 header. The MCP server verifies the token’s validity and, if it is valid and authorized, processes the user’s request hence giving access to the protected MCP tool.

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!
MCP - This article is part of a series.
Part 2.1: This Article

Related

MCP - You can run but you can't hide
·1176 words·6 mins· loading · loading
Automation MCP Microsoft Azure LLM
Introduction # The growth of large‑language models has been exponential since Google introduced the transformer in June 2017.