-
Notifications
You must be signed in to change notification settings - Fork 913
/
Copy pathroutes.py
172 lines (148 loc) · 5.57 KB
/
routes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
from collections.abc import Callable
from typing import Any
from pydantic import AnyHttpUrl
from starlette.middleware.cors import CORSMiddleware
from starlette.routing import Route
from starlette.requests import Request
from starlette.responses import JSONResponse, Response
from mcp.server.auth.handlers.authorize import AuthorizationHandler
from mcp.server.auth.handlers.metadata import MetadataHandler
from mcp.server.auth.handlers.register import RegistrationHandler
from mcp.server.auth.handlers.revoke import RevocationHandler
from mcp.server.auth.handlers.token import TokenHandler
from mcp.server.auth.middleware.client_auth import ClientAuthenticator
from mcp.server.auth.provider import OAuthServerProvider
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
from mcp.shared.auth import OAuthMetadata
def validate_issuer_url(url: AnyHttpUrl):
"""
Validate that the issuer URL meets OAuth 2.0 requirements.
Args:
url: The issuer URL to validate
Raises:
ValueError: If the issuer URL is invalid
"""
# RFC 8414 requires HTTPS, but we allow localhost HTTP for testing
if (
url.scheme != "https"
and url.host != "localhost"
and not url.host.startswith("127.0.0.1")
):
raise ValueError("Issuer URL must be HTTPS")
# No fragments or query parameters allowed
if url.fragment:
raise ValueError("Issuer URL must not have a fragment")
if url.query:
raise ValueError("Issuer URL must not have a query string")
AUTHORIZATION_PATH = "/authorize"
TOKEN_PATH = "/token"
REGISTRATION_PATH = "/register"
REVOCATION_PATH = "/revoke"
def create_auth_routes(
provider: OAuthServerProvider[Any, Any, Any],
issuer_url: AnyHttpUrl,
service_documentation_url: AnyHttpUrl | None = None,
client_registration_options: ClientRegistrationOptions | None = None,
revocation_options: RevocationOptions | None = None,
) -> list[Route]:
validate_issuer_url(issuer_url)
client_registration_options = (
client_registration_options or ClientRegistrationOptions()
)
revocation_options = revocation_options or RevocationOptions()
metadata = build_metadata(
issuer_url,
service_documentation_url,
client_registration_options,
revocation_options,
)
client_authenticator = ClientAuthenticator(provider)
# Create routes
routes = [
Route(
"/.well-known/oauth-authorization-server",
endpoint=MetadataHandler(metadata).handle,
methods=["GET", "OPTIONS"],
),
Route(
AUTHORIZATION_PATH,
endpoint=AuthorizationHandler(provider).handle,
methods=["GET", "POST", "OPTIONS"],
),
Route(
TOKEN_PATH,
endpoint=TokenHandler(provider, client_authenticator).handle,
methods=["POST", "OPTIONS"],
),
]
if client_registration_options.enabled:
registration_handler = RegistrationHandler(
provider,
options=client_registration_options,
)
routes.append(
Route(
REGISTRATION_PATH,
endpoint=registration_handler.handle,
methods=["POST"],
)
)
if revocation_options.enabled:
revocation_handler = RevocationHandler(provider, client_authenticator)
routes.append(
Route(REVOCATION_PATH, endpoint=revocation_handler.handle, methods=["POST"])
)
return routes
def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl:
return AnyHttpUrl.build(
scheme=url.scheme,
username=url.username,
password=url.password,
host=url.host,
port=url.port,
path=path_mapper(url.path or ""),
query=url.query,
fragment=url.fragment,
)
def build_metadata(
issuer_url: AnyHttpUrl,
service_documentation_url: AnyHttpUrl | None,
client_registration_options: ClientRegistrationOptions,
revocation_options: RevocationOptions,
) -> OAuthMetadata:
authorization_url = modify_url_path(
issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/")
)
token_url = modify_url_path(
issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/")
)
# Create metadata
metadata = OAuthMetadata(
issuer=issuer_url,
authorization_endpoint=authorization_url,
token_endpoint=token_url,
scopes_supported=None,
response_types_supported=["code"],
response_modes_supported=None,
grant_types_supported=["authorization_code", "refresh_token"],
token_endpoint_auth_methods_supported=["client_secret_post"],
token_endpoint_auth_signing_alg_values_supported=None,
service_documentation=service_documentation_url,
ui_locales_supported=None,
op_policy_uri=None,
op_tos_uri=None,
introspection_endpoint=None,
code_challenge_methods_supported=["S256"],
)
# Add registration endpoint if supported
if client_registration_options.enabled:
metadata.registration_endpoint = modify_url_path(
issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/")
)
# Add revocation endpoint if supported
if revocation_options.enabled:
metadata.revocation_endpoint = modify_url_path(
issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/")
)
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]
return metadata