Skip to content

Commit a8c99eb

Browse files
committed
refactor legacy AS+MCP and AS examples
1 parent a565625 commit a8c99eb

File tree

3 files changed

+293
-517
lines changed

3 files changed

+293
-517
lines changed

examples/servers/simple-auth/mcp_simple_auth/auth_server.py

Lines changed: 29 additions & 297 deletions
Original file line numberDiff line numberDiff line change
@@ -10,37 +10,27 @@
1010

1111
import asyncio
1212
import logging
13-
import secrets
1413
import time
1514

1615
import click
17-
import httpx
1816
from pydantic import AnyHttpUrl
19-
from pydantic_settings import BaseSettings, SettingsConfigDict
17+
from pydantic_settings import SettingsConfigDict
2018
from starlette.applications import Starlette
2119
from starlette.exceptions import HTTPException
2220
from starlette.requests import Request
23-
from starlette.responses import JSONResponse, Response
21+
from starlette.responses import JSONResponse, RedirectResponse, Response
2422
from starlette.routing import Route
2523
from uvicorn import Config, Server
2624

27-
from mcp.server.auth.provider import (
28-
AccessToken,
29-
AuthorizationCode,
30-
AuthorizationParams,
31-
OAuthAuthorizationServerProvider,
32-
RefreshToken,
33-
construct_redirect_uri,
34-
)
3525
from mcp.server.auth.routes import cors_middleware, create_auth_routes
3626
from mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions
37-
from mcp.shared._httpx_utils import create_mcp_http_client
38-
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
27+
28+
from .github_oauth_provider import GitHubOAuthProvider, GitHubOAuthSettings
3929

4030
logger = logging.getLogger(__name__)
4131

4232

43-
class AuthServerSettings(BaseSettings):
33+
class AuthServerSettings(GitHubOAuthSettings):
4434
"""Settings for the Authorization Server."""
4535

4636
model_config = SettingsConfigDict(env_prefix="MCP_")
@@ -49,25 +39,14 @@ class AuthServerSettings(BaseSettings):
4939
host: str = "localhost"
5040
port: int = 9000
5141
server_url: AnyHttpUrl = AnyHttpUrl("http://localhost:9000")
52-
53-
# GitHub OAuth settings - MUST be provided via environment variables
54-
github_client_id: str # Type: MCP_GITHUB_CLIENT_ID env var
55-
github_client_secret: str # Type: MCP_GITHUB_CLIENT_SECRET env var
5642
github_callback_path: str = "http://localhost:9000/github/callback"
5743

58-
# GitHub OAuth URLs
59-
github_auth_url: str = "https://github.com/login/oauth/authorize"
60-
github_token_url: str = "https://github.com/login/oauth/access_token"
61-
62-
mcp_scope: str = "user"
63-
github_scope: str = "read:user"
64-
6544
def __init__(self, **data):
6645
"""Initialize settings with values from environment variables."""
6746
super().__init__(**data)
6847

6948

70-
class GitHubProxyAuthProvider(OAuthAuthorizationServerProvider):
49+
class GitHubProxyAuthProvider(GitHubOAuthProvider):
7150
"""
7251
Authorization Server provider that proxies GitHub OAuth.
7352
@@ -78,239 +57,7 @@ class GitHubProxyAuthProvider(OAuthAuthorizationServerProvider):
7857
"""
7958

