diff --git a/asgi_app.py b/asgi_app.py index 72a84c2..18fd669 100644 --- a/asgi_app.py +++ b/asgi_app.py @@ -10,7 +10,6 @@ Usage: """ import os -import jwt import time import logging from datetime import datetime, timedelta @@ -33,7 +32,6 @@ from mcp_auth_http_adapter import router as mcp_auth_router # OAuth configuration from environment variables CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com") BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com") -JWT_SECRET = os.getenv("JWT_SECRET_KEY", "your-secret-key-here") # Setup logging logger = logging.getLogger(__name__) @@ -97,19 +95,63 @@ app.mount("/mcp-server", mcp_app) async def mcp_protocol_handler(request: Request): """Handle MCP protocol requests by forwarding to mounted app""" - # Optional: Validate Bearer JWT tokens for direct API access + # Optional: Validate Clerk Bearer JWT tokens for direct API access auth_header = request.headers.get("Authorization") if auth_header and auth_header.startswith("Bearer "): token = auth_header.split(" ")[1] try: - # Validate custom JWT token (for direct API access) - user_payload = validate_mcp_token(token) - logger.info(f"Bearer JWT token validated for user: {user_payload.get('user_id')}") - # Add user info to request state - request.state.user_id = user_payload["user_id"] - request.state.token_scopes = user_payload.get("scopes", ["read", "search"]) - except HTTPException as e: - logger.warning(f"Bearer token validation failed: {e.detail}") + # Validate Clerk JWT token + from clerk_backend_api import Clerk + clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) + + # Validate Clerk JWT token using authenticate_request + import httpx + from clerk_backend_api.security import authenticate_request + from clerk_backend_api.security.types import AuthenticateRequestOptions + + # Create a mock request with the Bearer token + mock_request = httpx.Request( + method="POST", + url="https://api.yargimcp.com/mock", + headers={"Authorization": f"Bearer {token}"} + ) + + request_state = clerk.authenticate_request( + mock_request, + AuthenticateRequestOptions( + authorized_parties=['https://api.yargimcp.com'] + ) + ) + + if request_state.is_signed_in: + # Get user info from token payload + payload = getattr(request_state, 'payload', {}) + + # Extract user information from JWT payload + user_email = payload.get('email') # Primary identifier + user_name = payload.get('name') + user_plan = payload.get('plan', 'free') # Default to free plan + scopes = payload.get('scopes', ['yargi.read']) + + # Use email as primary user identifier + user_id = user_email or request_state.user_id + + logger.info(f"Clerk JWT token validated successfully") + logger.info(f"User: {user_email}") + logger.info(f"Plan: {user_plan}") + logger.info(f"Scopes: {scopes}") + + # Add user info to request state + request.state.user_id = user_id + request.state.user_email = user_email + request.state.user_name = user_name + request.state.user_plan = user_plan + request.state.token_scopes = scopes + else: + logger.warning(f"Clerk JWT token validation failed. Reason: {getattr(request_state, 'reason', 'UNKNOWN')}") + user_id = None + except Exception as e: + logger.warning(f"Clerk Bearer token validation failed: {str(e)}") # Don't fail here - let MCP Auth Toolkit handle it pass @@ -156,19 +198,59 @@ async def mcp_protocol_handler(request: Request): async def sse_protocol_handler(request: Request): """Handle SSE MCP protocol requests by forwarding to mounted SSE app""" - # Optional: Validate Bearer JWT tokens for direct API access + # Optional: Validate Clerk Bearer JWT tokens for direct API access auth_header = request.headers.get("Authorization") if auth_header and auth_header.startswith("Bearer "): token = auth_header.split(" ")[1] try: - # Validate custom JWT token (for direct API access) - user_payload = validate_mcp_token(token) - logger.info(f"SSE Bearer JWT token validated for user: {user_payload.get('user_id')}") - # Add user info to request state - request.state.user_id = user_payload["user_id"] - request.state.token_scopes = user_payload.get("scopes", ["read", "search"]) - except HTTPException as e: - logger.warning(f"SSE Bearer token validation failed: {e.detail}") + # Validate Clerk JWT token + from clerk_backend_api import Clerk + clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) + + # Validate Clerk JWT token using authenticate_request + import httpx + from clerk_backend_api.security import authenticate_request + from clerk_backend_api.security.types import AuthenticateRequestOptions + + # Create a mock request with the Bearer token + mock_request = httpx.Request( + method="POST", + url="https://api.yargimcp.com/mock", + headers={"Authorization": f"Bearer {token}"} + ) + + request_state = clerk.authenticate_request( + mock_request, + AuthenticateRequestOptions( + authorized_parties=['https://api.yargimcp.com'] + ) + ) + + if request_state.is_signed_in: + # Get user info from token payload + payload = getattr(request_state, 'payload', {}) + + # Extract user information from JWT payload + user_email = payload.get('email') # Primary identifier + user_plan = payload.get('plan', 'free') + scopes = payload.get('scopes', ['yargi.read']) + + # Use email as primary user identifier + user_id = user_email or request_state.user_id + else: + user_id = None + + if user_id: + logger.info(f"SSE Clerk Bearer JWT token validated for user: {user_id}") + # Add user info to request state + request.state.user_id = user_id + request.state.user_email = user_email + request.state.user_plan = user_plan + request.state.token_scopes = scopes + else: + logger.warning("SSE Clerk JWT token validation failed - no user_id in claims") + except Exception as e: + logger.warning(f"SSE Clerk Bearer token validation failed: {str(e)}") # Don't fail here - let MCP Auth Toolkit handle it pass @@ -419,28 +501,8 @@ async def status(): "auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled" }) -# MCP Token Generation and Validation -def generate_mcp_token(user_id: str, expires_in: int = 3600) -> str: - """Generate MCP access token for authenticated user""" - payload = { - "user_id": user_id, - "iat": int(time.time()), - "exp": int(time.time()) + expires_in, - "iss": BASE_URL, - "aud": "mcp-client", - "scopes": ["read", "search"] - } - return jwt.encode(payload, JWT_SECRET, algorithm="HS256") - -def validate_mcp_token(token: str) -> dict: - """Validate MCP access token and return user info""" - try: - payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) - return payload - except jwt.ExpiredSignatureError: - raise HTTPException(status_code=401, detail="Token expired") - except jwt.InvalidTokenError: - raise HTTPException(status_code=401, detail="Invalid token") +# Note: JWT token validation is now handled entirely by Clerk +# All authentication flows use Clerk JWT tokens directly async def validate_clerk_session(request: Request, clerk_token: str = None) -> str: """Validate Clerk session from cookies or JWT token and return user_id""" @@ -498,9 +560,8 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)): user_id = await validate_clerk_session(request, clerk_token) logger.info(f"User authenticated successfully - user_id: {user_id}") - # Generate MCP token - mcp_token = generate_mcp_token(user_id) - logger.info("MCP token generated successfully") + # Use the Clerk JWT token directly (no need to generate custom token) + logger.info("User authenticated successfully via Clerk") # Return success response return HTMLResponse(f""" @@ -517,8 +578,8 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):

✅ MCP Connection Successful!

Your Yargı MCP integration is now active.

- Access Token:
- {mcp_token} + Authentication:
+ Use your Clerk JWT token directly with Bearer authentication

You can now close this window and return to your MCP client.