diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index 193993c15..c8bf9516b 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -342,23 +342,23 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow.
+
+ Sets an explicit domain for the signed cookies used by FastMCP's consent flow. By default, FastMCP uses `__Host-` cookies on HTTPS, which browsers restrict to the exact host.
- - `"host-only"` (default) uses the `__Host-` prefix on HTTPS. This is the strongest policy because browsers reject a `Domain` attribute and prevent sibling subdomains from setting the cookie.
- - `"domain-compatible"` uses the `__Secure-` prefix on HTTPS. Use this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. Cookie signing, `Secure`, `HttpOnly`, and `SameSite=Lax` remain enabled, but browser-enforced host isolation is lost.
+ Set this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. The value must exactly match the hostname in `base_url`; FastMCP rejects parent domains. Configuring it uses `__Secure-` cookies and preserves signing, `Secure`, `HttpOnly`, and `SameSite=Lax`.
```python
from fastmcp.server.auth.providers.azure import AzureProvider
auth = AzureProvider(
...,
- consent_cookie_policy="domain-compatible",
+ base_url="https://my-app.azurewebsites.net",
+ consent_cookie_domain="my-app.azurewebsites.net",
)
```
- Prefer the default `"host-only"` policy. The compatibility policy allows consent cookies to be scoped to sibling subdomains and should only be enabled when the deployment platform makes `__Host-` cookies unusable.
+ Prefer the host-only default. A domain cookie is also sent to child subdomains, so only enable this when those hosts are trusted and the deployment platform makes `__Host-` cookies unusable.
diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx
index aa8072fd2..a1b62f52e 100644
--- a/docs/servers/auth/oidc-proxy.mdx
+++ b/docs/servers/auth/oidc-proxy.mdx
@@ -209,8 +209,8 @@ auth = OIDCProxy(
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
-
- Consent cookie scope policy. The default `"host-only"` uses `__Host-` cookies on HTTPS. `"domain-compatible"` uses `__Secure-` cookies for hosting layers that add a `Domain` attribute, weakening browser-enforced host isolation while retaining signed, `Secure`, `HttpOnly`, `SameSite=Lax` cookies. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
+
+ Explicit domain for consent cookies. By default, FastMCP uses host-only `__Host-` cookies on HTTPS. Set this only for hosting layers that add a `Domain` attribute; the value must exactly match the hostname in `base_url`. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx
index 56caf5c85..5daef663e 100644
--- a/docs/v2/servers/auth/oauth-proxy.mdx
+++ b/docs/v2/servers/auth/oauth-proxy.mdx
@@ -323,23 +323,23 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow.
+
+ Sets an explicit domain for the signed cookies used by FastMCP's consent flow. By default, FastMCP uses `__Host-` cookies on HTTPS, which browsers restrict to the exact host.
- - `"host-only"` (default) uses the `__Host-` prefix on HTTPS. This is the strongest policy because browsers reject a `Domain` attribute and prevent sibling subdomains from setting the cookie.
- - `"domain-compatible"` uses the `__Secure-` prefix on HTTPS. Use this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. Cookie signing, `Secure`, `HttpOnly`, and `SameSite=Lax` remain enabled, but browser-enforced host isolation is lost.
+ Set this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. The value must exactly match the hostname in `base_url`; FastMCP rejects parent domains. Configuring it uses `__Secure-` cookies and preserves signing, `Secure`, `HttpOnly`, and `SameSite=Lax`.
```python
from fastmcp.server.auth.providers.azure import AzureProvider
auth = AzureProvider(
...,
- consent_cookie_policy="domain-compatible",
+ base_url="https://my-app.azurewebsites.net",
+ consent_cookie_domain="my-app.azurewebsites.net",
)
```
- Prefer the default `"host-only"` policy. The compatibility policy allows consent cookies to be scoped to sibling subdomains and should only be enabled when the deployment platform makes `__Host-` cookies unusable.
+ Prefer the host-only default. A domain cookie is also sent to child subdomains, so only enable this when those hosts are trusted and the deployment platform makes `__Host-` cookies unusable.
diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx
index e1071180a..c90efc171 100644
--- a/docs/v2/servers/auth/oidc-proxy.mdx
+++ b/docs/v2/servers/auth/oidc-proxy.mdx
@@ -202,8 +202,8 @@ auth = OIDCProxy(
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
-
- Consent cookie scope policy. The default `"host-only"` uses `__Host-` cookies on HTTPS. `"domain-compatible"` uses `__Secure-` cookies for hosting layers that add a `Domain` attribute, weakening browser-enforced host isolation while retaining signed, `Secure`, `HttpOnly`, `SameSite=Lax` cookies. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
+
+ Explicit domain for consent cookies. By default, FastMCP uses host-only `__Host-` cookies on HTTPS. Set this only for hosting layers that add a `Domain` attribute; the value must exactly match the hostname in `base_url`. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
diff --git a/docs/v3/servers/auth/oauth-proxy.mdx b/docs/v3/servers/auth/oauth-proxy.mdx
index 4fbed28e1..d7227b961 100644
--- a/docs/v3/servers/auth/oauth-proxy.mdx
+++ b/docs/v3/servers/auth/oauth-proxy.mdx
@@ -337,23 +337,23 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow.
+
+ Sets an explicit domain for the signed cookies used by FastMCP's consent flow. By default, FastMCP uses `__Host-` cookies on HTTPS, which browsers restrict to the exact host.
- - `"host-only"` (default) uses the `__Host-` prefix on HTTPS. This is the strongest policy because browsers reject a `Domain` attribute and prevent sibling subdomains from setting the cookie.
- - `"domain-compatible"` uses the `__Secure-` prefix on HTTPS. Use this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. Cookie signing, `Secure`, `HttpOnly`, and `SameSite=Lax` remain enabled, but browser-enforced host isolation is lost.
+ Set this only when a hosting layer, such as Azure Functions, adds a `Domain` attribute to response cookies. The value must exactly match the hostname in `base_url`; FastMCP rejects parent domains. Configuring it uses `__Secure-` cookies and preserves signing, `Secure`, `HttpOnly`, and `SameSite=Lax`.
```python
from fastmcp.server.auth.providers.azure import AzureProvider
auth = AzureProvider(
...,
- consent_cookie_policy="domain-compatible",
+ base_url="https://my-app.azurewebsites.net",
+ consent_cookie_domain="my-app.azurewebsites.net",
)
```
- Prefer the default `"host-only"` policy. The compatibility policy allows consent cookies to be scoped to sibling subdomains and should only be enabled when the deployment platform makes `__Host-` cookies unusable.
+ Prefer the host-only default. A domain cookie is also sent to child subdomains, so only enable this when those hosts are trusted and the deployment platform makes `__Host-` cookies unusable.
diff --git a/docs/v3/servers/auth/oidc-proxy.mdx b/docs/v3/servers/auth/oidc-proxy.mdx
index c44ee2f20..8993b5e3b 100644
--- a/docs/v3/servers/auth/oidc-proxy.mdx
+++ b/docs/v3/servers/auth/oidc-proxy.mdx
@@ -202,8 +202,8 @@ auth = OIDCProxy(
Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
-
- Consent cookie scope policy. The default `"host-only"` uses `__Host-` cookies on HTTPS. `"domain-compatible"` uses `__Secure-` cookies for hosting layers that add a `Domain` attribute, weakening browser-enforced host isolation while retaining signed, `Secure`, `HttpOnly`, `SameSite=Lax` cookies. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
+
+ Explicit domain for consent cookies. By default, FastMCP uses host-only `__Host-` cookies on HTTPS. Set this only for hosting layers that add a `Domain` attribute; the value must exactly match the hostname in `base_url`. See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for the security trade-off.
diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py
index 58e0b9f05..5e9ff4315 100644
--- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py
+++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py
@@ -7,9 +7,8 @@ This package provides OAuth proxy functionality split across multiple modules:
- proxy: Main OAuthProxy class
"""
-from fastmcp.server.auth.oauth_proxy.proxy import ConsentCookiePolicy, OAuthProxy
+from fastmcp.server.auth.oauth_proxy.proxy import OAuthProxy
__all__ = [
- "ConsentCookiePolicy",
"OAuthProxy",
]
diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py
index 3b7f65c70..21b37414a 100644
--- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py
+++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py
@@ -76,7 +76,7 @@ class ConsentMixin:
def _cookie_name(self: OAuthProxy, base_name: str) -> str:
"""Return secure cookie name for HTTPS, fallback for HTTP development."""
if self._is_https:
- if self._consent_cookie_policy == "domain-compatible":
+ if self._consent_cookie_domain is not None:
return f"__Secure-{base_name}"
return f"__Host-{base_name}"
return f"__{base_name}"
@@ -178,6 +178,7 @@ class ConsentMixin:
httponly=True,
samesite="lax",
path="/",
+ domain=self._consent_cookie_domain,
)
def _read_consent_bindings(self: OAuthProxy, request: Request) -> dict[str, str]:
@@ -220,6 +221,7 @@ class ConsentMixin:
httponly=True,
samesite="lax",
path="/",
+ domain=self._consent_cookie_domain,
)
return
payload_bytes = json.dumps(bindings, separators=(",", ":")).encode()
@@ -233,6 +235,7 @@ class ConsentMixin:
httponly=True,
samesite="lax",
path="/",
+ domain=self._consent_cookie_domain,
)
def _set_consent_binding_cookie(
diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py
index afe5611ec..985f057fb 100644
--- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py
+++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py
@@ -119,8 +119,6 @@ from fastmcp.utilities.logging import get_logger
logger = get_logger(__name__)
-ConsentCookiePolicy = Literal["host-only", "domain-compatible"]
-
_REFRESH_LOCK_CACHE_SIZE = 10_000
#: SEP-837: the client's declared `application_type`, recovered from the raw DCR
@@ -334,7 +332,7 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
jwt_signing_key: str | bytes | None = None,
# Consent screen configuration
require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_cookie_policy: ConsentCookiePolicy = "host-only",
+ consent_cookie_domain: str | None = None,
consent_csp_policy: str | None = None,
# Token expiry fallback
fallback_access_token_expiry_seconds: int | None = None,
@@ -406,13 +404,14 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
FastMCP does not provide or verify those external protections.
- False: skip consent entirely. SECURITY WARNING: only set to
False for local development or testing environments.
- consent_cookie_policy: Cookie scope policy for the consent flow.
- ``"host-only"`` (default) uses ``__Host-`` cookies on HTTPS,
- which browsers reject if a hosting layer adds a ``Domain``
- attribute. ``"domain-compatible"`` uses ``__Secure-`` cookies
- instead so those rewrites remain valid, at the cost of allowing
- the cookies to be scoped to sibling subdomains. Cookie signing,
- ``Secure``, ``HttpOnly``, and ``SameSite=Lax`` remain enabled.
+ consent_cookie_domain: Explicit domain for consent cookies. By default,
+ FastMCP uses host-only ``__Host-`` cookies on HTTPS. Set this only
+ when a hosting layer adds a ``Domain`` attribute to response cookies.
+ The value must exactly match the hostname in ``base_url``; parent
+ domains are rejected. Configuring a domain uses ``__Secure-`` cookies
+ and allows them to reach child subdomains, weakening browser-enforced
+ host isolation. Cookie signing, ``Secure``, ``HttpOnly``, and
+ ``SameSite=Lax`` remain enabled.
consent_csp_policy: Content Security Policy for the consent page.
If None (default), uses the built-in CSP policy with appropriate directives.
If empty string "", disables CSP entirely (no meta tag is rendered).
@@ -527,17 +526,26 @@ class OAuthProxy(OAuthProvider, ConsentMixin):
self._require_authorization_consent: bool | Literal["remember", "external"] = (
require_authorization_consent
)
- if consent_cookie_policy not in ("host-only", "domain-compatible"):
- raise ValueError(
- "consent_cookie_policy must be 'host-only' or 'domain-compatible'"
- )
- self._consent_cookie_policy: ConsentCookiePolicy = consent_cookie_policy
+ self._consent_cookie_domain: str | None = None
+ if consent_cookie_domain is not None:
+ assert self.base_url is not None
+ assert self.base_url.host is not None
+ configured_domain = consent_cookie_domain.lower().rstrip(".")
+ base_host = self.base_url.host.lower().rstrip(".")
+ if configured_domain != base_host:
+ raise ValueError(
+ "consent_cookie_domain must exactly match the hostname in base_url"
+ )
+ if self.base_url.scheme != "https":
+ raise ValueError("consent_cookie_domain requires an HTTPS base_url")
+ self._consent_cookie_domain = configured_domain
self._consent_csp_policy: str | None = consent_csp_policy
- if consent_cookie_policy == "domain-compatible":
+ if self._consent_cookie_domain is not None:
logger.warning(
- "Domain-compatible consent cookies use __Secure- instead of "
- "__Host-; this permits Domain rewrites but weakens browser-enforced "
- "host isolation."
+ "Consent cookies are scoped to %s and use __Secure- instead of "
+ "__Host-. Child subdomains can receive or replace these cookies; "
+ "only use this setting when those hosts are trusted.",
+ self._consent_cookie_domain,
)
if require_authorization_consent == "external":
logger.info(
diff --git a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py
index 783436c37..69413a8fd 100644
--- a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py
+++ b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py
@@ -19,7 +19,7 @@ from typing_extensions import Self
from fastmcp.server.auth import TokenVerifier
from fastmcp.server.auth.identity_assertion import IdentityAssertion
-from fastmcp.server.auth.oauth_proxy import ConsentCookiePolicy, OAuthProxy
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.oauth_proxy.models import UpstreamTokenSet
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.logging import get_logger
@@ -234,7 +234,7 @@ class OIDCProxy(OAuthProxy):
token_endpoint_auth_method: str | None = None,
# Consent screen configuration
require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_cookie_policy: ConsentCookiePolicy = "host-only",
+ consent_cookie_domain: str | None = None,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
# Extra parameters
@@ -311,11 +311,11 @@ class OIDCProxy(OAuthProxy):
but the warning is suppressed as an operator acknowledgment that
equivalent protections are enforced externally.
SECURITY WARNING: Only set to False for local development or testing environments.
- consent_cookie_policy: Cookie scope policy for the consent flow.
- ``"host-only"`` (default) uses ``__Host-`` cookies on HTTPS.
- ``"domain-compatible"`` uses ``__Secure-`` cookies for hosting
- layers that add a ``Domain`` attribute, weakening browser-enforced
- host isolation while retaining signed, secure cookies.
+ consent_cookie_domain: Explicit domain for consent cookies. By default,
+ FastMCP uses host-only ``__Host-`` cookies on HTTPS. Set this only
+ for hosting layers that add a ``Domain`` attribute. The value must
+ exactly match the hostname in ``base_url`` and causes ``__Secure-``
+ cookies to be used, allowing child subdomains to receive them.
consent_csp_policy: Content Security Policy for the consent page.
If None (default), uses the built-in CSP policy with appropriate directives.
If empty string "", disables CSP entirely (no meta tag is rendered).
@@ -436,7 +436,7 @@ class OIDCProxy(OAuthProxy):
"jwt_signing_key": jwt_signing_key,
"token_endpoint_auth_method": token_endpoint_auth_method,
"require_authorization_consent": require_authorization_consent,
- "consent_cookie_policy": consent_cookie_policy,
+ "consent_cookie_domain": consent_cookie_domain,
"consent_csp_policy": consent_csp_policy,
"forward_resource": forward_resource,
"fallback_access_token_expiry_seconds": fallback_access_token_expiry_seconds,
diff --git a/fastmcp_slim/fastmcp/server/auth/providers/azure.py b/fastmcp_slim/fastmcp/server/auth/providers/azure.py
index 74fe9a08f..b38acf9ec 100644
--- a/fastmcp_slim/fastmcp/server/auth/providers/azure.py
+++ b/fastmcp_slim/fastmcp/server/auth/providers/azure.py
@@ -15,7 +15,7 @@ from key_value.aio.protocols import AsyncKeyValue
from fastmcp.dependencies import Dependency
from fastmcp.server.auth.auth import MultiAuth
-from fastmcp.server.auth.oauth_proxy import ConsentCookiePolicy, OAuthProxy
+from fastmcp.server.auth.oauth_proxy import OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.utilities.auth import decode_jwt_payload, parse_scopes
from fastmcp.utilities.logging import get_logger
@@ -113,7 +113,7 @@ class AzureProvider(OAuthProxy):
client_storage: AsyncKeyValue | None = None,
jwt_signing_key: str | bytes | None = None,
require_authorization_consent: bool | Literal["remember", "external"] = True,
- consent_cookie_policy: ConsentCookiePolicy = "host-only",
+ consent_cookie_domain: str | None = None,
consent_csp_policy: str | None = None,
forward_resource: bool = True,
fallback_refresh_token_expiry_seconds: int | None = None,
@@ -178,12 +178,12 @@ class AzureProvider(OAuthProxy):
but the warning is suppressed as an operator acknowledgment that
equivalent protections are enforced externally.
SECURITY WARNING: Only set to False for local development or testing environments.
- consent_cookie_policy: Cookie scope policy for the consent flow.
- ``"host-only"`` (default) uses ``__Host-`` cookies on HTTPS.
- ``"domain-compatible"`` uses ``__Secure-`` cookies for hosting
- layers such as Azure Functions that add a ``Domain`` attribute,
- weakening browser-enforced host isolation while retaining signed,
- secure cookies.
+ consent_cookie_domain: Explicit domain for consent cookies. By default,
+ FastMCP uses host-only ``__Host-`` cookies on HTTPS. Set this only
+ for hosting layers such as Azure Functions that add a ``Domain``
+ attribute. The value must exactly match the hostname in ``base_url``
+ and causes ``__Secure-`` cookies to be used, allowing child
+ subdomains to receive them.
http_client: Optional httpx2.AsyncClient for connection pooling in JWKS fetches.
When provided, the client is reused for JWT key fetches and the caller
is responsible for its lifecycle. When None (default), a fresh client is created per fetch.
@@ -280,7 +280,7 @@ class AzureProvider(OAuthProxy):
client_storage=client_storage,
jwt_signing_key=jwt_signing_key,
require_authorization_consent=require_authorization_consent,
- consent_cookie_policy=consent_cookie_policy,
+ consent_cookie_domain=consent_cookie_domain,
consent_csp_policy=consent_csp_policy,
forward_resource=forward_resource,
fallback_refresh_token_expiry_seconds=fallback_refresh_token_expiry_seconds,
diff --git a/tests/server/auth/providers/test_azure.py b/tests/server/auth/providers/test_azure.py
index 4bbcdb29d..981b4f3e0 100644
--- a/tests/server/auth/providers/test_azure.py
+++ b/tests/server/auth/providers/test_azure.py
@@ -66,7 +66,7 @@ class TestAzureProvider:
assert provider._redirect_path == "/auth/callback"
# Azure provider defaults are set but we can't easily verify them without accessing internals
- def test_domain_compatible_consent_cookies(self, memory_storage: MemoryStore):
+ def test_explicit_consent_cookie_domain(self, memory_storage: MemoryStore):
provider = AzureProvider(
client_id="test_client",
client_secret="test_secret",
@@ -75,12 +75,13 @@ class TestAzureProvider:
required_scopes=["read"],
jwt_signing_key="test-secret",
client_storage=memory_storage,
- consent_cookie_policy="domain-compatible",
+ consent_cookie_domain="myserver.com",
)
assert (
provider._cookie_name("MCP_CONSENT_STATE") == "__Secure-MCP_CONSENT_STATE"
)
+ assert provider._consent_cookie_domain == "myserver.com"
def test_offline_access_automatically_included(self, memory_storage: MemoryStore):
"""Test that offline_access is automatically added to get refresh tokens."""
diff --git a/tests/server/auth/test_oauth_consent_page.py b/tests/server/auth/test_oauth_consent_page.py
index f7019906d..2f092eff8 100644
--- a/tests/server/auth/test_oauth_consent_page.py
+++ b/tests/server/auth/test_oauth_consent_page.py
@@ -65,8 +65,8 @@ def oauth_proxy_https_remember():
@pytest.fixture
-def oauth_proxy_https_domain_compatible():
- """OAuthProxy using __Secure- cookies for platforms that add Domain."""
+def oauth_proxy_https_with_cookie_domain():
+ """OAuthProxy using an explicit cookie domain for platform compatibility."""
return OAuthProxy(
upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
upstream_token_endpoint="https://github.com/login/oauth/access_token",
@@ -76,7 +76,7 @@ def oauth_proxy_https_domain_compatible():
base_url="https://myserver.example",
client_storage=MemoryStore(),
jwt_signing_key="test-secret",
- consent_cookie_policy="domain-compatible",
+ consent_cookie_domain="myserver.example",
)
@@ -116,11 +116,11 @@ def _extract_csrf(html: str) -> str | None:
return m.group(1) if m else None
-class TestConsentCookiePolicy:
- def test_invalid_policy_is_rejected(self):
+class TestConsentCookieDomain:
+ def test_parent_domain_is_rejected(self):
with pytest.raises(
ValueError,
- match="consent_cookie_policy must be 'host-only' or 'domain-compatible'",
+ match="consent_cookie_domain must exactly match the hostname in base_url",
):
OAuthProxy(
upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
@@ -131,20 +131,36 @@ class TestConsentCookiePolicy:
base_url="https://myserver.example",
client_storage=MemoryStore(),
jwt_signing_key="test-secret",
- consent_cookie_policy="invalid", # ty: ignore[invalid-argument-type]
+ consent_cookie_domain="example",
)
- async def test_domain_compatible_policy_completes_consent_with_secure_cookies(
- self, oauth_proxy_https_domain_compatible
+ def test_domain_requires_https(self):
+ with pytest.raises(
+ ValueError, match="consent_cookie_domain requires an HTTPS base_url"
+ ):
+ OAuthProxy(
+ upstream_authorization_endpoint="https://github.com/login/oauth/authorize",
+ upstream_token_endpoint="https://github.com/login/oauth/access_token",
+ upstream_client_id="client-id",
+ upstream_client_secret="client-secret",
+ token_verifier=_Verifier(),
+ base_url="http://myserver.example",
+ client_storage=MemoryStore(),
+ jwt_signing_key="test-secret",
+ consent_cookie_domain="myserver.example",
+ )
+
+ async def test_explicit_domain_completes_consent_with_secure_cookies(
+ self, oauth_proxy_https_with_cookie_domain
):
txn_id, _ = await _start_flow(
- oauth_proxy_https_domain_compatible,
- "domain-compatible-client",
+ oauth_proxy_https_with_cookie_domain,
+ "explicit-domain-client",
"http://localhost:6000/callback",
)
- app = Starlette(routes=oauth_proxy_https_domain_compatible.get_routes())
+ app = Starlette(routes=oauth_proxy_https_with_cookie_domain.get_routes())
- with TestClient(app) as client:
+ with TestClient(app, base_url="https://myserver.example") as client:
consent = client.get(f"/consent?txn_id={txn_id}")
csrf = _extract_csrf(consent.text)
assert csrf
@@ -154,9 +170,8 @@ class TestConsentCookiePolicy:
assert "Secure" in set_cookie
assert "HttpOnly" in set_cookie
assert "SameSite=lax" in set_cookie
+ assert "Domain=myserver.example" in set_cookie
- for key, value in consent.cookies.items():
- client.cookies.set(key, value)
response = client.post(
"/consent",
data={
@@ -168,25 +183,27 @@ class TestConsentCookiePolicy:
)
assert response.status_code in (302, 303)
- assert "__Secure-MCP_CONSENT_BINDING=" in response.headers.get("set-cookie", "")
+ binding_cookie = response.headers.get("set-cookie", "")
+ assert "__Secure-MCP_CONSENT_BINDING=" in binding_cookie
+ assert "Domain=myserver.example" in binding_cookie
- async def test_domain_compatible_policy_does_not_accept_host_prefix(
- self, oauth_proxy_https_domain_compatible
+ async def test_explicit_domain_does_not_accept_host_prefix(
+ self, oauth_proxy_https_with_cookie_domain
):
txn_id, _ = await _start_flow(
- oauth_proxy_https_domain_compatible,
+ oauth_proxy_https_with_cookie_domain,
"wrong-prefix-client",
"http://localhost:6001/callback",
)
- app = Starlette(routes=oauth_proxy_https_domain_compatible.get_routes())
+ app = Starlette(routes=oauth_proxy_https_with_cookie_domain.get_routes())
- with TestClient(app) as browser:
+ with TestClient(app, base_url="https://myserver.example") as browser:
consent = browser.get(f"/consent?txn_id={txn_id}")
csrf = _extract_csrf(consent.text)
assert csrf
signed_state = consent.cookies["__Secure-MCP_CONSENT_STATE"]
- with TestClient(app) as browser:
+ with TestClient(app, base_url="https://myserver.example") as browser:
browser.cookies.set("__Host-MCP_CONSENT_STATE", signed_state)
response = browser.post(
"/consent",
@@ -200,9 +217,7 @@ class TestConsentCookiePolicy:
assert response.status_code == 403
- async def test_default_policy_does_not_accept_secure_prefix(
- self, oauth_proxy_https
- ):
+ async def test_default_does_not_accept_secure_prefix(self, oauth_proxy_https):
txn_id, _ = await _start_flow(
oauth_proxy_https,
"default-policy-client",
@@ -230,11 +245,12 @@ class TestConsentCookiePolicy:
assert response.status_code == 403
- def test_default_policy_remains_host_only(self, oauth_proxy_https):
+ def test_default_remains_host_only(self, oauth_proxy_https):
assert (
oauth_proxy_https._cookie_name("MCP_CONSENT_STATE")
== "__Host-MCP_CONSENT_STATE"
)
+ assert oauth_proxy_https._consent_cookie_domain is None
class TestConsentPageServerIcon:
diff --git a/tests/server/auth/test_oidc_proxy.py b/tests/server/auth/test_oidc_proxy.py
index c10ec990c..2bad5140c 100644
--- a/tests/server/auth/test_oidc_proxy.py
+++ b/tests/server/auth/test_oidc_proxy.py
@@ -489,7 +489,7 @@ class TestOIDCProxyInitialization:
call_args = mock_get.call_args
assert call_args[1]["timeout_seconds"] == 12
- def test_domain_compatible_consent_cookies(self, valid_oidc_configuration_dict):
+ def test_explicit_consent_cookie_domain(self, valid_oidc_configuration_dict):
with patch(
"fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration"
) as mock_get:
@@ -504,13 +504,14 @@ class TestOIDCProxyInitialization:
client_secret=TEST_CLIENT_SECRET,
base_url=TEST_BASE_URL,
jwt_signing_key="test-secret",
- consent_cookie_policy="domain-compatible",
+ consent_cookie_domain="example.com",
)
validate_proxy(mock_get, proxy, oidc_config)
assert (
proxy._cookie_name("MCP_CONSENT_STATE") == "__Secure-MCP_CONSENT_STATE"
)
+ assert proxy._consent_cookie_domain == "example.com"
def test_token_verifier_initialization(self, valid_oidc_configuration_dict):
"""Test token verifier initialization."""