1010
1111import asyncio
1212import logging
13- import secrets
1413import time
1514
1615import click
17- import httpx
1816from pydantic import AnyHttpUrl
19- from pydantic_settings import BaseSettings , SettingsConfigDict
17+ from pydantic_settings import SettingsConfigDict
2018from starlette .applications import Starlette
2119from starlette .exceptions import HTTPException
2220from starlette .requests import Request
23- from starlette .responses import JSONResponse , Response
21+ from starlette .responses import JSONResponse , RedirectResponse , Response
2422from starlette .routing import Route
2523from 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- )
3525from mcp .server .auth .routes import cors_middleware , create_auth_routes
3626from 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
4030logger = 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
31663def 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