8059
def __init__(self, settings: AuthServerSettings):
81-
self.settings = settings
82-
self.clients: dict[str, OAuthClientInformationFull] = {}
83-
self.auth_codes: dict[str, AuthorizationCode] = {}
84-
self.tokens: dict[str, AccessToken] = {}
85-
self.state_mapping: dict[str, dict[str, str]] = {}
86-
# Store GitHub tokens with MCP tokens using the format:
87-
# {"mcp_token": "github_token"}
88-
self.token_mapping: dict[str, str] = {}
89-
90-
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
91-
"""Get OAuth client information."""
92-
return self.clients.get(client_id)
93-
94-
async def register_client(self, client_info: OAuthClientInformationFull):
95-
"""Register a new OAuth client."""
96-
self.clients[client_info.client_id] = client_info
97-
98-
async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
99-
"""Generate an authorization URL for GitHub OAuth flow."""
100-
state = params.state or secrets.token_hex(16)
101-
102-
# Store the state mapping
103-
self.state_mapping[state] = {
104-
"redirect_uri": str(params.redirect_uri),
105-
"code_challenge": params.code_challenge,
106-
"redirect_uri_provided_explicitly": str(params.redirect_uri_provided_explicitly),
107-
"client_id": client.client_id,
108-
}
109-
110-
# Build GitHub authorization URL
111-
auth_url = (
112-
f"{self.settings.github_auth_url}"
113-
f"?client_id={self.settings.github_client_id}"
114-
f"&redirect_uri={self.settings.github_callback_path}"
115-
f"&scope={self.settings.github_scope}"
116-
f"&state={state}"
117-
)
118-
119-
return auth_url
120-
121-
async def handle_github_callback(self, code: str, state: str) -> str:
122-
"""Handle GitHub OAuth callback."""
123-
state_data = self.state_mapping.get(state)
124-
if not state_data:
125-
raise HTTPException(400, "Invalid state parameter")
126-
127-
redirect_uri = state_data["redirect_uri"]
128-
code_challenge = state_data["code_challenge"]
129-
redirect_uri_provided_explicitly = state_data["redirect_uri_provided_explicitly"] == "True"
130-
client_id = state_data["client_id"]
131-
132-
# Exchange code for token with GitHub
133-
async with create_mcp_http_client() as client:
134-
response = await client.post(
135-
self.settings.github_token_url,
136-
data={
137-
"client_id": self.settings.github_client_id,
138-
"client_secret": self.settings.github_client_secret,
139-
"code": code,
140-
"redirect_uri": self.settings.github_callback_path,
141-
},
142-
headers={"Accept": "application/json"},
143-
)
144-
145-
if response.status_code != 200:
146-
raise HTTPException(400, "Failed to exchange code for token")
147-
148-
data = response.json()
149-
150-
if "error" in data:
151-
raise HTTPException(400, data.get("error_description", data["error"]))
152-
153-
github_token = data["access_token"]
154-
155-
# Create MCP authorization code
156-
new_code = f"mcp_{secrets.token_hex(16)}"
157-
auth_code = AuthorizationCode(
158-
code=new_code,
159-
client_id=client_id,
160-
redirect_uri=AnyHttpUrl(redirect_uri),
161-
redirect_uri_provided_explicitly=redirect_uri_provided_explicitly,
162-
expires_at=time.time() + 300,
163-
scopes=[self.settings.mcp_scope],
164-
code_challenge=code_challenge,
165-
)
166-
self.auth_codes[new_code] = auth_code
167-
168-
# Store GitHub token with client_id for later mapping
169-
# IMPORTANT: Store with MCP client_id, not GitHub client_id
170-
self.tokens[github_token] = AccessToken(
171-
token=github_token,
172-
client_id=client_id, # This is the MCP client_id from state mapping
173-
scopes=[self.settings.github_scope],
174-
expires_at=None,
175-
)
176-
logger.info(f"🔑 Stored GitHub token {github_token[:10]}... for MCP client {client_id}")
177-
178-
del self.state_mapping[state]
179-
final_redirect = construct_redirect_uri(redirect_uri, code=new_code, state=state)
180-
logger.info(f"🔗 Final redirect URI: {final_redirect}")
181-
logger.info(" Expected callback: http://localhost:3000/callback")
182-
logger.info(" Redirect URI components:")
183-
logger.info(f" - redirect_uri: {redirect_uri}")
184-
logger.info(f" - new_code: {new_code}")
185-
logger.info(f" - state: {state}")
186-
# Debug: Verify that the redirect URI looks correct
187-
if not final_redirect.startswith("http://localhost:3000/callback"):
188-
logger.warning("⚠️ POTENTIAL ISSUE: Final redirect URI doesn't start with expected callback base!")
189-
logger.warning(" Expected: http://localhost:3000/callback?...")
190-
logger.warning(f" Actual: {final_redirect}")
191-
else:
192-
logger.info("✅ Redirect URI format looks correct")
193-
logger.info("🚀 About to return final_redirect to GitHub callback handler")
194-
return final_redirect
195-
196-
async def load_authorization_code(
197-
self, client: OAuthClientInformationFull, authorization_code: str
198-
) -> AuthorizationCode | None:
199-
"""Load an authorization code."""
200-
auth_code_obj = self.auth_codes.get(authorization_code)
201-
if auth_code_obj:
202-
logger.info("🔍 LOADED AUTH CODE FOR VALIDATION:")
203-
logger.info(f" - Code: {authorization_code}")
204-
logger.info(f" - Stored redirect_uri: {auth_code_obj.redirect_uri}")
205-
logger.info(f" - Client ID: {auth_code_obj.client_id}")
206-
logger.info(f" - Redirect URI provided explicitly: {auth_code_obj.redirect_uri_provided_explicitly}")
207-
else:
208-
logger.warning(f"❌ AUTH CODE NOT FOUND: {authorization_code}")
209-
logger.warning(f" Available codes: {list(self.auth_codes.keys())}")
210-
return auth_code_obj
211-
212-
async def exchange_authorization_code(
213-
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
214-
) -> OAuthToken:
215-
"""Exchange authorization code for tokens."""
216-
logger.info("🔄 STARTING TOKEN EXCHANGE")
217-
logger.info(f" ✅ Code received: {authorization_code.code}")
218-
logger.info(f" ✅ Client ID: {client.client_id}")
219-
logger.info(f" 📊 Available codes in storage: {list(self.auth_codes.keys())}")
220-
logger.info(" 🔎 Code lookup in progress...")
221-
if authorization_code.code not in self.auth_codes:
222-
logger.error(f"❌ CRITICAL: Authorization code not found: {authorization_code.code}")
223-
logger.error(f" Available codes: {list(self.auth_codes.keys())}")
224-
logger.error(" This indicates the code was either:")
225-
logger.error(" 1. Already used and removed")
226-
logger.error(" 2. Never created (redirect flow failed)")
227-
logger.error(" 3. Expired and cleaned up")
228-
raise ValueError("Invalid authorization code")
229-
230-
# Generate MCP access token
231-
mcp_token = f"mcp_{secrets.token_hex(32)}"
232-
logger.info(f"🎫 Generated MCP access token: {mcp_token[:10]}...")
233-
234-
# Store MCP token
235-
self.tokens[mcp_token] = AccessToken(
236-
token=mcp_token,
237-
client_id=client.client_id,
238-
scopes=authorization_code.scopes,
239-
expires_at=int(time.time()) + 3600,
240-
)
241-
logger.info("💾 Stored MCP token in server memory")
242-
243-
# Find GitHub token for this client
244-
logger.info(f"🔍 Looking for GitHub token for client {client.client_id}")
245-
logger.info(f" Available tokens: {[(t[:10] + '...', d.client_id) for t, d in self.tokens.items()]}")
246-
247-
github_token = next(
248-
(
249-
token
250-
for token, data in self.tokens.items()
251-
# see https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
252-
# which you get depends on your GH app setup.
253-
if (token.startswith("ghu_") or token.startswith("gho_")) and data.client_id == client.client_id
254-
),
255-
None,
256-
)
257-
258-
if github_token:
259-
logger.info(f"✅ Found GitHub token {github_token[:10]}... for mapping")
260-
else:
261-
logger.warning("⚠️ No GitHub token found for client - user data access will be limited")
262-
263-
# Store mapping between MCP token and GitHub token
264-
if github_token:
265-
self.token_mapping[mcp_token] = github_token
266-
267-
logger.info(f"🧹 Cleaning up used authorization code: {authorization_code.code}")
268-
del self.auth_codes[authorization_code.code]
269-
logger.info("✅ Authorization code removed to prevent reuse")
270-
271-
token_response = OAuthToken(
272-
access_token=mcp_token,
273-
token_type="Bearer",
274-
expires_in=3600,
275-
scope=" ".join(authorization_code.scopes),
276-
)
277-
logger.info("🎉 TOKEN EXCHANGE COMPLETE!")
278-
logger.info(f" ✅ MCP access token: {mcp_token[:10]}...")
279-
logger.info(" ✅ Token type: Bearer")
280-
logger.info(" ✅ Expires in: 3600 seconds")
281-
logger.info(f" ✅ Scopes: {authorization_code.scopes}")
282-
return token_response
283-
284-
async def load_access_token(self, token: str) -> AccessToken | None:
285-
"""Load and validate an access token."""
286-
access_token = self.tokens.get(token)
287-
if not access_token:
288-
return None
289-
290-
# Check if expired
291-
if access_token.expires_at and access_token.expires_at < time.time():
292-
del self.tokens[token]
293-
return None
294-
295-
return access_token
296-
297-
async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
298-
"""Load a refresh token - not supported."""
299-
return None
300-
301-
async def exchange_refresh_token(
302-
self,
303-
client: OAuthClientInformationFull,
304-
refresh_token: RefreshToken,
305-
scopes: list[str],
306-
) -> OAuthToken:
307-
"""Exchange refresh token"""
308-
raise NotImplementedError("Not supported")
309-
310-
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
311-
"""Revoke a token."""
312-
if token in self.tokens:
313-
del self.tokens[token]
60+
super().__init__(settings, settings.github_callback_path)
31461

