attempt to fix auth

This commit is contained in:
saidsurucu
2025-07-02 02:17:37 +03:00
parent 7e6819affa
commit cb6d6ee8df
7 changed files with 289 additions and 799 deletions
+6 -6
View File
@@ -23,12 +23,12 @@ from mcp_server_main import app as mcp_server
# Import Stripe webhook router
from stripe_webhook import router as stripe_router
# Import OAuth router
from oauth_router import router as oauth_router
# Import MCP Auth HTTP adapter
from mcp_auth_http_adapter import router as mcp_auth_router
# OAuth configuration from environment variables
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://artistic-swan-81.clerk.accounts.dev")
BASE_URL = os.getenv("BASE_URL", "https://yargi-mcp.fly.dev")
CLERK_ISSUER = os.getenv("CLERK_ISSUER", "https://accounts.yargimcp.com")
BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
# Configure CORS middleware
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
@@ -60,8 +60,8 @@ app = FastAPI(
# Add Stripe webhook router to FastAPI
app.include_router(stripe_router, prefix="/api")
# Add OAuth router to FastAPI
app.include_router(oauth_router)
# Add MCP Auth HTTP adapter to FastAPI (replaces old OAuth router)
app.include_router(mcp_auth_router)
# Custom 401 exception handler for MCP spec compliance
@app.exception_handler(401)
+7
View File
@@ -191,6 +191,13 @@ class OAuthProvider:
"scope": " ".join(session["scopes"]),
}
def validate_pkce(self, code_verifier: str, code_challenge: str) -> bool:
"""Validate PKCE code challenge (RFC 7636)"""
# S256 method
verifier_hash = hashlib.sha256(code_verifier.encode()).digest()
expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode().rstrip('=')
return expected_challenge == code_challenge
def _create_mcp_token(
self, scopes: list[str], upstream_token: str, session_id: str
) -> str:
+275
View File
@@ -0,0 +1,275 @@
"""
HTTP adapter for MCP Auth Toolkit OAuth endpoints
Exposes MCP OAuth tools as HTTP endpoints for Claude.ai integration
"""
import os
import logging
from typing import Optional
from urllib.parse import urlencode
from datetime import datetime
from fastapi import APIRouter, Request, Query, HTTPException
from fastapi.responses import RedirectResponse, JSONResponse
logger = logging.getLogger(__name__)
router = APIRouter()
# OAuth configuration
BASE_URL = os.getenv("BASE_URL", "https://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}/authorize",
"token_endpoint": f"{BASE_URL}/token",
"registration_endpoint": f"{BASE_URL}/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"scopes_supported": ["mcp:tools:read", "mcp:tools:write", "openid", "profile", "email"]
})
@router.get("/.well-known/oauth-protected-resource")
async def get_protected_resource_metadata():
"""OAuth Protected Resource Metadata (RFC 9728)"""
return JSONResponse({
"resource": BASE_URL,
"authorization_servers": [BASE_URL],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:tools:read", "mcp:tools:write"],
"resource_documentation": f"{BASE_URL}/docs"
})
@router.get("/authorize")
async def authorize_endpoint(
response_type: str = Query(...),
client_id: str = Query(...),
redirect_uri: str = Query(...),
code_challenge: str = Query(...),
code_challenge_method: str = Query("S256"),
state: Optional[str] = Query(None),
scope: Optional[str] = Query(None)
):
"""OAuth 2.1 Authorization Endpoint - Redirects to MCP Auth tool"""
logger.info(f"OAuth authorize request - client_id: {client_id}, redirect_uri: {redirect_uri}")
# Import here to avoid circular imports
try:
from mcp_server_main import app as mcp_app
from mcp_auth_factory import get_oauth_provider
# Get OAuth provider from MCP app
oauth_provider = get_oauth_provider(mcp_app)
if not oauth_provider:
logger.error("OAuth provider not available in MCP app")
raise HTTPException(status_code=500, detail="OAuth provider not configured")
# Generate authorization URL using MCP Auth Toolkit
auth_url, pkce = oauth_provider.generate_authorization_url(
redirect_uri=redirect_uri,
state=state,
scopes=scope.split(" ") if scope else None
)
logger.info(f"Generated auth URL: {auth_url[:100]}...")
# Redirect to Clerk OAuth
return RedirectResponse(url=auth_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(
code: Optional[str] = Query(None),
state: Optional[str] = Query(None),
error: Optional[str] = Query(None),
error_description: Optional[str] = Query(None)
):
"""Handle OAuth callback from Clerk"""
logger.info(f"OAuth callback - code: {code[:20] if code else 'None'}..., state: {state[:20] if state else 'None'}...")
if error:
logger.error(f"OAuth error: {error} - {error_description}")
return JSONResponse(
status_code=400,
content={"error": error, "error_description": error_description}
)
if not code or not state:
logger.error("Missing code or state in callback")
return JSONResponse(
status_code=400,
content={"error": "invalid_request", "error_description": "Missing code or state parameter"}
)
try:
# Import here to avoid circular imports
from mcp_server_main import app as mcp_app
from mcp_auth_factory import get_oauth_provider
# Get OAuth provider
oauth_provider = get_oauth_provider(mcp_app)
if not oauth_provider:
raise HTTPException(status_code=500, detail="OAuth provider not configured")
# Parse state to get original client state and session ID
try:
original_state, session_id = state.split(":", 1)
except ValueError:
logger.error(f"Invalid state format: {state}")
raise HTTPException(status_code=400, detail="Invalid state format")
# Get session data from storage
session = oauth_provider.storage.get_session(session_id)
if not session:
logger.error(f"Session {session_id} not found")
raise HTTPException(status_code=400, detail="Invalid session")
# Exchange code for token with Clerk
token_result = await oauth_provider.exchange_code_for_token(
code=code,
state=state,
redirect_uri=session["redirect_uri"]
)
# Build redirect URL back to Claude with authorization code
# The "code" here is our session ID that Claude will exchange for a token
redirect_params = {
"code": session_id,
"state": original_state
}
redirect_url = f"{session['redirect_uri']}?{urlencode(redirect_params)}"
logger.info(f"Redirecting back to Claude: {redirect_url}")
return RedirectResponse(url=redirect_url)
except Exception as e:
logger.exception(f"Callback processing failed: {e}")
# Try to redirect back with error
if session and "redirect_uri" in session:
error_params = {
"error": "server_error",
"error_description": str(e),
"state": original_state if 'original_state' in locals() else state
}
error_url = f"{session['redirect_uri']}?{urlencode(error_params)}"
return RedirectResponse(url=error_url)
else:
return JSONResponse(
status_code=500,
content={"error": "server_error", "error_description": str(e)}
)
@router.post("/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", "refresh_token"],
"response_types": ["code"],
"client_name": data.get("client_name", "MCP Client"),
"token_endpoint_auth_method": "none",
"client_id_issued_at": int(datetime.now().timestamp())
})
@router.post("/token")
async def token_endpoint(request: Request):
"""OAuth 2.1 Token Endpoint"""
# 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}, code: {code[:20] if code else 'None'}...")
if grant_type != "authorization_code":
return JSONResponse(
status_code=400,
content={"error": "unsupported_grant_type"}
)
try:
# Import here to avoid circular imports
from mcp_server_main import app as mcp_app
from mcp_auth_factory import get_oauth_provider
# Get OAuth provider
oauth_provider = get_oauth_provider(mcp_app)
if not oauth_provider:
raise HTTPException(status_code=500, detail="OAuth provider not configured")
# The "code" is actually our session ID
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 "pkce_verifier" in session:
# Session has the verifier stored, validate it matches
if code_verifier != session["pkce_verifier"]:
logger.error("PKCE verifier mismatch")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Invalid code verifier"}
)
else:
logger.warning("No PKCE verifier in session, skipping validation")
# Create JWT token
access_token = oauth_provider._create_mcp_token(
session["scopes"],
session.get("clerk_token", ""),
session_id
)
# Clean up session
oauth_provider.storage.delete_session(session_id)
return JSONResponse({
"access_token": access_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": " ".join(session["scopes"])
})
except Exception as e:
logger.exception(f"Token exchange failed: {e}")
return JSONResponse(
status_code=500,
content={"error": "server_error", "error_description": str(e)}
)
-48
View File
@@ -1,48 +0,0 @@
import os
from functools import lru_cache
from fastmcp import FastMCP
from fastmcp.server.auth import BearerAuthProvider
# Conditional import for OAuth middleware
try:
from oauth_middleware import ClerkOAuthMiddleware
OAUTH_AVAILABLE = True
except ImportError:
# OAuth middleware not available - will disable OAuth features
OAUTH_AVAILABLE = False
ClerkOAuthMiddleware = None
@lru_cache
def create_app() -> FastMCP:
"""Return a FastMCP instance; OAuth authentication when ENABLE_AUTH=true."""
# Base app configuration
app_config = {
"instructions": "MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay, Rekabet).",
"dependencies": ["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"]
}
enable_auth = os.getenv("ENABLE_AUTH", "false").lower() == "true"
if not enable_auth or not OAUTH_AVAILABLE:
# Development mode - no authentication
# Either auth is disabled OR OAuth dependencies not available
app = FastMCP(
name="Yargı MCP DEV",
**app_config
)
if enable_auth and not OAUTH_AVAILABLE:
print("Warning: OAuth authentication requested but dependencies not available.")
print("Install with: uv pip install .[saas]")
else:
# Production mode - OAuth authentication via middleware
app_config["instructions"] += " with OAuth authentication via Clerk."
app = FastMCP(
name="Yargı MCP PROD",
**app_config
)
# Add OAuth middleware instead of BearerAuthProvider
app.add_middleware(ClerkOAuthMiddleware())
return app
-245
View File
@@ -1,245 +0,0 @@
"""
OAuth Middleware for FastMCP Server
Handles Clerk OAuth token validation and user context
"""
import os
import logging
from typing import Optional, Dict, Any
from fastmcp.server.middleware import Middleware, MiddlewareContext
try:
from clerk_backend_api import Clerk, SDKError, authenticate_request, AuthenticateRequestOptions
CLERK_AVAILABLE = True
except ImportError:
# Clerk SDK not available - OAuth features will be disabled
CLERK_AVAILABLE = False
Clerk = None
SDKError = Exception
authenticate_request = None
AuthenticateRequestOptions = None
from mcp import McpError
from mcp.types import ErrorData
from starlette.responses import Response
logger = logging.getLogger(__name__)
class ClerkOAuthMiddleware(Middleware):
"""
Middleware that validates OAuth tokens via Clerk API and adds user context.
This middleware intercepts MCP requests over HTTP transport and validates
OAuth access tokens provided in the Authorization header.
"""
def __init__(self):
"""Initialize the middleware with Clerk client."""
self.enable_auth = os.getenv("ENABLE_AUTH", "false").lower() == "true"
self.clerk_secret = os.getenv("CLERK_SECRET_KEY")
# Check if Clerk SDK is available
if self.enable_auth and not CLERK_AVAILABLE:
raise ValueError("Clerk SDK not available. Install with: uv pip install .[saas]")
# Only require Clerk credentials if auth is enabled
if self.enable_auth and not self.clerk_secret:
raise ValueError("CLERK_SECRET_KEY environment variable is required when ENABLE_AUTH=true")
# Initialize Clerk client only if auth is enabled and available
self.clerk = None
if self.enable_auth and self.clerk_secret and CLERK_AVAILABLE:
self.clerk = Clerk(bearer_auth=self.clerk_secret)
async def on_request(self, context: MiddlewareContext, call_next):
"""
Validate OAuth token on every request.
For HTTP transport:
1. Extract OAuth token from Authorization header
2. Validate token with Clerk API
3. Add user info to context
4. Check user permissions/plan
"""
# Skip auth if disabled
if not self.enable_auth:
return await call_next(context)
# Check if this is an HTTP transport request
if not hasattr(context, 'fastmcp_context') or not context.fastmcp_context:
# Non-HTTP transport (e.g., stdio), skip auth
return await call_next(context)
# Try to get the request object from context
request = getattr(context.fastmcp_context, 'request', None)
if not request:
# No HTTP request object, likely stdio transport
return await call_next(context)
# Check for Authorization header (Clerk SDK will handle token extraction)
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Bearer '):
raise McpError(ErrorData(
code=-32001,
message="Missing or invalid Authorization header. Expected: Bearer <token>"
))
# Validate token and get user info using Clerk SDK
if not self.clerk:
raise McpError(ErrorData(
code=-32001,
message="Authentication service not available"
))
user_info = self._validate_oauth_token(request)
if not user_info:
raise McpError(ErrorData(
code=-32001,
message="Invalid or expired OAuth token"
))
# Add user info to context for downstream use
context.user_info = user_info
# Check user permissions/plan
if not self._check_user_permissions(user_info):
raise McpError(ErrorData(
code=-32002,
message="Insufficient permissions. Upgrade your plan for access."
))
logger.info(f"Authenticated user: {user_info.get('id')} ({user_info.get('email')})")
# Continue with the request
return await call_next(context)
def _validate_oauth_token(self, request) -> Optional[Dict[str, Any]]:
"""
Validate OAuth token using Clerk SDK's authenticate_request method.
For development tokens, decode directly.
Returns user info if token is valid, None otherwise.
"""
try:
# Check for development token first
auth_header = request.headers.get('Authorization', '')
if auth_header.startswith('Bearer dev_token_'):
return self._validate_dev_token(auth_header)
# Use Clerk SDK for production tokens
# Get the host for authorized parties
host = request.url.host if hasattr(request.url, 'host') else 'localhost'
# Use Clerk SDK's authenticate_request method
request_state = self.clerk.authenticate_request(
request,
AuthenticateRequestOptions(
# Accept both session tokens and OAuth tokens
accepts_token=['session', 'oauth_token'],
authorized_parties=[host, 'localhost', '127.0.0.1']
)
)
if request_state.is_signed_in and request_state.payload:
payload = request_state.payload
# Extract user information from JWT payload
return {
"id": payload.get("sub"), # Subject (user ID)
"email": payload.get("email"),
"first_name": payload.get("given_name"),
"last_name": payload.get("family_name"),
"metadata": payload.get("metadata", {}),
"plan": payload.get("metadata", {}).get("plan", "free"),
"session_id": payload.get("sid"), # Session ID
"org_id": payload.get("org_id"), # Organization ID (if any)
"org_role": payload.get("org_role"), # Organization role (if any)
"iat": payload.get("iat"), # Issued at
"exp": payload.get("exp"), # Expires at
}
else:
logger.warning(f"Token validation failed: {request_state.reason if hasattr(request_state, 'reason') else 'Unknown reason'}")
return None
except SDKError as e:
logger.error(f"Clerk SDK error validating token: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error validating OAuth token: {e}")
return None
def _validate_dev_token(self, auth_header: str) -> Optional[Dict[str, Any]]:
"""
Validate development token for testing purposes.
"""
try:
import json
import base64
import time
# Extract token data
token = auth_header.replace('Bearer dev_token_', '')
payload_json = base64.b64decode(token).decode()
payload = json.loads(payload_json)
# Check expiration
if payload.get('exp', 0) < time.time():
logger.warning("Development token expired")
return None
# Return user info
return {
"id": payload.get("sub"),
"email": payload.get("email"),
"first_name": payload.get("given_name"),
"last_name": payload.get("family_name"),
"metadata": payload.get("metadata", {}),
"plan": payload.get("metadata", {}).get("plan", "free"),
"session_id": payload.get("sid"),
"org_id": payload.get("org_id"),
"org_role": payload.get("org_role"),
"iat": payload.get("iat"),
"exp": payload.get("exp")
}
except Exception as e:
logger.error(f"Error validating development token: {e}")
return None
def _check_user_permissions(self, user_info: Dict[str, Any]) -> bool:
"""
Check if user has necessary permissions based on their plan.
This is where you can implement role-based access control.
"""
# Get user's plan from metadata
user_plan = user_info.get("plan", "free")
# For now, allow all authenticated users
# You can implement more sophisticated permission checks here
# For example:
# - Free users: limited to X requests per day
# - Pro users: full access
# - Enterprise: priority access + higher limits
return True # Allow all authenticated users for now
async def on_call_tool(self, context: MiddlewareContext, call_next):
"""
Additional validation for tool calls.
Can be used to implement tool-specific permissions.
"""
# Check if user has access to this specific tool
if hasattr(context, 'user_info'):
tool_name = context.message.name if hasattr(context.message, 'name') else None
user_plan = context.user_info.get('plan', 'free')
# Example: Restrict certain tools to paid users
premium_tools = ["advanced_analysis", "bulk_export"]
if tool_name in premium_tools and user_plan == 'free':
raise McpError(ErrorData(
code=-32002,
message=f"Tool '{tool_name}' requires a Pro plan or higher"
))
return await call_next(context)
-499
View File
@@ -1,499 +0,0 @@
"""
OAuth Authentication Router for FastAPI
Handles OAuth flow with Clerk and Google
"""
import os
import secrets
import logging
from typing import Optional
from datetime import datetime, timedelta
from urllib.parse import urlencode
from fastapi import APIRouter, Request, Response, HTTPException, Query
from fastapi.responses import RedirectResponse, JSONResponse
from starlette.responses import Response as StarletteResponse
try:
from clerk_backend_api import Clerk, SDKError, authenticate_request, AuthenticateRequestOptions
CLERK_AVAILABLE = True
except ImportError:
# Clerk SDK not available - OAuth features will be disabled
CLERK_AVAILABLE = False
Clerk = None
SDKError = Exception
authenticate_request = None
AuthenticateRequestOptions = None
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/auth")
# Initialize Clerk client conditionally
clerk_secret = os.getenv("CLERK_SECRET_KEY")
clerk_publishable = os.getenv("CLERK_PUBLISHABLE_KEY")
clerk_domain = os.getenv("CLERK_DOMAIN") # e.g., "artistic-swan-81"
clerk_issuer = os.getenv("CLERK_ISSUER", f"https://{clerk_domain}.accounts.dev" if clerk_domain else None)
base_url = os.getenv("BASE_URL", "https://yargi-mcp.fly.dev")
clerk_frontend_url = os.getenv("CLERK_FRONTEND_URL", "http://localhost:3000")
redirect_url = os.getenv("CLERK_OAUTH_REDIRECT_URL", f"{base_url}/auth/callback")
enable_auth = os.getenv("ENABLE_AUTH", "false").lower() == "true"
# Check if Clerk SDK is available when auth is enabled
if enable_auth and not CLERK_AVAILABLE:
raise ValueError("Clerk SDK not available. Install with: uv pip install .[saas]")
# Only require Clerk credentials if auth is enabled
if enable_auth and not clerk_secret:
raise ValueError("CLERK_SECRET_KEY environment variable is required when ENABLE_AUTH=true")
# Initialize Clerk client only if auth is enabled and available
clerk = None
if enable_auth and clerk_secret and CLERK_AVAILABLE:
clerk = Clerk(bearer_auth=clerk_secret)
@router.get("/login")
async def oauth_login(request: Request, redirect_uri: Optional[str] = None):
"""
Initiate OAuth login flow with Clerk.
This endpoint redirects to Clerk's OAuth authorization URL.
After user authorizes, they'll be redirected back to /auth/callback
"""
# Store the original redirect URI in session/state
state = secrets.token_urlsafe(32)
# Build Clerk OAuth URL
# Note: Clerk handles the OAuth flow internally, we just need to redirect to Clerk's sign-in
final_redirect = redirect_uri or redirect_url
# For ChatGPT, ensure the redirect URL is properly encoded
if "chatgpt.com" in (final_redirect or ""):
final_redirect = "https://chatgpt.com/connector_platform_oauth_redirect"
clerk_oauth_params = {
"redirect_url": final_redirect,
}
# For Clerk test environment, redirect to our own OAuth endpoint
# which will handle the Clerk OAuth flow properly
# Always use Clerk hosted OAuth (force production-style flow)
# Get domain from environment variable or extract from publishable key
if clerk_domain:
domain = clerk_domain
elif clerk_publishable:
# Extract domain from key format: pk_test_xxxxx or pk_live_xxxxx
key_parts = clerk_publishable.split('_')
if len(key_parts) >= 3:
domain = key_parts[2] # Domain is usually the third part
else:
# Fallback to environment variable if key parsing fails
domain = os.getenv("CLERK_DOMAIN", "localhost")
else:
# Use environment variable as fallback
domain = os.getenv("CLERK_DOMAIN", "localhost")
# Production Clerk hosted sign-in URL
# Check if domain already includes full URL or just subdomain
if domain.startswith('http'):
# Full URL provided
clerk_sign_in_url = f"{domain}/sign-in"
elif '.' in domain and not domain.endswith('.accounts.dev'):
# Custom domain like clerk.yargimcp.com
clerk_sign_in_url = f"https://{domain}/sign-in"
else:
# Standard Clerk subdomain
clerk_sign_in_url = f"https://{domain}.accounts.dev/sign-in"
oauth_url = f"{clerk_sign_in_url}?{urlencode(clerk_oauth_params)}"
logger.info(f"Redirecting to Clerk OAuth: {oauth_url}")
return RedirectResponse(url=oauth_url)
@router.get("/clerk-oauth")
async def clerk_oauth_handler(request: Request, redirect_url: Optional[str] = None):
"""
Handle Clerk OAuth flow for test environment.
This endpoint creates a mock OAuth flow that simulates Clerk's behavior
but works around the 404 issues in test environment.
"""
# For development/testing, create a simulated OAuth flow
# In production, this would integrate with Clerk's actual OAuth endpoints
# Generate a mock authorization code
auth_code = secrets.token_urlsafe(32)
state = secrets.token_urlsafe(16)
# For ChatGPT, redirect back with the authorization code
if redirect_url and "chatgpt.com" in redirect_url:
callback_url = f"{redirect_url}?code={auth_code}&state={state}"
return RedirectResponse(url=callback_url)
# For other clients, show a simple OAuth consent page
return JSONResponse({
"message": "OAuth Authorization Required",
"authorization_url": f"/auth/callback?code={auth_code}&state={state}",
"redirect_url": redirect_url or "http://localhost:3000",
"note": "This is a development OAuth flow. In production, use Clerk's hosted OAuth."
})
@router.get("/callback")
@router.post("/callback")
async def oauth_callback(
request: Request,
code: Optional[str] = Query(None),
state: Optional[str] = Query(None),
error: Optional[str] = Query(None),
error_description: Optional[str] = Query(None),
grant_type: Optional[str] = Query(None),
redirect_uri: Optional[str] = Query(None),
redirect_url: Optional[str] = Query(None)
):
"""
Handle OAuth callback from Clerk.
This endpoint receives the authorization code from Clerk
and exchanges it for an access token.
"""
# Handle POST requests with form data
if request.method == "POST":
try:
form_data = await request.form()
code = code or form_data.get("code")
grant_type = grant_type or form_data.get("grant_type")
redirect_uri = redirect_uri or form_data.get("redirect_uri")
state = state or form_data.get("state")
except Exception:
pass # Continue with query parameters
if error:
logger.error(f"OAuth error: {error} - {error_description}")
return JSONResponse(
status_code=400,
content={"error": error, "description": error_description}
)
if not code:
raise HTTPException(status_code=400, detail="Missing authorization code")
try:
# Handle Clerk hosted OAuth callback
# In production, this exchanges the authorization code with Clerk for session tokens
if enable_auth and clerk and CLERK_AVAILABLE:
# Production: Exchange code with Clerk for real session token
try:
# Use Clerk SDK to exchange authorization code for session
# This would typically involve calling Clerk's token exchange endpoint
logger.info(f"Exchanging authorization code with Clerk: {code}")
# For now, create a development token until Clerk SDK token exchange is implemented
import time
import json
import base64
jwt_payload = {
"sub": f"clerk_user_{secrets.token_urlsafe(8)}",
"iss": clerk_issuer,
"aud": base_url,
"iat": int(time.time()),
"exp": int(time.time()) + 3600, # 1 hour
"email": "user@example.com",
"given_name": "Clerk",
"family_name": "User",
"sid": f"clerk_session_{secrets.token_urlsafe(8)}",
"metadata": {"plan": "free", "oauth_provider": "clerk_hosted"}
}
token_data = base64.b64encode(json.dumps(jwt_payload).encode()).decode()
session_token = f"clerk_token_{token_data}"
except Exception as e:
logger.error(f"Clerk token exchange failed: {e}")
raise HTTPException(status_code=500, detail="OAuth token exchange failed")
else:
# Development mode: Create mock token
import time
import json
import base64
jwt_payload = {
"sub": "dev_user_123",
"iss": clerk_issuer,
"aud": base_url,
"iat": int(time.time()),
"exp": int(time.time()) + 3600, # 1 hour
"email": "dev@example.com",
"given_name": "Dev",
"family_name": "User",
"sid": "dev_session_123",
"metadata": {"plan": "free"}
}
token_data = base64.b64encode(json.dumps(jwt_payload).encode()).decode()
session_token = f"dev_token_{token_data}"
# Check if this is a token exchange request (POST with grant_type)
if request.method == "POST" and grant_type == "authorization_code":
# OAuth 2.1 token response
return JSONResponse(content={
"access_token": session_token,
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": f"refresh_{secrets.token_urlsafe(32)}",
"scope": "read search"
})
# Always redirect back to the original redirect URL if provided
original_redirect = redirect_uri or redirect_url
if original_redirect:
# For Claude.ai, redirect with access token
if "claude.ai" in original_redirect:
return RedirectResponse(
url=f"{original_redirect}?access_token={session_token}&token_type=Bearer"
)
else:
# For other clients, redirect with authorization code
return RedirectResponse(
url=f"{original_redirect}?code={code}&state={state or ''}"
)
# If no redirect URL provided, return JSON response with session token
response = JSONResponse(content={
"status": "success",
"message": "Authentication successful",
"session_token": session_token,
"redirect_url": clerk_frontend_url
})
# Optionally set as cookie
response.set_cookie(
key="mcp_session",
value=session_token,
httponly=True,
secure=True, # Use HTTPS in production
samesite="lax",
max_age=86400 # 24 hours
)
return response
except Exception as e:
logger.error(f"OAuth callback error: {e}")
raise HTTPException(status_code=500, detail="Authentication failed")
@router.post("/logout")
async def logout(request: Request, response: Response):
"""
Logout user by clearing session.
"""
# Clear session cookie
response.delete_cookie("mcp_session")
# If using Clerk session tokens, revoke them here
# You might also want to call Clerk's signOut endpoint
return JSONResponse(content={
"status": "success",
"message": "Logged out successfully"
})
@router.get("/user")
async def get_current_user(request: Request):
"""
Get current authenticated user information using Clerk SDK.
This endpoint validates the token and returns user info using authenticate_request.
"""
# Check if auth is disabled
if not enable_auth:
return JSONResponse(content={
"auth_disabled": True,
"message": "Authentication is disabled (ENABLE_AUTH=false)",
"id": "dev_user",
"email": "dev@example.com",
"authenticated": False,
"development_mode": True
})
# Check if Clerk is not initialized
if not clerk:
raise HTTPException(status_code=500, detail="Authentication service not available")
# Check for Authorization header
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
# Also check for session cookie as fallback
session_token = request.cookies.get("mcp_session")
if not session_token:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
# Use Clerk SDK to validate the request
host = request.url.host if hasattr(request.url, 'host') else 'localhost'
request_state = clerk.authenticate_request(
request,
AuthenticateRequestOptions(
accepts_token=['session', 'oauth_token'],
authorized_parties=[host, 'localhost', '127.0.0.1']
)
)
if not request_state.is_signed_in:
raise HTTPException(status_code=401, detail="Invalid or expired token")
# Extract user info from JWT payload
payload = request_state.payload
user_info = {
"id": payload.get("sub"),
"email": payload.get("email"),
"first_name": payload.get("given_name"),
"last_name": payload.get("family_name"),
"metadata": payload.get("metadata", {}),
"plan": payload.get("metadata", {}).get("plan", "free"),
"session_id": payload.get("sid"),
"org_id": payload.get("org_id"),
"org_role": payload.get("org_role"),
"authenticated": True,
"iat": payload.get("iat"),
"exp": payload.get("exp")
}
return JSONResponse(content=user_info)
except SDKError as e:
logger.error(f"Clerk SDK error: {e}")
raise HTTPException(status_code=401, detail="Authentication failed")
except Exception as e:
logger.error(f"Error fetching user info: {e}")
raise HTTPException(status_code=401, detail="Invalid session")
@router.get("/google/login")
async def google_oauth_login(request: Request):
"""
Initiate Google OAuth login through Clerk.
Clerk handles the OAuth provider connections,
so we redirect to Clerk's sign-in with Google specified.
"""
# Extract domain using same logic as main OAuth flow
if clerk_domain:
domain = clerk_domain
elif clerk_publishable:
key_parts = clerk_publishable.split('_')
if len(key_parts) >= 3:
domain = key_parts[2]
else:
# Fallback to environment variable if key parsing fails
domain = os.getenv("CLERK_DOMAIN", "localhost")
else:
# Use environment variable as fallback
domain = os.getenv("CLERK_DOMAIN", "localhost")
# Build Clerk sign-in URL with Google as the provider
# Check if domain already includes full URL or just subdomain
if domain.startswith('http'):
# Full URL provided
google_oauth_url = f"{domain}/sign-in#/?strategy=oauth_google"
elif '.' in domain and not domain.endswith('.accounts.dev'):
# Custom domain like clerk.yargimcp.com
google_oauth_url = f"https://{domain}/sign-in#/?strategy=oauth_google"
else:
# Standard Clerk subdomain
google_oauth_url = f"https://{domain}.accounts.dev/sign-in#/?strategy=oauth_google"
return RedirectResponse(url=google_oauth_url)
@router.post("/register")
async def dynamic_client_registration(request: Request):
"""
OAuth 2.0 Dynamic Client Registration (RFC 7591) - MCP Spec SHOULD support.
For development/testing, returns a static client configuration.
In production, this would integrate with Clerk's client management.
"""
try:
# In a real implementation, you would:
# 1. Validate the request
# 2. Register the client with Clerk
# 3. Return proper client credentials
# For now, return a development client configuration
return JSONResponse(content={
"client_id": "yargi-mcp-dynamic-client",
"client_secret": "dev-client-secret-123",
"client_id_issued_at": 1625097600,
"client_secret_expires_at": 0, # Never expires for development
"redirect_uris": [
"https://chatgpt.com/connector_platform_oauth_redirect",
"https://chatgpt.com/auth/callback",
"http://localhost:3000/auth/callback"
],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read search openid profile email",
"token_endpoint_auth_method": "client_secret_basic"
})
except Exception as e:
logger.error(f"Dynamic client registration error: {e}")
raise HTTPException(status_code=400, detail="Invalid client registration request")
@router.get("/session/validate")
async def validate_session(request: Request):
"""
Validate if the current session is active using Clerk SDK.
Used by clients to check auth status.
"""
auth_header = request.headers.get("Authorization", "")
session_token = request.cookies.get("mcp_session")
if not auth_header.startswith("Bearer ") and not session_token:
return JSONResponse(content={"valid": False})
try:
# Use Clerk SDK to validate the request
host = request.url.host if hasattr(request.url, 'host') else 'localhost'
request_state = clerk.authenticate_request(
request,
AuthenticateRequestOptions(
accepts_token=['session', 'oauth_token'],
authorized_parties=[host, 'localhost', '127.0.0.1']
)
)
if request_state.is_signed_in and request_state.payload:
# Get expiration time from JWT payload
exp_timestamp = request_state.payload.get("exp")
expires_at = datetime.utcfromtimestamp(exp_timestamp).isoformat() if exp_timestamp else None
return JSONResponse(content={
"valid": True,
"user_id": request_state.payload.get("sub"),
"session_id": request_state.payload.get("sid"),
"expires_at": expires_at,
"org_id": request_state.payload.get("org_id"),
"org_role": request_state.payload.get("org_role")
})
else:
return JSONResponse(content={
"valid": False,
"reason": getattr(request_state, 'reason', 'Unknown')
})
except Exception as e:
logger.error(f"Session validation error: {e}")
return JSONResponse(content={"valid": False, "error": str(e)})
+1 -1
View File
@@ -39,7 +39,7 @@ saas = [
yargi-mcp = "mcp_server_main:main"
[tool.setuptools]
py-modules = ["mcp_server_main", "mcp_factory", "mcp_auth_factory", "asgi_app", "fastapi_app", "starlette_app", "run_asgi", "oauth_middleware", "oauth_router", "stripe_webhook"]
py-modules = ["mcp_server_main", "mcp_auth_factory", "mcp_auth_http_adapter", "asgi_app", "fastapi_app", "starlette_app", "run_asgi", "stripe_webhook"]
[tool.setuptools.packages.find]
include = ["*_mcp_module", "mcp_auth"]