This commit is contained in:
saidsurucu
2025-07-09 19:55:09 +03:00
parent e8b92e347c
commit 3ccb52e719
4 changed files with 328 additions and 82 deletions
+28 -9
View File
@@ -26,8 +26,8 @@ from mcp_server_main import app as mcp_server
# Import Stripe webhook router # Import Stripe webhook router
from stripe_webhook import router as stripe_router from stripe_webhook import router as stripe_router
# Import MCP Auth HTTP adapter # Import simplified MCP Auth HTTP adapter
from mcp_auth_http_adapter import router as mcp_auth_router from mcp_auth_http_simple import router as mcp_auth_router
# OAuth configuration from environment variables # OAuth configuration from environment variables
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com") CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
@@ -127,25 +127,44 @@ async def mcp_protocol_handler(request: Request):
content="Session terminated successfully" content="Session terminated successfully"
) )
# Optional: Validate Bearer JWT tokens for direct API access # REQUIRED: Validate Bearer JWT tokens for all MCP requests
auth_header = request.headers.get("Authorization") auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "): if not auth_header or not auth_header.startswith("Bearer "):
logger.error("Missing or invalid Authorization header")
raise HTTPException(
status_code=401,
detail="Missing or invalid Authorization header. Bearer token required."
)
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
try: try:
# Validate Clerk JWT token (simplified validation) # Validate Clerk JWT token (required)
from clerk_backend_api import Clerk from clerk_backend_api import Clerk
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
jwt_claims = clerk.jwt_templates.verify_token(token) jwt_claims = clerk.jwt_templates.verify_token(token)
user_id = jwt_claims.get("sub") user_id = jwt_claims.get("sub")
if user_id:
if not user_id:
logger.error("JWT token validation failed - no user_id in claims")
raise HTTPException(
status_code=401,
detail="Invalid token - no user_id in claims"
)
logger.info(f"Bearer JWT token validated for user: {user_id}") logger.info(f"Bearer JWT token validated for user: {user_id}")
# Add user info to request state # Add user info to request state
request.state.user_id = user_id request.state.user_id = user_id
request.state.token_scopes = jwt_claims.get("scopes", ["read", "search"]) request.state.token_scopes = jwt_claims.get("scopes", ["read", "search"])
except HTTPException:
# Re-raise HTTPException as-is
raise
except Exception as e: except Exception as e:
logger.warning(f"Bearer token validation failed: {str(e)}") logger.error(f"Bearer token validation failed: {str(e)}")
# Don't fail here - let MCP Auth Toolkit handle it raise HTTPException(
pass status_code=401,
detail=f"Token validation failed: {str(e)}"
)
# Forward the request to the mounted MCP app # Forward the request to the mounted MCP app
async def receive(): async def receive():
+40 -54
View File
@@ -316,70 +316,56 @@ async def token_endpoint(request: Request):
) )
try: try:
# Import here to avoid circular imports # OAuth token exchange - validate code and return Clerk JWT
from mcp_server_main import app as mcp_app # This supports proper OAuth flow while using Clerk JWT tokens
from mcp_auth_factory import get_oauth_provider
# Get OAuth provider if not code or not redirect_uri:
oauth_provider = get_oauth_provider(mcp_app) logger.error("Missing required parameters: code or redirect_uri")
if not oauth_provider:
raise HTTPException(status_code=500, detail="OAuth provider not configured")
# Extract session info from code
code_session = None
if code.startswith("clerk_"):
# Get the code mapping
code_session = oauth_provider.storage.get_session(f"code_{code}")
if code_session:
session_id = code_session.get("session_id")
else:
logger.error(f"Code mapping not found for: {code}")
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid authorization code"} content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
)
else:
session_id = code
session = oauth_provider.storage.get_session(session_id)
if not session:
logger.error(f"Session {session_id} not found for token exchange")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
) )
# Validate PKCE if present # Validate OAuth code with Clerk
if "pkce_challenge" in session and code_verifier: if CLERK_AVAILABLE:
# Validate PKCE challenge try:
if not oauth_provider.validate_pkce(code_verifier, session["pkce_challenge"]): clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
logger.error("PKCE challenge validation failed")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid code verifier"}
)
logger.info("PKCE validation successful")
else:
logger.info("No PKCE validation required")
# Create JWT token # In a real implementation, you'd validate the code with Clerk
access_token = oauth_provider._create_mcp_token( # For now, we'll assume the code is valid if it looks like a Clerk code
session["scopes"], if len(code) > 10: # Basic validation
session.get("clerk_token", ""), # Create a mock session with the code
session_id # In practice, this would be validated with Clerk's OAuth flow
)
# Clean up sessions
oauth_provider.storage.delete_session(session_id)
if code_session:
oauth_provider.storage.delete_session(f"code_{code}")
# Return Clerk JWT token format
# This should be the actual Clerk JWT token from the OAuth flow
return JSONResponse({ return JSONResponse({
"access_token": access_token, "access_token": "use_clerk_jwt_token_here",
"token_type": "Bearer", "token_type": "Bearer",
"expires_in": 3600, "expires_in": 3600,
"scope": " ".join(session["scopes"]) "scope": "yargi.read yargi.search",
"instructions": "Replace 'use_clerk_jwt_token_here' with actual Clerk JWT token from OAuth callback"
})
else:
logger.error(f"Invalid code format: {code}")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
)
except Exception as e:
logger.error(f"Clerk validation failed: {e}")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Authorization code validation failed"}
)
else:
logger.warning("Clerk SDK not available, using mock response")
return JSONResponse({
"access_token": "mock_jwt_token_for_development",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "yargi.read yargi.search"
}) })
except Exception as e: except Exception as e:
+246
View File
@@ -0,0 +1,246 @@
"""
Simplified MCP OAuth HTTP adapter - only Clerk JWT based authentication
"""
import os
import logging
from typing import Optional
from urllib.parse import urlencode, quote
from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
# Try to import Clerk SDK
try:
from clerk_backend_api import Clerk
CLERK_AVAILABLE = True
except ImportError:
CLERK_AVAILABLE = False
Clerk = None
logger = logging.getLogger(__name__)
router = APIRouter()
# OAuth configuration
BASE_URL = os.getenv("BASE_URL", "https://api.yargimcp.com")
CLERK_DOMAIN = os.getenv("CLERK_DOMAIN", "accounts.yargimcp.com")
@router.get("/.well-known/oauth-authorization-server")
async def get_oauth_metadata():
"""OAuth 2.0 Authorization Server Metadata (RFC 8414)"""
return JSONResponse({
"issuer": BASE_URL,
"authorization_endpoint": f"{BASE_URL}/auth/login",
"token_endpoint": f"{BASE_URL}/auth/callback",
"registration_endpoint": f"{BASE_URL}/auth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["read", "search", "openid", "profile", "email"],
"service_documentation": f"{BASE_URL}/mcp/"
})
@router.get("/auth/login")
async def oauth_authorize(
request: Request,
client_id: str = Query(...),
redirect_uri: str = Query(...),
response_type: str = Query("code"),
scope: Optional[str] = Query("read search"),
state: Optional[str] = Query(None),
code_challenge: Optional[str] = Query(None),
code_challenge_method: Optional[str] = Query(None)
):
"""OAuth 2.1 Authorization Endpoint - redirects to Clerk"""
logger.info(f"OAuth authorize request - client_id: {client_id}")
logger.info(f"Redirect URI: {redirect_uri}")
logger.info(f"State: {state}")
logger.info(f"PKCE Challenge: {bool(code_challenge)}")
try:
# Build callback URL with all necessary parameters
callback_url = f"{BASE_URL}/auth/callback"
callback_params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
"state": state or "",
"scope": scope or "read search"
}
# Add PKCE parameters if present
if code_challenge:
callback_params["code_challenge"] = code_challenge
callback_params["code_challenge_method"] = code_challenge_method or "S256"
# Encode callback URL as redirect_url for Clerk
callback_with_params = f"{callback_url}?{urlencode(callback_params)}"
# Build Clerk sign-in URL
clerk_params = {
"redirect_url": callback_with_params
}
clerk_signin_url = f"https://{CLERK_DOMAIN}/sign-in?{urlencode(clerk_params)}"
logger.info(f"Redirecting to Clerk: {clerk_signin_url}")
return RedirectResponse(url=clerk_signin_url)
except Exception as e:
logger.exception(f"Authorization failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/auth/callback")
async def oauth_callback(
request: Request,
client_id: str = Query(...),
redirect_uri: str = Query(...),
state: Optional[str] = Query(None),
scope: Optional[str] = Query("read search"),
code_challenge: Optional[str] = Query(None),
code_challenge_method: Optional[str] = Query(None),
clerk_token: Optional[str] = Query(None)
):
"""OAuth callback from Clerk - generates authorization code"""
logger.info(f"OAuth callback - client_id: {client_id}")
logger.info(f"Clerk token provided: {bool(clerk_token)}")
try:
# Validate user with Clerk
user_authenticated = False
user_id = None
if clerk_token and CLERK_AVAILABLE:
try:
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
jwt_claims = clerk.jwt_templates.verify_token(clerk_token)
user_id = jwt_claims.get("sub")
if user_id:
user_authenticated = True
logger.info(f"User authenticated via JWT - user_id: {user_id}")
except Exception as e:
logger.error(f"JWT validation failed: {e}")
# Fallback to cookie validation
if not user_authenticated:
clerk_session = request.cookies.get("__session")
if clerk_session:
user_authenticated = True
logger.info("User authenticated via cookie")
# Last resort - trust Clerk redirect
if not user_authenticated:
user_authenticated = True
logger.info("User authenticated via trusted redirect")
if not user_authenticated:
return JSONResponse(
status_code=401,
content={"error": "access_denied", "error_description": "User not authenticated"}
)
# Generate authorization code
auth_code = f"clerk_auth_{os.urandom(16).hex()}"
# Store code temporarily (in production, use proper storage)
# For simplicity, we'll include user info in the code itself
# Redirect back to client with authorization code
redirect_params = {
"code": auth_code,
"state": state or ""
}
final_redirect_url = f"{redirect_uri}?{urlencode(redirect_params)}"
logger.info(f"Redirecting back to client: {final_redirect_url}")
return RedirectResponse(url=final_redirect_url)
except Exception as e:
logger.exception(f"Callback processing failed: {e}")
return JSONResponse(
status_code=500,
content={"error": "server_error", "error_description": str(e)}
)
@router.post("/auth/register")
async def register_client(request: Request):
"""Dynamic Client Registration (RFC 7591)"""
data = await request.json()
logger.info(f"Client registration request: {data}")
# Simple dynamic registration - accept any client
client_id = f"mcp-client-{os.urandom(8).hex()}"
return JSONResponse({
"client_id": client_id,
"client_secret": None, # Public client
"redirect_uris": data.get("redirect_uris", []),
"grant_types": ["authorization_code"],
"response_types": ["code"],
"client_name": data.get("client_name", "MCP Client"),
"token_endpoint_auth_method": "none"
})
@router.post("/token")
async def token_endpoint(request: Request):
"""OAuth 2.1 Token Endpoint - exchanges code for Clerk JWT"""
# Parse form data
form_data = await request.form()
grant_type = form_data.get("grant_type")
code = form_data.get("code")
redirect_uri = form_data.get("redirect_uri")
client_id = form_data.get("client_id")
code_verifier = form_data.get("code_verifier")
logger.info(f"Token exchange - grant_type: {grant_type}")
logger.info(f"Code: {code[:20] if code else 'None'}...")
if grant_type != "authorization_code":
return JSONResponse(
status_code=400,
content={"error": "unsupported_grant_type"}
)
if not code or not redirect_uri:
return JSONResponse(
status_code=400,
content={"error": "invalid_request", "error_description": "Missing code or redirect_uri"}
)
try:
# Validate authorization code
if not code.startswith("clerk_auth_"):
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid authorization code"}
)
# In a real implementation, you would:
# 1. Validate the code against stored session
# 2. Extract user info from the session
# 3. Return the actual Clerk JWT token
# For now, return a placeholder response
return JSONResponse({
"access_token": "PLACEHOLDER_USE_ACTUAL_CLERK_JWT_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search",
"instructions": "Replace with actual Clerk JWT token from authentication flow"
})
except Exception as e:
logger.exception(f"Token exchange failed: {e}")
return JSONResponse(
status_code=500,
content={"error": "server_error", "error_description": str(e)}
)
+1 -6
View File
@@ -31,7 +31,7 @@ root_logger.addHandler(console_handler)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# --- Logging Configuration End --- # --- Logging Configuration End ---
from mcp_auth_factory import create_app # Removed mcp_auth_factory import to eliminate OAuth tools from MCP protocol
# --- Module Imports --- # --- Module Imports ---
from yargitay_mcp_module.client import YargitayOfficialApiClient from yargitay_mcp_module.client import YargitayOfficialApiClient
@@ -2837,11 +2837,6 @@ async def fetch(
raise raise
def main(): def main():
from mcp_auth_factory import enable_tool_authentication
# Enable authentication on all tools now that they're defined
enable_tool_authentication(app)
logger.info(f"Starting {app.name} server via main() function...") logger.info(f"Starting {app.name} server via main() function...")
logger.info(f"Logs will be written to: {LOG_FILE_PATH}") logger.info(f"Logs will be written to: {LOG_FILE_PATH}")
try: try: