diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx index 03727ec87..193993c15 100644 --- a/docs/servers/auth/oauth-proxy.mdx +++ b/docs/servers/auth/oauth-proxy.mdx @@ -342,6 +342,26 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) + + Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow. + + - `"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. + + ```python + from fastmcp.server.auth.providers.azure import AzureProvider + + auth = AzureProvider( + ..., + consent_cookie_policy="domain-compatible", + ) + ``` + + + 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. + + + Content Security Policy for the consent page. diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx index be4bcb22d..aa8072fd2 100644 --- a/docs/servers/auth/oidc-proxy.mdx +++ b/docs/servers/auth/oidc-proxy.mdx @@ -209,6 +209,10 @@ 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. + + Content Security Policy for the consent page. diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx index 678c396b5..56caf5c85 100644 --- a/docs/v2/servers/auth/oauth-proxy.mdx +++ b/docs/v2/servers/auth/oauth-proxy.mdx @@ -323,6 +323,26 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) + + Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow. + + - `"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. + + ```python + from fastmcp.server.auth.providers.azure import AzureProvider + + auth = AzureProvider( + ..., + consent_cookie_policy="domain-compatible", + ) + ``` + + + 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. + + + Content Security Policy for the consent page. diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx index a7988995e..e1071180a 100644 --- a/docs/v2/servers/auth/oidc-proxy.mdx +++ b/docs/v2/servers/auth/oidc-proxy.mdx @@ -202,6 +202,10 @@ 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. + + Content Security Policy for the consent page. diff --git a/docs/v3/servers/auth/oauth-proxy.mdx b/docs/v3/servers/auth/oauth-proxy.mdx index 79ba7bf09..4fbed28e1 100644 --- a/docs/v3/servers/auth/oauth-proxy.mdx +++ b/docs/v3/servers/auth/oauth-proxy.mdx @@ -337,6 +337,26 @@ auth = OAuthProxy(..., client_storage=MemoryStore()) + + Controls the browser-enforced scope of the signed cookies used by FastMCP's consent flow. + + - `"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. + + ```python + from fastmcp.server.auth.providers.azure import AzureProvider + + auth = AzureProvider( + ..., + consent_cookie_policy="domain-compatible", + ) + ``` + + + 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. + + + Content Security Policy for the consent page. diff --git a/docs/v3/servers/auth/oidc-proxy.mdx b/docs/v3/servers/auth/oidc-proxy.mdx index 81ca677a2..c44ee2f20 100644 --- a/docs/v3/servers/auth/oidc-proxy.mdx +++ b/docs/v3/servers/auth/oidc-proxy.mdx @@ -202,6 +202,10 @@ 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. + + Content Security Policy for the consent page. diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py index 5e9ff4315..58e0b9f05 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/__init__.py @@ -7,8 +7,9 @@ This package provides OAuth proxy functionality split across multiple modules: - proxy: Main OAuthProxy class """ -from fastmcp.server.auth.oauth_proxy.proxy import OAuthProxy +from fastmcp.server.auth.oauth_proxy.proxy import ConsentCookiePolicy, 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 5eda1d70d..3b7f65c70 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/consent.py @@ -76,6 +76,8 @@ 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": + return f"__Secure-{base_name}" return f"__Host-{base_name}" return f"__{base_name}" @@ -129,9 +131,8 @@ class ConsentMixin: """Decode and verify a signed base64-encoded JSON list from cookie. Returns [] if missing/invalid.""" secure_name = self._cookie_name(base_name) raw = request.cookies.get(secure_name) - # Only fall back to the non-__Host- name over plain HTTP. On HTTPS, - # __Host- enforces host-only scope; accepting the weaker name would - # let a sibling-subdomain attacker inject a domain-scoped cookie. + # Only fall back to the unprefixed development name over plain HTTP. + # On HTTPS, accept exactly the prefix selected by the configured policy. if not raw and not self._is_https: raw = request.cookies.get(f"__{base_name}") if not raw: @@ -186,9 +187,8 @@ class ConsentMixin: """ cookie_name = self._cookie_name("MCP_CONSENT_BINDING") raw = request.cookies.get(cookie_name) - # Only fall back to the non-__Host- name over plain HTTP. On HTTPS, - # __Host- enforces host-only scope; accepting the weaker name would - # bypass that guarantee. + # Only fall back to the unprefixed development name over plain HTTP. + # On HTTPS, accept exactly the prefix selected by the configured policy. if not raw and not self._is_https: raw = request.cookies.get("__MCP_CONSENT_BINDING") if not raw: diff --git a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py index 760552bcc..afe5611ec 100644 --- a/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py +++ b/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py @@ -119,6 +119,8 @@ 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 @@ -332,6 +334,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_csp_policy: str | None = None, # Token expiry fallback fallback_access_token_expiry_seconds: int | None = None, @@ -403,6 +406,13 @@ 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_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). @@ -517,7 +527,18 @@ 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_csp_policy: str | None = consent_csp_policy + if consent_cookie_policy == "domain-compatible": + logger.warning( + "Domain-compatible consent cookies use __Secure- instead of " + "__Host-; this permits Domain rewrites but weakens browser-enforced " + "host isolation." + ) if require_authorization_consent == "external": logger.info( "Built-in consent screen disabled; consent is handled externally." diff --git a/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py b/fastmcp_slim/fastmcp/server/auth/oidc_proxy.py index 5e4086c7a..783436c37 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 OAuthProxy +from fastmcp.server.auth.oauth_proxy import ConsentCookiePolicy, 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,6 +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_csp_policy: str | None = None, forward_resource: bool = True, # Extra parameters @@ -310,6 +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_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). @@ -430,6 +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_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 b0cf9d360..74fe9a08f 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 OAuthProxy +from fastmcp.server.auth.oauth_proxy import ConsentCookiePolicy, 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,6 +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_csp_policy: str | None = None, forward_resource: bool = True, fallback_refresh_token_expiry_seconds: int | None = None, @@ -177,6 +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. 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. @@ -273,6 +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_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 704af3ab4..4bbcdb29d 100644 --- a/tests/server/auth/providers/test_azure.py +++ b/tests/server/auth/providers/test_azure.py @@ -66,6 +66,22 @@ 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): + provider = AzureProvider( + client_id="test_client", + client_secret="test_secret", + tenant_id="test-tenant", + base_url="https://myserver.com", + required_scopes=["read"], + jwt_signing_key="test-secret", + client_storage=memory_storage, + consent_cookie_policy="domain-compatible", + ) + + assert ( + provider._cookie_name("MCP_CONSENT_STATE") == "__Secure-MCP_CONSENT_STATE" + ) + def test_offline_access_automatically_included(self, memory_storage: MemoryStore): """Test that offline_access is automatically added to get refresh tokens.""" # Without specifying offline_access diff --git a/tests/server/auth/test_oauth_consent_page.py b/tests/server/auth/test_oauth_consent_page.py index 1cab95190..f7019906d 100644 --- a/tests/server/auth/test_oauth_consent_page.py +++ b/tests/server/auth/test_oauth_consent_page.py @@ -64,6 +64,22 @@ def oauth_proxy_https_remember(): ) +@pytest.fixture +def oauth_proxy_https_domain_compatible(): + """OAuthProxy using __Secure- cookies for platforms that add Domain.""" + return 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="https://myserver.example", + client_storage=MemoryStore(), + jwt_signing_key="test-secret", + consent_cookie_policy="domain-compatible", + ) + + async def _start_flow( proxy: OAuthProxy, client_id: str, redirect: str ) -> tuple[str, str]: @@ -100,6 +116,127 @@ def _extract_csrf(html: str) -> str | None: return m.group(1) if m else None +class TestConsentCookiePolicy: + def test_invalid_policy_is_rejected(self): + with pytest.raises( + ValueError, + match="consent_cookie_policy must be 'host-only' or 'domain-compatible'", + ): + 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="https://myserver.example", + client_storage=MemoryStore(), + jwt_signing_key="test-secret", + consent_cookie_policy="invalid", # ty: ignore[invalid-argument-type] + ) + + async def test_domain_compatible_policy_completes_consent_with_secure_cookies( + self, oauth_proxy_https_domain_compatible + ): + txn_id, _ = await _start_flow( + oauth_proxy_https_domain_compatible, + "domain-compatible-client", + "http://localhost:6000/callback", + ) + app = Starlette(routes=oauth_proxy_https_domain_compatible.get_routes()) + + with TestClient(app) as client: + consent = client.get(f"/consent?txn_id={txn_id}") + csrf = _extract_csrf(consent.text) + assert csrf + set_cookie = consent.headers.get("set-cookie", "") + assert "__Secure-MCP_CONSENT_STATE=" in set_cookie + assert "__Host-" not in set_cookie + assert "Secure" in set_cookie + assert "HttpOnly" in set_cookie + assert "SameSite=lax" in set_cookie + + for key, value in consent.cookies.items(): + client.cookies.set(key, value) + response = client.post( + "/consent", + data={ + "action": "approve", + "txn_id": txn_id, + "csrf_token": csrf, + }, + follow_redirects=False, + ) + + assert response.status_code in (302, 303) + assert "__Secure-MCP_CONSENT_BINDING=" in response.headers.get("set-cookie", "") + + async def test_domain_compatible_policy_does_not_accept_host_prefix( + self, oauth_proxy_https_domain_compatible + ): + txn_id, _ = await _start_flow( + oauth_proxy_https_domain_compatible, + "wrong-prefix-client", + "http://localhost:6001/callback", + ) + app = Starlette(routes=oauth_proxy_https_domain_compatible.get_routes()) + + with TestClient(app) 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: + browser.cookies.set("__Host-MCP_CONSENT_STATE", signed_state) + response = browser.post( + "/consent", + data={ + "action": "approve", + "txn_id": txn_id, + "csrf_token": csrf, + }, + follow_redirects=False, + ) + + assert response.status_code == 403 + + async def test_default_policy_does_not_accept_secure_prefix( + self, oauth_proxy_https + ): + txn_id, _ = await _start_flow( + oauth_proxy_https, + "default-policy-client", + "http://localhost:6002/callback", + ) + app = Starlette(routes=oauth_proxy_https.get_routes()) + + with TestClient(app) as browser: + consent = browser.get(f"/consent?txn_id={txn_id}") + csrf = _extract_csrf(consent.text) + assert csrf + signed_state = consent.cookies["__Host-MCP_CONSENT_STATE"] + + with TestClient(app) as browser: + browser.cookies.set("__Secure-MCP_CONSENT_STATE", signed_state) + response = browser.post( + "/consent", + data={ + "action": "approve", + "txn_id": txn_id, + "csrf_token": csrf, + }, + follow_redirects=False, + ) + + assert response.status_code == 403 + + def test_default_policy_remains_host_only(self, oauth_proxy_https): + assert ( + oauth_proxy_https._cookie_name("MCP_CONSENT_STATE") + == "__Host-MCP_CONSENT_STATE" + ) + + class TestConsentPageServerIcon: """Tests for server icon display in OAuth consent screen.""" diff --git a/tests/server/auth/test_oidc_proxy.py b/tests/server/auth/test_oidc_proxy.py index 755049120..c10ec990c 100644 --- a/tests/server/auth/test_oidc_proxy.py +++ b/tests/server/auth/test_oidc_proxy.py @@ -489,6 +489,29 @@ 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): + with patch( + "fastmcp.server.auth.oidc_proxy.OIDCConfiguration.get_oidc_configuration" + ) as mock_get: + oidc_config = OIDCConfiguration.model_validate( + valid_oidc_configuration_dict + ) + mock_get.return_value = oidc_config + + proxy = OIDCProxy( + config_url=TEST_CONFIG_URL, + client_id=TEST_CLIENT_ID, + client_secret=TEST_CLIENT_SECRET, + base_url=TEST_BASE_URL, + jwt_signing_key="test-secret", + consent_cookie_policy="domain-compatible", + ) + + validate_proxy(mock_get, proxy, oidc_config) + assert ( + proxy._cookie_name("MCP_CONSENT_STATE") == "__Secure-MCP_CONSENT_STATE" + ) + def test_token_verifier_initialization(self, valid_oidc_configuration_dict): """Test token verifier initialization.""" with patch(