Skip to content

Commit fd60ed8

Browse files
authored
Merge pull request #4 from mcp-auth/gao-init-class
refactor: use sync functions for fetch server config
2 parents 8fb73f3 + b805aad commit fd60ed8

20 files changed

+671
-753
lines changed

mcpauth/__init__.py

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,98 @@
1-
from .exceptions import (
2-
MCPAuthException as MCPAuthException,
3-
MCPAuthConfigException as MCPAuthConfigException,
4-
AuthServerExceptionCode as AuthServerExceptionCode,
5-
MCPAuthAuthServerException as MCPAuthAuthServerException,
6-
BearerAuthExceptionCode as BearerAuthExceptionCode,
7-
MCPAuthBearerAuthExceptionDetails as MCPAuthBearerAuthExceptionDetails,
8-
MCPAuthBearerAuthException as MCPAuthBearerAuthException,
9-
MCPAuthJwtVerificationExceptionCode as MCPAuthJwtVerificationExceptionCode,
10-
MCPAuthJwtVerificationException as MCPAuthJwtVerificationException,
11-
)
1+
import logging
2+
from typing import Any, Literal, Union
3+
4+
from .middleware.create_bearer_auth import BaseBearerAuthConfig, BearerAuthConfig
5+
from .types import VerifyAccessTokenFunction
6+
from .config import MCPAuthConfig
7+
from .exceptions import MCPAuthAuthServerException, AuthServerExceptionCode
8+
from .utils import validate_server_config
9+
from starlette.middleware.base import BaseHTTPMiddleware
10+
from starlette.responses import JSONResponse
1211

1312

1413
class MCPAuth:
15-
def __init__(self):
16-
self.config = None
14+
"""
15+
The main class for the mcp-auth library, which provides methods for creating middleware
16+
functions for handling OAuth 2.0-related tasks and bearer token auth.
17+
18+
See Also: https://mcp-auth.dev for more information about the library and its usage.
19+
20+
:param config: An instance of `MCPAuthConfig` containing the server configuration.
21+
"""
22+
23+
def __init__(self, config: MCPAuthConfig):
24+
result = validate_server_config(config.server)
25+
26+
if not result.is_valid:
27+
logging.error(
28+
"The authorization server configuration is invalid:\n"
29+
f"{result.errors}\n"
30+
)
31+
raise MCPAuthAuthServerException(
32+
AuthServerExceptionCode.INVALID_SERVER_CONFIG, cause=result
33+
)
34+
35+
if len(result.warnings) > 0:
36+
logging.warning("The authorization server configuration has warnings:\n")
37+
for warning in result.warnings:
38+
logging.warning(f"- {warning}")
39+
40+
self.config = config
41+
42+
def metadata_response(self) -> JSONResponse:
43+
"""
44+
Returns a response containing the server metadata in JSON format with CORS support.
45+
"""
46+
server_config = self.config.server
47+
48+
response = JSONResponse(
49+
server_config.metadata.model_dump(exclude_none=True),
50+
status_code=200,
51+
)
52+
response.headers["Access-Control-Allow-Origin"] = "*"
53+
response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
54+
return response
55+
56+
def bearer_auth_middleware(
57+
self,
58+
mode_or_verify: Union[Literal["jwt"], VerifyAccessTokenFunction],
59+
config: BaseBearerAuthConfig = BaseBearerAuthConfig(),
60+
jwt_options: dict[str, Any] = {},
61+
) -> type[BaseHTTPMiddleware]:
62+
"""
63+
Creates a middleware that handles bearer token authentication.
64+
65+
:param mode_or_verify: If "jwt", uses built-in JWT verification; or a custom function that
66+
takes a string token and returns an `AuthInfo` object.
67+
:param config: Configuration for the Bearer auth handler, including audience, required
68+
scopes, etc.
69+
:param jwt_options: Optional dictionary of additional options for JWT verification
70+
(`jwt.decode`). Not used if a custom function is provided.
71+
:return: A middleware class that can be used in a Starlette or FastAPI application.
72+
"""
73+
74+
metadata = self.config.server.metadata
75+
if isinstance(mode_or_verify, str) and mode_or_verify == "jwt":
76+
from .utils import create_verify_jwt
77+
78+
if not metadata.jwks_uri:
79+
raise MCPAuthAuthServerException(
80+
AuthServerExceptionCode.MISSING_JWKS_URI
81+
)
82+
83+
verify = create_verify_jwt(
84+
metadata.jwks_uri,
85+
options=jwt_options,
86+
)
87+
elif callable(mode_or_verify):
88+
verify = mode_or_verify
89+
else:
90+
raise ValueError(
91+
"mode_or_verify must be 'jwt' or a callable function that verifies tokens."
92+
)
93+
94+
from .middleware.create_bearer_auth import create_bearer_auth
95+
96+
return create_bearer_auth(
97+
verify, BearerAuthConfig(issuer=metadata.issuer, **config.model_dump())
98+
)