31562

31663
def create_authorization_server(settings: AuthServerSettings) -> Starlette:
@@ -348,12 +95,6 @@ async def github_callback_handler(request: Request) -> Response:
34895

34996
try:
35097
redirect_uri = await oauth_provider.handle_github_callback(code, state)
351-
logger.info(f"🔄 GitHub callback complete, redirecting to: {redirect_uri}")
352-
logger.info(" Redirect type: HTTP 302 (simple redirect)")
353-
354-
from starlette.responses import RedirectResponse
355-
356-
logger.info("🚀 Sending HTTP 302 redirect to client callback server...")
35798
return RedirectResponse(url=redirect_uri, status_code=302)
35899
except HTTPException:
359100
raise
@@ -428,25 +169,16 @@ async def github_user_handler(request: Request) -> Response:
428169

429170
mcp_token = auth_header[7:]
430171

431-
# Look up GitHub token for this MCP token
432-
github_token = oauth_provider.token_mapping.get(mcp_token)
433-
if not github_token:
434-
return JSONResponse({"error": "no_github_token"}, status_code=404)
435-
436-
# Call GitHub API with the stored GitHub token
437-
async with httpx.AsyncClient() as client:
438-
response = await client.get(
439-
"https://api.github.com/user",
440-
headers={
441-
"Authorization": f"Bearer {github_token}",
442-
"Accept": "application/vnd.github.v3+json",
443-
},
444-
)
445-
446-
if response.status_code != 200:
447-
return JSONResponse({"error": "github_api_error", "status": response.status_code}, status_code=502)
448-
449-
return JSONResponse(response.json())
172+
# Get GitHub user info using the provider method
173+
try:
174+
user_info = await oauth_provider.get_github_user_info(mcp_token)
175+
return JSONResponse(user_info)
176+
except ValueError as e:
177+
if "No GitHub token found" in str(e):
178+
return JSONResponse({"error": "no_github_token"}, status_code=404)
179+
elif "GitHub API error" in str(e):
180+
return JSONResponse({"error": "github_api_error"}, status_code=502)
181+
raise
450182

