From 05db547d8683c2a762a4ac48421e33a9ed8826d0 Mon Sep 17 00:00:00 2001
From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com>
Date: Sat, 22 Nov 2025 09:20:50 -0800
Subject: [PATCH] feat: Made Changes to DescopeProvider to Support New Well
Known URLs (#2392)
* added new DescopeProvider for Agentic Identity Hub
* fixed formatting with ruff
* made coderabbit change
* changed wording in descope.mdx
* added backwards compatibility
* ran ruff
* added scopes_supported to protected metadata resource
* empty commit to re-run tests
---
docs/integrations/descope.mdx | 60 +++----
.../fastmcp-server-auth-providers-descope.mdx | 25 ++-
src/fastmcp/server/auth/providers/descope.py | 105 ++++++++---
tests/server/auth/providers/test_descope.py | 169 ++++++++++++++----
4 files changed, 254 insertions(+), 105 deletions(-)
diff --git a/docs/integrations/descope.mdx b/docs/integrations/descope.mdx
index fdb8b0230..81eb0642b 100644
--- a/docs/integrations/descope.mdx
+++ b/docs/integrations/descope.mdx
@@ -6,7 +6,7 @@ icon: shield-check
tag: NEW
---
-import { VersionBadge } from "/snippets/version-badge.mdx"
+import { VersionBadge } from "/snippets/version-badge.mdx";
@@ -17,28 +17,29 @@ This guide shows you how to secure your FastMCP server using [**Descope**](https
### Prerequisites
Before you begin, you will need:
+
1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account
-2. Your **Project ID** from the [Descope Console](https://app.descope.com/settings/project)
-3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)
### Step 1: Configure Descope
-
- 1. Go to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope Console
- 2. Click **DCR Settings**
- 3. Enable **Dynamic Client Registration (DCR)**
- 4. Define allowed scopes
+
+ 1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.
+ 2. Give the MCP server a name and description.
+ 3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.
+ 4. Once you've created the MCP Server, note your Well-Known URL.
+
DCR is required for FastMCP clients to automatically register with your authentication server.
-
- Save your Project ID from [Project Settings](https://app.descope.com/settings/project):
+
+ Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):
```
- Project ID: P2abc...123
+ Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
```
@@ -48,15 +49,10 @@ Before you begin, you will need:
Create a `.env` file with your Descope configuration:
```bash
-DESCOPE_PROJECT_ID=P2abc...123 # Your Descope Project ID
-DESCOPE_BASE_URL=https://api.descope.com # Descope API URL
+DESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration # Your Descope Well-Known URL
SERVER_URL=http://localhost:3000 # Your server's base URL
```
-
-You can find your project's Descope Base URL in the [Multi-Region Support Guide](https://docs.descope.com/management/project-settings/multi-regional).
-
-
### Step 3: FastMCP Configuration
Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
@@ -68,9 +64,8 @@ from fastmcp.server.auth.providers.descope import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
auth_provider = DescopeProvider(
- project_id=DESCOPE_PROJECT_ID, # Your Descope Project ID
+ config_url=https://.../.well-known/openid-configuration, # Your MCP Server .well-known URL
base_url=SERVER_URL, # Your server's public URL
- descope_base_url=DESCOPE_BASE_URL, # Descope API base URL
)
# Create FastMCP server with auth
@@ -80,7 +75,7 @@ mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
## Testing
-To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the `project_id`, `base_url`, and `descope_base_url` with your actual values!), you can run the following command:
+To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:
```bash
fastmcp run server.py --transport http --port 8000
@@ -102,7 +97,6 @@ if __name__ == "__main__":
## Environment Variables
-
For production deployments, use environment variables instead of hardcoding credentials.
### Provider Selection
@@ -110,9 +104,10 @@ For production deployments, use environment variables instead of hardcoding cred
Setting this environment variable allows the Descope provider to be used automatically without explicitly instantiating it in code.
-
-Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use Descope authentication.
-
+
+ Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use
+ Descope authentication.
+
### Descope-Specific Configuration
@@ -120,28 +115,25 @@ Set to `fastmcp.server.auth.providers.descope.DescopeProvider` to use Descope au
These environment variables provide default values for the Descope provider, whether it's instantiated manually or configured via `FASTMCP_SERVER_AUTH`.
-
-Your Descope Project ID from the [Descope Console](https://app.descope.com/settings/project)
+
+Your Well-Known URL from the [Descope Console](https://app.descope.com/mcp-servers)
-Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)
-
-
-
-Descope API base URL for your [region/environment](https://docs.descope.com/management/project-settings/multi-regional)
+ Public URL of your FastMCP server (e.g., `https://your-server.com` or
+ `http://localhost:8000` for development)
Example `.env` file:
+
```bash
# Use the Descope provider
FASTMCP_SERVER_AUTH=fastmcp.server.auth.providers.descope.DescopeProvider
# Descope configuration
-FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_PROJECT_ID=P2abc...123
+FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL=https://your-server.com
-FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_DESCOPE_BASE_URL=https://api.descope.com
```
With environment variables set, your server code simplifies to:
@@ -151,4 +143,4 @@ from fastmcp import FastMCP
# Authentication is automatically configured from environment
mcp = FastMCP(name="My Descope Protected Server")
-```
\ No newline at end of file
+```
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx b/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
index 9d14c4964..8b282c398 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
@@ -5,21 +5,18 @@ sidebarTitle: descope
# `fastmcp.server.auth.providers.descope`
-
Descope authentication provider for FastMCP.
This module provides DescopeProvider - a complete authentication solution that integrates
with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
for seamless MCP client authentication.
-
## Classes
### `DescopeProviderSettings`
### `DescopeProvider`
-
Descope metadata provider for DCR (Dynamic Client Registration).
This provider implements Descope integration using metadata forwarding.
@@ -29,20 +26,20 @@ as a resource server.
IMPORTANT SETUP REQUIREMENTS:
-1. Enable Dynamic Client Registration in Descope Console:
- - Go to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope Console
- - Click **DCR Settings**
- - Enable **Dynamic Client Registration (DCR)**
- - Define allowed scopes
+1. Create an MCP Server in Descope Console:
-2. Note your Project ID:
- - Save your Project ID from [Project Settings](https://app.descope.com/settings/project)
- - Example: P2abc...123
+ - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
+ - Create a new MCP Server
+ - Ensure that **Dynamic Client Registration (DCR)** is enabled
+ - Note your Well-Known URL
+
+2. Note your Well-Known URL:
+ - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
+ - Format: `https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration`
For detailed setup instructions, see:
https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
-
**Methods:**
#### `get_routes`
@@ -57,6 +54,6 @@ This returns the standard protected resource routes plus an authorization server
metadata endpoint that forwards Descope's OAuth metadata to clients.
**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
+- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
+ This is used to advertise the resource URL in metadata.
diff --git a/src/fastmcp/server/auth/providers/descope.py b/src/fastmcp/server/auth/providers/descope.py
index 9e89339aa..610acf429 100644
--- a/src/fastmcp/server/auth/providers/descope.py
+++ b/src/fastmcp/server/auth/providers/descope.py
@@ -7,8 +7,10 @@ for seamless MCP client authentication.
from __future__ import annotations
+from urllib.parse import urlparse
+
import httpx
-from pydantic import AnyHttpUrl
+from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from starlette.responses import JSONResponse
from starlette.routing import Route
@@ -16,6 +18,7 @@ from starlette.routing import Route
from fastmcp.server.auth import RemoteAuthProvider, TokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier
from fastmcp.settings import ENV_FILE
+from fastmcp.utilities.auth import parse_scopes
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import NotSet, NotSetT
@@ -29,9 +32,16 @@ class DescopeProviderSettings(BaseSettings):
extra="ignore",
)
- project_id: str
+ config_url: AnyHttpUrl | None = None
+ project_id: str | None = None
+ descope_base_url: AnyHttpUrl | str | None = None
base_url: AnyHttpUrl
- descope_base_url: AnyHttpUrl = AnyHttpUrl("https://api.descope.com")
+ required_scopes: list[str] | None = None
+
+ @field_validator("required_scopes", mode="before")
+ @classmethod
+ def _parse_scopes(cls, v):
+ return parse_scopes(v)
class DescopeProvider(RemoteAuthProvider):
@@ -44,15 +54,15 @@ class DescopeProvider(RemoteAuthProvider):
IMPORTANT SETUP REQUIREMENTS:
- 1. Enable Dynamic Client Registration in Descope Console:
- - Go to the [Inbound Apps page](https://app.descope.com/apps/inbound) of the Descope Console
- - Click **DCR Settings**
- - Enable **Dynamic Client Registration (DCR)**
- - Define allowed scopes
+ 1. Create an MCP Server in Descope Console:
+ - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
+ - Create a new MCP Server
+ - Ensure that **Dynamic Client Registration (DCR)** is enabled
+ - Note your Well-Known URL
- 2. Note your Project ID:
- - Save your Project ID from [Project Settings](https://app.descope.com/settings/project)
- - Example: P2abc...123
+ 2. Note your Well-Known URL:
+ - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
+ - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
For detailed setup instructions, see:
https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
@@ -63,9 +73,8 @@ class DescopeProvider(RemoteAuthProvider):
# Create Descope metadata provider (JWT verifier created automatically)
descope_auth = DescopeProvider(
- project_id="P2abc...123",
+ config_url="https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration",
base_url="https://your-fastmcp-server.com",
- descope_base_url="https://api.descope.com",
)
# Use with FastMCP
@@ -76,50 +85,100 @@ class DescopeProvider(RemoteAuthProvider):
def __init__(
self,
*,
+ config_url: AnyHttpUrl | str | NotSetT = NotSet,
project_id: str | NotSetT = NotSet,
- base_url: AnyHttpUrl | str | NotSetT = NotSet,
descope_base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ base_url: AnyHttpUrl | str | NotSetT = NotSet,
+ required_scopes: list[str] | NotSetT | None = NotSet,
token_verifier: TokenVerifier | None = None,
):
"""Initialize Descope metadata provider.
Args:
- project_id: Your Descope Project ID (e.g., "P2abc...123")
+ config_url: Your Descope Well-Known URL (e.g., "https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration")
+ This is the new recommended way. If provided, project_id and descope_base_url are ignored.
+ project_id: Your Descope Project ID (e.g., "P2abc123"). Used with descope_base_url for backwards compatibility.
+ descope_base_url: Your Descope base URL (e.g., "https://api.descope.com"). Used with project_id for backwards compatibility.
base_url: Public URL of this FastMCP server
- descope_base_url: Descope API base URL (defaults to https://api.descope.com)
+ required_scopes: Optional list of scopes that must be present in validated tokens.
+ These scopes will be included in the protected resource metadata.
token_verifier: Optional token verifier. If None, creates JWT verifier for Descope
"""
settings = DescopeProviderSettings.model_validate(
{
k: v
for k, v in {
+ "config_url": config_url,
"project_id": project_id,
- "base_url": base_url,
"descope_base_url": descope_base_url,
+ "base_url": base_url,
+ "required_scopes": required_scopes,
}.items()
if v is not NotSet
}
)
- self.project_id = settings.project_id
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
- self.descope_base_url = str(settings.descope_base_url).rstrip("/")
+
+ # Determine which API is being used
+ if settings.config_url is not None:
+ # New API: use config_url
+ # Strip /.well-known/openid-configuration from config_url if present
+ issuer_url = str(settings.config_url)
+ if issuer_url.endswith("/.well-known/openid-configuration"):
+ issuer_url = issuer_url[: -len("/.well-known/openid-configuration")]
+
+ # Parse the issuer URL to extract descope_base_url and project_id for other uses
+ parsed_url = urlparse(issuer_url)
+ path_parts = parsed_url.path.strip("/").split("/")
+
+ # Extract project_id from path (format: /v1/apps/agentic/P.../M...)
+ if "agentic" in path_parts:
+ agentic_index = path_parts.index("agentic")
+ if agentic_index + 1 < len(path_parts):
+ self.project_id = path_parts[agentic_index + 1]
+ else:
+ raise ValueError(
+ f"Could not extract project_id from config_url: {issuer_url}"
+ )
+ else:
+ raise ValueError(
+ f"Could not find 'agentic' in config_url path: {issuer_url}"
+ )
+
+ # Extract descope_base_url (scheme + netloc)
+ self.descope_base_url = f"{parsed_url.scheme}://{parsed_url.netloc}".rstrip(
+ "/"
+ )
+ elif settings.project_id is not None and settings.descope_base_url is not None:
+ # Old API: use project_id and descope_base_url
+ self.project_id = settings.project_id
+ descope_base_url_str = str(settings.descope_base_url).rstrip("/")
+ # Ensure descope_base_url has a scheme
+ if not descope_base_url_str.startswith(("http://", "https://")):
+ descope_base_url_str = f"https://{descope_base_url_str}"
+ self.descope_base_url = descope_base_url_str
+ # Old issuer format
+ issuer_url = f"{self.descope_base_url}/v1/apps/{self.project_id}"
+ else:
+ raise ValueError(
+ "Either config_url (new API) or both project_id and descope_base_url (old API) must be provided"
+ )
# Create default JWT verifier if none provided
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.descope_base_url}/{self.project_id}/.well-known/jwks.json",
- issuer=f"{self.descope_base_url}/v1/apps/{self.project_id}",
+ issuer=issuer_url,
algorithm="RS256",
audience=self.project_id,
+ required_scopes=settings.required_scopes,
)
# Initialize RemoteAuthProvider with Descope as the authorization server
super().__init__(
token_verifier=token_verifier,
- authorization_servers=[
- AnyHttpUrl(f"{self.descope_base_url}/v1/apps/{self.project_id}")
- ],
+ authorization_servers=[AnyHttpUrl(issuer_url)],
base_url=self.base_url,
)
diff --git a/tests/server/auth/providers/test_descope.py b/tests/server/auth/providers/test_descope.py
index eb6d63b47..7ddeeb0ee 100644
--- a/tests/server/auth/providers/test_descope.py
+++ b/tests/server/auth/providers/test_descope.py
@@ -18,30 +18,21 @@ class TestDescopeProvider:
def test_init_with_explicit_params(self):
"""Test DescopeProvider initialization with explicit parameters."""
provider = DescopeProvider(
- project_id="P2abc123",
+ config_url="https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration",
base_url="https://myserver.com",
- descope_base_url="https://api.descope.com",
)
assert provider.project_id == "P2abc123"
assert str(provider.base_url) == "https://myserver.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
- @pytest.mark.parametrize(
- "scopes_env",
- [
- "openid,email",
- '["openid", "email"]',
- ],
- )
- def test_init_with_env_vars(self, scopes_env):
+ def test_init_with_env_vars(self):
"""Test DescopeProvider initialization from environment variables."""
with patch.dict(
os.environ,
{
- "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_PROJECT_ID": "P2env123",
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL": "https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration",
"FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL": "https://envserver.com",
- "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_DESCOPE_BASE_URL": "https://api.descope.com",
},
):
provider = DescopeProvider()
@@ -50,11 +41,32 @@ class TestDescopeProvider:
assert str(provider.base_url) == "https://envserver.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
+ def test_init_with_old_env_vars(self):
+ """Test DescopeProvider initialization from old environment variables (backwards compatibility)."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_PROJECT_ID": "P2oldenv123",
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_DESCOPE_BASE_URL": "https://api.descope.com",
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL": "https://envserver.com",
+ },
+ ):
+ provider = DescopeProvider()
+
+ assert provider.project_id == "P2oldenv123"
+ assert str(provider.base_url) == "https://envserver.com/"
+ assert str(provider.descope_base_url) == "https://api.descope.com"
+ assert (
+ provider.token_verifier.issuer # type: ignore[attr-defined]
+ == "https://api.descope.com/v1/apps/P2oldenv123"
+ )
+
def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
# This test verifies that the provider can be created with environment variables
provider = DescopeProvider(
- project_id="P2env123", base_url="http://env-server.com"
+ config_url="https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration",
+ base_url="http://env-server.com",
)
# Should have loaded from environment
@@ -62,48 +74,101 @@ class TestDescopeProvider:
assert str(provider.base_url) == "http://env-server.com/"
assert str(provider.descope_base_url) == "https://api.descope.com"
- def test_descope_base_url_https_prefix_handling(self):
- """Test that descope_base_url handles missing https:// prefix."""
- # Without https:// - should add it
+ def test_config_url_parsing(self):
+ """Test that config_url is parsed correctly to extract base URL and project ID."""
+ # Standard HTTPS URL
provider1 = DescopeProvider(
- project_id="P2abc123",
+ config_url="https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration",
base_url="https://myserver.com",
- descope_base_url="https://api.descope.com",
)
assert str(provider1.descope_base_url) == "https://api.descope.com"
+ assert provider1.project_id == "P2abc123"
- # With https:// - should keep it
+ # HTTP URL (for local testing)
provider2 = DescopeProvider(
- project_id="P2abc123",
+ config_url="http://localhost:8080/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration",
base_url="https://myserver.com",
- descope_base_url="https://api.descope.com",
)
- assert str(provider2.descope_base_url) == "https://api.descope.com"
+ assert str(provider2.descope_base_url) == "http://localhost:8080"
+ assert provider2.project_id == "P2abc123"
- # With http:// - should be preserved
+ # URL without .well-known/openid-configuration suffix
provider3 = DescopeProvider(
- project_id="P2abc123",
+ config_url="https://api.descope.com/v1/apps/agentic/P2abc123/M123",
base_url="https://myserver.com",
- descope_base_url="http://localhost:8080",
)
- assert str(provider3.descope_base_url) == "http://localhost:8080"
+ assert str(provider3.descope_base_url) == "https://api.descope.com"
+ assert provider3.project_id == "P2abc123"
- def test_init_defaults(self):
- """Test that default values are applied correctly."""
+ def test_requires_config_url_or_project_id_and_descope_base_url(self):
+ """Test that either config_url or both project_id and descope_base_url are required."""
+ # Should raise error when neither API is provided
+ with pytest.raises(ValueError, match="Either config_url"):
+ DescopeProvider(
+ base_url="https://myserver.com",
+ )
+
+ def test_backwards_compatibility_with_project_id_and_descope_base_url(self):
+ """Test backwards compatibility with old API using project_id and descope_base_url."""
provider = DescopeProvider(
project_id="P2abc123",
+ descope_base_url="https://api.descope.com",
base_url="https://myserver.com",
)
- # Check defaults
+ assert provider.project_id == "P2abc123"
assert str(provider.descope_base_url) == "https://api.descope.com"
+ assert str(provider.base_url) == "https://myserver.com/"
+
+ # Check that JWT verifier uses the old issuer format
+ assert (
+ provider.token_verifier.issuer # type: ignore[attr-defined]
+ == "https://api.descope.com/v1/apps/P2abc123"
+ )
+ assert (
+ provider.token_verifier.jwks_uri # type: ignore[attr-defined]
+ == "https://api.descope.com/P2abc123/.well-known/jwks.json"
+ )
+
+ def test_backwards_compatibility_descope_base_url_without_scheme(self):
+ """Test that descope_base_url without scheme gets https:// prefix added."""
+ provider = DescopeProvider(
+ project_id="P2abc123",
+ descope_base_url="api.descope.com",
+ base_url="https://myserver.com",
+ )
+
+ assert str(provider.descope_base_url) == "https://api.descope.com"
+ assert (
+ provider.token_verifier.issuer # type: ignore[attr-defined]
+ == "https://api.descope.com/v1/apps/P2abc123"
+ )
+
+ def test_config_url_takes_precedence_over_old_api(self):
+ """Test that config_url takes precedence when both APIs are provided."""
+ provider = DescopeProvider(
+ config_url="https://api.descope.com/v1/apps/agentic/P2new123/M123/.well-known/openid-configuration",
+ project_id="P2old123", # Should be ignored
+ descope_base_url="https://old.descope.com", # Should be ignored
+ base_url="https://myserver.com",
+ )
+
+ # Should use values from config_url, not the old API
+ assert provider.project_id == "P2new123"
+ assert str(provider.descope_base_url) == "https://api.descope.com"
+ assert (
+ provider.token_verifier.issuer # type: ignore[attr-defined]
+ == "https://api.descope.com/v1/apps/agentic/P2new123/M123"
+ )
def test_jwt_verifier_configured_correctly(self):
"""Test that JWT verifier is configured correctly."""
+ config_url = "https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration"
+ issuer_url = "https://api.descope.com/v1/apps/agentic/P2abc123/M123"
+
provider = DescopeProvider(
- project_id="P2abc123",
+ config_url=config_url,
base_url="https://myserver.com",
- descope_base_url="https://api.descope.com",
)
# Check that JWT verifier uses the correct endpoints
@@ -112,19 +177,55 @@ class TestDescopeProvider:
== "https://api.descope.com/P2abc123/.well-known/jwks.json"
)
assert (
- provider.token_verifier.issuer == "https://api.descope.com/v1/apps/P2abc123" # type: ignore[attr-defined]
+ provider.token_verifier.issuer == issuer_url # type: ignore[attr-defined]
)
assert provider.token_verifier.audience == "P2abc123" # type: ignore[attr-defined]
+ def test_required_scopes_support(self):
+ """Test that required_scopes are supported and passed to JWT verifier."""
+ provider = DescopeProvider(
+ config_url="https://api.descope.com/v1/apps/agentic/P2abc123/M123/.well-known/openid-configuration",
+ base_url="https://myserver.com",
+ required_scopes=["read", "write"],
+ )
+
+ # Check that required_scopes are set on the token verifier
+ assert provider.token_verifier.required_scopes == ["read", "write"] # type: ignore[attr-defined]
+
+ def test_required_scopes_with_old_api(self):
+ """Test that required_scopes work with the old API (project_id + descope_base_url)."""
+ provider = DescopeProvider(
+ project_id="P2abc123",
+ descope_base_url="https://api.descope.com",
+ base_url="https://myserver.com",
+ required_scopes=["openid", "email"],
+ )
+
+ # Check that required_scopes are set on the token verifier
+ assert provider.token_verifier.required_scopes == ["openid", "email"] # type: ignore[attr-defined]
+
+ def test_required_scopes_from_env(self):
+ """Test that required_scopes can be set via environment variable."""
+ with patch.dict(
+ os.environ,
+ {
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_CONFIG_URL": "https://api.descope.com/v1/apps/agentic/P2env123/M123/.well-known/openid-configuration",
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_BASE_URL": "https://envserver.com",
+ "FASTMCP_SERVER_AUTH_DESCOPEPROVIDER_REQUIRED_SCOPES": "read,write",
+ },
+ ):
+ provider = DescopeProvider()
+
+ assert provider.token_verifier.required_scopes == ["read", "write"] # type: ignore[attr-defined]
+
@pytest.fixture
async def mcp_server_url():
"""Start Descope server."""
mcp = FastMCP(
auth=DescopeProvider(
- project_id="P2test123",
+ config_url="https://api.descope.com/v1/apps/agentic/P2test123/M123/.well-known/openid-configuration",
base_url="http://localhost:4321",
- descope_base_url="https://api.descope.com",
)
)