mcpauth/exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def to_json(self, show_cause: bool = False) -> Record:
3636
"error_description": self.message,
3737
"cause": (
3838
(
39-
{k: v for k, v in self.cause.model_dump().items() if v is not None}
39+
self.cause.model_dump(exclude_none=True)
4040
if isinstance(self.cause, BaseModel)
4141
else str(self.cause)
4242
)

mcpauth/middleware/create_bearer_auth.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,39 @@
1818
from ..types import VerifyAccessTokenFunction, Record
1919

2020

21-
class BearerAuthConfig(BaseModel):
21+
class BaseBearerAuthConfig(BaseModel):
2222
"""
23-
Configuration for the Bearer auth handler.
24-
25-
Attributes:
26-
issuer: The expected issuer of the access token.
27-
audience: The expected audience of the access token.
28-
required_scopes: An array of required scopes that the access token must have.
29-
show_error_details: Whether to show detailed error information in the response.
23+
Base configuration for the Bearer auth handler.
3024
"""
3125

32-
issuer: str
3326
audience: Optional[str] = None
27+
"""
28+
The expected audience of the access token. If not provided, no audience check is performed.
29+
"""
30+
3431
required_scopes: Optional[List[str]] = None
32+
"""
33+
An array of required scopes that the access token must have. If not provided, no scope check is
34+
performed.
35+
"""
36+
3537
show_error_details: bool = False
38+
"""
39+
Whether to show detailed error information in the response. Defaults to False.
40+
If True, detailed error information will be included in the response body for debugging
41+
purposes.
42+
"""
43+
44+
45+
class BearerAuthConfig(BaseBearerAuthConfig):
46+
"""
47+
Configuration for the Bearer auth handler.
48+
"""
49+
50+
issuer: str
51+
"""
52+
The expected issuer of the access token. This should be a valid URL.
53+
"""
3654

3755

3856
def get_bearer_token_from_headers(headers: Headers) -> str:

mcpauth/models/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .auth_server import (
2+
AuthServerConfig as AuthServerConfig,
3+
AuthServerType as AuthServerType,
4+
)
5+
from .oauth import (
6+
AuthorizationServerMetadata as AuthorizationServerMetadata,
7+
)

mcpauth/utils/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from ._create_verify_jwt import create_verify_jwt as create_verify_jwt
2+
from ._fetch_server_config import (
3+
fetch_server_config as fetch_server_config,
4+
fetch_server_config_by_well_known_url as fetch_server_config_by_well_known_url,
5+
ServerMetadataPaths as ServerMetadataPaths,
6+
)
7+
from ._validate_server_config import (
8+
validate_server_config as validate_server_config,
9+
AuthServerConfigErrorCode as AuthServerConfigErrorCode,
10+
AuthServerConfigError as AuthServerConfigError,
11+
AuthServerConfigWarningCode as AuthServerConfigWarningCode,
12+
AuthServerConfigWarning as AuthServerConfigWarning,
13+
AuthServerConfigValidationResult as AuthServerConfigValidationResult,
14+
)

mcpauth/utils/fetch_server_config.py renamed to mcpauth/utils/_fetch_server_config.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from enum import Enum
22
from typing import Callable, Optional
33
from urllib.parse import urlparse, urlunparse
4-
import aiohttp
4+
import requests
55
import pydantic
66
from pathlib import Path
77

@@ -46,7 +46,7 @@ def get_oidc_well_known_url(issuer: str) -> str:
4646
return urlunparse(parsed._replace(path=new_path))
4747

4848

49-
async def fetch_server_config_by_well_known_url(
49+
def fetch_server_config_by_well_known_url(
5050
well_known_url: str,
5151
type: AuthServerType,
5252
transpile_data: Optional[Callable[[Record], Record]] = None,
@@ -69,14 +69,13 @@ async def fetch_server_config_by_well_known_url(
6969
"""
7070

7171
try:
72-
async with aiohttp.ClientSession() as session:
73-
async with session.get(well_known_url) as response:
74-
response.raise_for_status()
75-
json = await response.json()
76-
transpiled_data = transpile_data(json) if transpile_data else json
77-
return AuthServerConfig(
78-
metadata=AuthorizationServerMetadata(**transpiled_data), type=type
79-
)
72+
response = requests.get(well_known_url, timeout=10)
73+
response.raise_for_status()
74+
json = response.json()
75+
transpiled_data = transpile_data(json) if transpile_data else json
76+
return AuthServerConfig(
77+
metadata=AuthorizationServerMetadata(**transpiled_data), type=type
78+
)
8079
except pydantic.ValidationError as e:
8180
raise MCPAuthAuthServerException(
8281
AuthServerExceptionCode.INVALID_SERVER_METADATA,
@@ -90,7 +89,7 @@ async def fetch_server_config_by_well_known_url(
9089
) from e
9190

9291

93-
async def fetch_server_config(
92+
def fetch_server_config(
9493
issuer: str,
9594
type: AuthServerType,
9695
transpile_data: Optional[Callable[[Record], Record]] = None,
@@ -141,6 +140,4 @@ async def fetch_server_config(
141140
if type == AuthServerType.OAUTH
142141
else get_oidc_well_known_url(issuer)
143142
)
144-
return await fetch_server_config_by_well_known_url(
145-
well_known_url, type, transpile_data
146-
)
143+
return fetch_server_config_by_well_known_url(well_known_url, type, transpile_data)

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ keywords = [
1414
"openid-connect",
1515
]
1616
dependencies = [
17-
"aiohttp>=3.11.18",
1817
"pydantic>=2.11.3",
1918
"pyjwt[crypto]>=2.9.0",
19+
"requests>=2.32.3",
2020
"starlette>=0.46.2",
2121
]
2222

@@ -27,9 +27,14 @@ documentation = "https://mcp-auth.dev/docs"
2727

2828
[dependency-groups]
2929
dev = [
30-
"aresponses>=3.0.0",
3130
"black>=24.8.0",
3231
"pytest>=8.3.5",
3332
"pytest-asyncio>=0.26.0",
3433
"pytest-cov>=6.1.1",
34+
"responses>=0.25.7",
35+
"uvicorn>=0.34.2",
3536
]
37+
38+
[tool.coverage.run]
39+
branch = true
40+
source = ["mcpauth"]

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
pythonpath = .

0 commit comments

Comments
 (0)