Description
Problem:
The allowed_client_redirect_uris configuration parameter is not being enforced during OAuth authorization flows. Clients can bypass this restriction by registering arbitrary redirect URIs through Dynamic Client Registration (DCR), allowing unauthorized redirect URIs to be used in authorization requests.
If I understand this correctly this bypasses the intended design of allowed_client_redirect_uris where you can limit what redirect loopback uri mcp clients can utilise.
Setting the list to empty [] or even including specific uris doesn't appear to work, below is an example segment for what I'm trying to test this where I'm block everything but the "https://claude.ai/api/mcp/auth_callback" URI.
auth_provider = AzureProvider(
client_id=os.getenv("AZURE_OAUTH_CLIENT_ID"),
client_secret=os.getenv("AZURE_OAUTH_CLIENT_SECRET"),
tenant_id=os.getenv("AZURE_TENANT_ID"),
base_url=os.getenv("MCP_SERVER_BASE_URL"),
required_scopes=["read"],
allowed_client_redirect_uris=[
"https://claude.ai/api/mcp/auth_callback",
],
)
From the troubleshooting I've done myself I believe the main culprit is that at src/fastmcp/server/auth/oauth_proxy/models.py
When a redirect URI doesn't match allowed_patterns, it falls back to super().validate_redirect_uri() (from OAuthClientInformationFull), which only checks if the URI was registered during DCR. This creates a bypass: any URI registered via DCR is automatically validated, regardless of the allowed_client_redirect_uris restriction.
Below is the section in models.py Lines 160 - 179
def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
"""Validate redirect URI against allowed patterns.
Since we're acting as a proxy and clients register dynamically,
we validate their redirect URIs against configurable patterns.
This is essential for cached token scenarios where the client may
reconnect with a different port.
"""
if redirect_uri is not None:
# Validate against allowed patterns
if validate_redirect_uri(
redirect_uri=redirect_uri,
allowed_patterns=self.allowed_redirect_uri_patterns,
):
return redirect_uri
# Fall back to normal validation if not in allowed patterns
return super().validate_redirect_uri(redirect_uri)
# If no redirect_uri provided, use default behavior
return super().validate_redirect_uri(redirect_uri)
DCR Client registration section in src/fastmcp/server/auth/oauth_proxy/proxy.py
Lines 572 - 620
@override
async def register_client(self, client_info: OAuthClientInformationFull) -> None:
"""Register a client locally
When a client registers, we create a ProxyDCRClient that is more
forgiving about validating redirect URIs, since the DCR client's
redirect URI will likely be localhost or unknown to the proxied IDP. The
proxied IDP only knows about this server's fixed redirect URI.
"""
# Create a ProxyDCRClient with configured redirect URI validation
if client_info.client_id is None:
raise ValueError("client_id is required for client registration")
# We use token_endpoint_auth_method="none" because the proxy handles
# all upstream authentication. The client_secret must also be None
# because the SDK requires secrets to be provided if they're set,
# regardless of auth method.
proxy_client: ProxyDCRClient = ProxyDCRClient(
client_id=client_info.client_id,
client_secret=None,
redirect_uris=client_info.redirect_uris or [AnyUrl("http://localhost")],
grant_types=client_info.grant_types
or ["authorization_code", "refresh_token"],
scope=client_info.scope or self._default_scope_str,
token_endpoint_auth_method="none",
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
client_name=getattr(client_info, "client_name", None),
)
I haven't managed to get as far as approving the consent screen as I've been testing completing a registration just using DCR to add a disallowed redirect uri but theoretically a user could click to consent to the malicious redirect uri as per below which I got with my test example server code below.
INFO: 127.0.0.1:57536 - "POST /register HTTP/1.1" 201 Created
INFO: 127.0.0.1:57536 - "GET /authorize?client_id=c3438fa4-a0f3-4f3d-b8fc-9d2feb722f83&redirect_uri=https://evil.com/steal-authorization-codes&response_type=code&scope=read&state=test456&code_challenge=Lt3mTWF8q8c1lK8l26cNYD9LT6RiaS5yVvhpd6Arm60&code_challenge_method=S256 HTTP/1.1" 302 Found
Expectation:
My expectation is that the MCP server should deny the client attempting to register with the disallowed client redirect uri.
===
I'm not a seasoned python developer by any means so please tell me where I'm wrong with this and I hope this makes sense :), I'm currently trying to fully understand how to best allow only specific clients to use an MCP server for my organisation and this seems like a piece of that puzzle.
I believe this is the case for 2. versions and the 3. version (The one I tested with)
P.S. Your talk on "Your MCP server is bad (and you should feel bad)" was great :)
Example Code
import os
from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider
# Load environment variables from .env file
load_dotenv()
# The AzureProvider handles Azure's token format and validation
auth_provider = AzureProvider(
client_id=os.getenv("AZURE_OAUTH_CLIENT_ID"),
client_secret=os.getenv("AZURE_OAUTH_CLIENT_SECRET"),
tenant_id=os.getenv("AZURE_TENANT_ID"),
base_url=os.getenv("MCP_SERVER_BASE_URL"),
required_scopes=["read"],
allowed_client_redirect_uris=[
"https://claude.ai/api/mcp/auth_callback",
]
)
# Create the MCP server instance
mcp = FastMCP(os.getenv("MCP_SERVER_NAME", "MCP Server"),
auth=auth_provider)
Version Information
FastMCP version: 3.0.0b1
MCP version: 1.26.0
Python version: 3.12.3
Platform: Linux-6.6.87.2-microsoft-standard-WSL2-x86_64-with-glibc2.39
FastMCP root path: /home/REDACTED/REDACTED/.venv/lib/python3.12/site-packages
Description
Problem:
The allowed_client_redirect_uris configuration parameter is not being enforced during OAuth authorization flows. Clients can bypass this restriction by registering arbitrary redirect URIs through Dynamic Client Registration (DCR), allowing unauthorized redirect URIs to be used in authorization requests.
If I understand this correctly this bypasses the intended design of allowed_client_redirect_uris where you can limit what redirect loopback uri mcp clients can utilise.
Setting the list to empty [] or even including specific uris doesn't appear to work, below is an example segment for what I'm trying to test this where I'm block everything but the "https://claude.ai/api/mcp/auth_callback" URI.
From the troubleshooting I've done myself I believe the main culprit is that at src/fastmcp/server/auth/oauth_proxy/models.py
When a redirect URI doesn't match allowed_patterns, it falls back to super().validate_redirect_uri() (from OAuthClientInformationFull), which only checks if the URI was registered during DCR. This creates a bypass: any URI registered via DCR is automatically validated, regardless of the allowed_client_redirect_uris restriction.
Below is the section in models.py Lines 160 - 179
DCR Client registration section in src/fastmcp/server/auth/oauth_proxy/proxy.py
Lines 572 - 620
I haven't managed to get as far as approving the consent screen as I've been testing completing a registration just using DCR to add a disallowed redirect uri but theoretically a user could click to consent to the malicious redirect uri as per below which I got with my test example server code below.
Expectation:
My expectation is that the MCP server should deny the client attempting to register with the disallowed client redirect uri.
===
I'm not a seasoned python developer by any means so please tell me where I'm wrong with this and I hope this makes sense :), I'm currently trying to fully understand how to best allow only specific clients to use an MCP server for my organisation and this seems like a piece of that puzzle.
I believe this is the case for 2. versions and the 3. version (The one I tested with)
P.S. Your talk on "Your MCP server is bad (and you should feel bad)" was great :)
Example Code
Version Information