451183
except Exception as e:
452184
logger.exception("GitHub user info error")
@@ -476,20 +208,20 @@ async def run_server(settings: AuthServerSettings):
476208
server = Server(config)
477209

478210
logger.info("=" * 80)
479-
logger.info("🔑 MCP AUTHORIZATION SERVER")
211+
logger.info("MCP AUTHORIZATION SERVER")
480212
logger.info("=" * 80)
481-
logger.info(f"🌐 Server URL: {settings.server_url}")
482-
logger.info("📋 Endpoints:")
483-
logger.info(f" ┌─ OAuth Metadata: {settings.server_url}/.well-known/oauth-authorization-server")
484-
logger.info(f" ├─ Client Registration: {settings.server_url}/register")
485-
logger.info(f" ├─ Authorization: {settings.server_url}/authorize")
486-
logger.info(f" ├─ Token Exchange: {settings.server_url}/token")
487-
logger.info(f" ├─ Token Introspection: {settings.server_url}/introspect")
488-
logger.info(f" ├─ GitHub Callback: {settings.server_url}/github/callback")
489-
logger.info(f" └─ GitHub User Proxy: {settings.server_url}/github/user")
213+
logger.info(f"Server URL: {settings.server_url}")
214+
logger.info("Endpoints:")
215+
logger.info(f" - OAuth Metadata: {settings.server_url}/.well-known/oauth-authorization-server")
216+
logger.info(f" - Client Registration: {settings.server_url}/register")
217+
logger.info(f" - Authorization: {settings.server_url}/authorize")
218+
logger.info(f" - Token Exchange: {settings.server_url}/token")
219+
logger.info(f" - Token Introspection: {settings.server_url}/introspect")
220+
logger.info(f" - GitHub Callback: {settings.server_url}/github/callback")
221+
logger.info(f" - GitHub User Proxy: {settings.server_url}/github/user")
490222
logger.info("")
491-
logger.info("🔍 Resource Servers should use /introspect to validate tokens")
492-
logger.info("📱 Configure GitHub App callback URL: " + settings.github_callback_path)
223+
logger.info("Resource Servers should use /introspect to validate tokens")
224+
logger.info("Configure GitHub App callback URL: " + settings.github_callback_path)
493225
logger.info("=" * 80)
494226

495227
await server.serve()

0 commit comments

Comments
 (0)