1
1
from contextvars import ContextVar
2
- import logging
3
- from typing import Any , Callable , List , Literal , Optional , Union
4
-
2
+ from typing import Callable , List , Literal , Optional , Union
3
+ from typing_extensions import deprecated
4
+
5
+ from .auth .authorization_server_handler import (
6
+ AuthorizationServerHandler ,
7
+ AuthServerModeConfig ,
8
+ )
9
+ from .auth .mcp_auth_handler import MCPAuthHandler
10
+ from .auth .resource_server_handler import (
11
+ ResourceServerHandler ,
12
+ ResourceServerModeConfig ,
13
+ )
5
14
from .middleware .create_bearer_auth import BearerAuthConfig
6
- from .types import AuthInfo , VerifyAccessTokenFunction
7
- from .config import AuthServerConfig , ServerMetadataPaths
15
+ from .types import AuthInfo , ResourceServerConfig , VerifyAccessTokenFunction
16
+ from .config import AuthServerConfig
8
17
from .exceptions import MCPAuthAuthServerException , AuthServerExceptionCode
9
- from .utils import validate_server_config
10
18
from starlette .middleware .base import BaseHTTPMiddleware
11
- from starlette .responses import Response , JSONResponse
12
- from starlette .requests import Request
13
- from starlette .routing import Route
19
+ from starlette .routing import Router , Route
14
20
15
21
_context_var_name = "mcp_auth_context"
16
22
@@ -23,41 +29,47 @@ class MCPAuth:
23
29
See Also: https://mcp-auth.dev for more information about the library and its usage.
24
30
"""
25
31
26
- server : AuthServerConfig
27
- """
28
- The configuration for the remote authorization server.
29
- """
32
+ _handler : MCPAuthHandler
30
33
31
34
def __init__ (
32
35
self ,
33
- server : AuthServerConfig ,
36
+ server : Optional [AuthServerConfig ] = None ,
37
+ protected_resources : Optional [
38
+ Union [ResourceServerConfig , List [ResourceServerConfig ]]
39
+ ] = None ,
34
40
context_var : ContextVar [Optional [AuthInfo ]] = ContextVar (
35
41
_context_var_name , default = None
36
42
),
37
43
):
38
44
"""
39
- :param server: Configuration for the remote authorization server.
45
+ :param server: Configuration for the remote authorization server (deprecated).
46
+ :param protected_resources: Configuration for one or more protected resource servers.
40
47
:param context_var: Context variable to store the `AuthInfo` object for the current request.
41
48
By default, it will be created with the name "mcp_auth_context".
42
49
"""
43
50
44
- result = validate_server_config (server )
51
+ if server and protected_resources :
52
+ raise MCPAuthAuthServerException (
53
+ AuthServerExceptionCode .INVALID_SERVER_CONFIG ,
54
+ cause = {
55
+ "error_description" : "Either `server` or `protected_resources` must be provided, but not both."
56
+ },
57
+ )
45
58
46
- if not result .is_valid :
47
- logging .error (
48
- "The authorization server configuration is invalid:\n "
49
- f"{ result .errors } \n "
59
+ if server :
60
+ self ._handler = AuthorizationServerHandler (AuthServerModeConfig (server ))
61
+ elif protected_resources :
62
+ self ._handler = ResourceServerHandler (
63
+ ResourceServerModeConfig (protected_resources )
50
64
)
65
+ else :
51
66
raise MCPAuthAuthServerException (
52
- AuthServerExceptionCode .INVALID_SERVER_CONFIG , cause = result
67
+ AuthServerExceptionCode .INVALID_SERVER_CONFIG ,
68
+ cause = {
69
+ "error_description" : "Either `server` or `protected_resources` must be provided."
70
+ },
53
71
)
54
72
55
- if len (result .warnings ) > 0 :
56
- logging .warning ("The authorization server configuration has warnings:\n " )
57
- for warning in result .warnings :
58
- logging .warning (f"- { warning } " )
59
-
60
- self .server = server
61
73
self ._context_var = context_var
62
74
63
75
@property
@@ -72,64 +84,48 @@ def auth_info(self) -> Optional[AuthInfo]:
72
84
73
85
return self ._context_var .get ()
74
86
75
- def metadata_endpoint (self ) -> Callable [[Request ], Any ]:
87
+ @deprecated ("Use resource_metadata_router() instead for resource server mode" )
88
+ def metadata_route (self ) -> Route :
76
89
"""
77
- Returns a Starlette endpoint function that handles the OAuth 2.0 Authorization Metadata
78
- endpoint (`/.well-known/oauth-authorization-server`) with CORS support.
79
-
80
- Example:
81
- ```python
82
- from starlette.applications import Starlette
83
- from mcpauth import MCPAuth
84
- from mcpauth.config import ServerMetadataPaths
85
-
86
- mcp_auth = MCPAuth(server=your_server_config)
87
- app = Starlette(routes=[
88
- Route(
89
- ServerMetadataPaths.OAUTH.value,
90
- mcp_auth.metadata_endpoint(),
91
- methods=["GET", "OPTIONS"] # Ensure to handle both GET and OPTIONS methods
92
- )
93
- ])
94
- ```
90
+ Returns a router that handles the legacy OAuth 2.0 Authorization Server Metadata endpoint.
91
+
92
+ This method is deprecated and will be removed in a future version.
93
+ For resource server mode, use `resource_metadata_router()` instead to serve
94
+ the Protected Resource Metadata endpoints.
95
95
"""
96
+ if isinstance (self ._handler , ResourceServerHandler ):
97
+ raise MCPAuthAuthServerException (
98
+ AuthServerExceptionCode .INVALID_SERVER_CONFIG ,
99
+ cause = {
100
+ "error_description" : "`metadata_route` is not available in `resource server` mode. Use `resource_metadata_router()` instead."
101
+ },
102
+ )
96
103
97
- async def endpoint (request : Request ) -> Response :
98
- if request .method == "OPTIONS" :
99
- response = Response (status_code = 204 )
100
- else :
101
- server_config = self .server
102
- response = JSONResponse (
103
- server_config .metadata .model_dump (exclude_none = True ),
104
- status_code = 200 ,
105
- )
106
- response .headers ["Access-Control-Allow-Origin" ] = "*"
107
- response .headers ["Access-Control-Allow-Methods" ] = "GET, OPTIONS"
108
- response .headers ["Access-Control-Allow-Headers" ] = "*"
109
- return response
110
-
111
- return endpoint
104
+ oauth_metadata_route = self ._handler .create_metadata_route ().routes [0 ]
112
105
113
- def metadata_route ( self ) -> Route :
114
- """
115
- Returns a Starlette route that handles the OAuth 2.0 Authorization Metadata endpoint
116
- (`/.well-known/oauth-authorization-server`) with CORS support.
106
+ if not isinstance ( oauth_metadata_route , Route ) :
107
+ raise IndexError (
108
+ "No metadata endpoint route was created. Expected the authorization server metadata route to be present."
109
+ )
117
110
118
- Example:
119
- ```python
120
- from starlette.applications import Starlette
121
- from mcpauth import MCPAuth
111
+ return oauth_metadata_route
122
112
123
- mcp_auth = MCPAuth(server=your_server_config)
124
- app = Starlette(routes=[mcp_auth.metadata_route()])
125
- ```
113
+ def resource_metadata_router (self ) -> Router :
126
114
"""
115
+ Returns a router that serves the OAuth 2.0 Protected Resource Metadata endpoint
116
+ for all configured resources.
127
117
128
- return Route (
129
- ServerMetadataPaths .OAUTH .value ,
130
- self .metadata_endpoint (),
131
- methods = ["GET" , "OPTIONS" ],
132
- )
118
+ This is an alias for `metadata_route` and is the recommended method to use when
119
+ in "resource server" mode.
120
+ """
121
+ if isinstance (self ._handler , AuthorizationServerHandler ):
122
+ raise MCPAuthAuthServerException (
123
+ AuthServerExceptionCode .INVALID_SERVER_CONFIG ,
124
+ cause = {
125
+ "error_description" : "`resource_metadata_router` is not available in `authorization server` mode."
126
+ },
127
+ )
128
+ return self ._handler .create_metadata_route ()
133
129
134
130
def bearer_auth_middleware (
135
131
self ,
@@ -138,6 +134,7 @@ def bearer_auth_middleware(
138
134
required_scopes : Optional [List [str ]] = None ,
139
135
show_error_details : bool = False ,
140
136
leeway : float = 60 ,
137
+ resource : Optional [str ] = None ,
141
138
) -> type [BaseHTTPMiddleware ]:
142
139
"""
143
140
Creates a middleware that handles bearer token authentication.
@@ -150,38 +147,53 @@ def bearer_auth_middleware(
150
147
Defaults to `False`.
151
148
:param leeway: Optional leeway in seconds for JWT verification (`jwt.decode`). Defaults to
152
149
`60`. Not used if a custom function is provided.
150
+ :param resource: The identifier of the protected resource. Required when using `protected_resources`.
153
151
:return: A middleware class that can be used in a Starlette or FastAPI application.
154
152
"""
153
+ from .middleware .create_bearer_auth import create_bearer_auth
155
154
156
- metadata = self . server . metadata
157
- if isinstance ( mode_or_verify , str ) and mode_or_verify == "jwt" :
158
- from . utils import create_verify_jwt
155
+ issuer : Union [ str , Callable [[ str ], None ]]
156
+
157
+ resource_for_verifier : str
159
158
160
- if not metadata .jwks_uri :
159
+ if isinstance (self ._handler , ResourceServerHandler ):
160
+ if not resource :
161
161
raise MCPAuthAuthServerException (
162
- AuthServerExceptionCode .MISSING_JWKS_URI
162
+ AuthServerExceptionCode .INVALID_SERVER_CONFIG ,
163
+ cause = {
164
+ "error_description" : "A `resource` must be specified in the `bearer_auth_middleware` configuration when using a `protected_resources` configuration."
165
+ },
163
166
)
167
+ resource_for_verifier = resource
168
+ else : # AuthorizationServerHandler
169
+ # In the deprecated `authorization server` mode, `getTokenVerifier` does not utilize the
170
+ # `resource` parameter. Passing an empty string `''` is a straightforward approach that
171
+ # avoids over-engineering a solution for a legacy path.
172
+ resource_for_verifier = ""
164
173
165
- verify = create_verify_jwt (
166
- metadata . jwks_uri ,
167
- leeway = leeway ,
174
+ if isinstance ( mode_or_verify , str ) and mode_or_verify == "jwt" :
175
+ token_verifier = self . _handler . get_token_verifier (
176
+ resource = resource_for_verifier
168
177
)
178
+ verify = token_verifier .create_verify_jwt_function (leeway = leeway )
179
+ issuer = token_verifier .validate_jwt_issuer
169
180
elif callable (mode_or_verify ):
170
181
verify = mode_or_verify
182
+ # For custom verify functions, issuer validation should be handled by the custom logic
183
+ issuer = lambda _ : None # No-op function that accepts any issuer
171
184
else :
172
185
raise ValueError (
173
186
"mode_or_verify must be 'jwt' or a callable function that verifies tokens."
174
187
)
175
188
176
- from .middleware .create_bearer_auth import create_bearer_auth
177
-
178
189
return create_bearer_auth (
179
190
verify ,
180
191
config = BearerAuthConfig (
181
- issuer = metadata . issuer ,
192
+ issuer = issuer ,
182
193
audience = audience ,
183
194
required_scopes = required_scopes ,
184
195
show_error_details = show_error_details ,
196
+ resource = resource ,
185
197
),
186
198
context_var = self ._context_var ,
187
199
)
0 commit comments