add clerk oauth

This commit is contained in:
saidsurucu
2025-07-01 17:31:48 +03:00
parent bbf99870eb
commit e0bba7625f
5 changed files with 571 additions and 80 deletions
+83 -50
View File
@@ -1,63 +1,96 @@
# Yargı MCP Server Environment Configuration
# Copy this file to .env and customize as needed
# OAuth Configuration for Clerk + Google
# Copy this file to .env and fill in your actual values
# Server Configuration
# =============================================================================
# AUTHENTICATION SETTINGS
# =============================================================================
# Enable/disable authentication (set to "true" to enable OAuth)
ENABLE_AUTH=false
# =============================================================================
# CLERK CONFIGURATION
# =============================================================================
# Clerk API keys (get from https://dashboard.clerk.com/)
CLERK_SECRET_KEY=sk_test_your_secret_key_here
CLERK_PUBLISHABLE_KEY=pk_test_your_publishable_key_here
# OAuth Redirect URLs
CLERK_OAUTH_REDIRECT_URL=http://localhost:8000/auth/callback
CLERK_FRONTEND_URL=http://localhost:3000
# Clerk domain issuer (usually auto-configured)
CLERK_ISSUER=https://your-clerk-domain.clerk.accounts.dev
# =============================================================================
# GOOGLE OAUTH SETTINGS
# =============================================================================
# Note: Google OAuth is configured through Clerk dashboard
# You need to:
# 1. Go to Clerk Dashboard > Social Connections
# 2. Enable Google provider
# 3. Add your Google OAuth client ID and secret
# 4. Configure redirect URIs in Google Console
# =============================================================================
# STRIPE CONFIGURATION (for payments/subscriptions)
# =============================================================================
STRIPE_SECRET=sk_test_your_stripe_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
# =============================================================================
# SERVER CONFIGURATION
# =============================================================================
# CORS origins (comma-separated list)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8000,https://yourdomain.com
# Server settings
HOST=0.0.0.0
PORT=8000
LOG_LEVEL=INFO
LOG_LEVEL=info
# Authentication Configuration
ENABLE_AUTH=false # Set to true in production for JWT validation
# =============================================================================
# MCP SERVER SETTINGS
# =============================================================================
# CORS Configuration
# Comma-separated list of allowed origins
# Use * to allow all origins (not recommended for production)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,https://yourdomain.com
# Additional MCP server configuration can go here
# For example, rate limiting, feature flags, etc.
# Authentication (optional)
# Uncomment and set to enable token-based authentication
# API_TOKEN=your-secret-token-here
# Example: Rate limiting
# MAX_REQUESTS_PER_MINUTE=60
# BURST_CAPACITY=20
# Worker Configuration
# Number of worker processes (for production)
# WORKERS=4
# =============================================================================
# USAGE INSTRUCTIONS
# =============================================================================
# SSL Configuration (optional)
# SSL_CERT_FILE=/path/to/cert.pem
# SSL_KEY_FILE=/path/to/key.pem
# 1. Copy this file to .env:
# cp .env.example .env
# Database Timeouts (seconds)
# Adjust based on your network conditions
YARGITAY_TIMEOUT=60
DANISTAY_TIMEOUT=60
BEDESTEN_TIMEOUT=60
ANAYASA_TIMEOUT=90
KIK_TIMEOUT=45
REKABET_TIMEOUT=45
UYUSMAZLIK_TIMEOUT=30
EMSAL_TIMEOUT=60
# 2. Get Clerk credentials:
# - Sign up at https://clerk.com/
# - Create a new application
# - Go to API Keys tab
# - Copy Secret Key and Publishable Key
# Development Settings
# Enable debug mode (not for production)
# DEBUG=false
# 3. Configure Google OAuth in Clerk:
# - In Clerk Dashboard, go to Social Connections
# - Enable Google provider
# - Get Google OAuth credentials from Google Console
# - Add redirect URI: http://localhost:8000/auth/callback
# Monitoring (optional)
# Sentry DSN for error tracking
# SENTRY_DSN=https://your-sentry-dsn@sentry.io/project-id
# 4. Update OAuth URLs:
# - Set CLERK_OAUTH_REDIRECT_URL to your callback URL
# - Set CLERK_FRONTEND_URL to your frontend application URL
# ------ Clerk ------
CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
CLERK_ISSUER=https://your-app.clerk.accounts.dev # issuer & JWKS root (optional if using CLERK_PUBLIC_KEY)
# CLERK_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----\nMIIBIjANBg...\n-----END PUBLIC KEY-----
# 5. Enable authentication:
# - Set ENABLE_AUTH=true
# ------ Stripe ------
STRIPE_SECRET=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# Application Configuration
APP_URL=http://localhost:8000
# OpenTelemetry Configuration (optional)
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
# OTEL_SERVICE_NAME=yargi-mcp-server
# 6. Test the OAuth flow:
# - Start server: uvicorn asgi_app:app --reload
# - Visit: http://localhost:8000/auth/login
# - Complete OAuth flow with Google
# - Check: http://localhost:8000/auth/user
+16 -5
View File
@@ -21,6 +21,9 @@ 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
# Configure CORS middleware
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
custom_middleware = [
@@ -42,7 +45,7 @@ mcp_app = mcp_server.http_app(
# Create FastAPI wrapper application with MCP app's lifespan
app = FastAPI(
title="Yargı MCP Server",
description="MCP server for Turkish legal databases with JWT authentication",
description="MCP server for Turkish legal databases with OAuth authentication",
version="0.1.0",
middleware=custom_middleware,
lifespan=mcp_app.lifespan # Critical: Get lifespan from mcp_app, not mcp_server
@@ -51,6 +54,9 @@ 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)
# Mount MCP app as sub-application
app.mount("/mcp", mcp_app)
@@ -72,12 +78,16 @@ async def root():
"""Root endpoint with service information"""
return JSONResponse({
"service": "Yargı MCP Server",
"description": "MCP server for Turkish legal databases with JWT authentication",
"description": "MCP server for Turkish legal databases with OAuth authentication",
"endpoints": {
"mcp": "/mcp/",
"health": "/health",
"status": "/status",
"stripe_webhook": "/api/stripe/webhook"
"stripe_webhook": "/api/stripe/webhook",
"oauth_login": "/auth/login",
"oauth_callback": "/auth/callback",
"oauth_google": "/auth/google/login",
"user_info": "/auth/user"
},
"supported_databases": [
"Yargıtay (Court of Cassation)",
@@ -92,9 +102,10 @@ async def root():
],
"authentication": {
"enabled": os.getenv("ENABLE_AUTH", "false").lower() == "true",
"type": "JWT Bearer Token",
"type": "OAuth 2.0 via Clerk",
"issuer": os.getenv("CLERK_ISSUER", "https://clerk.accounts.dev"),
"required_scopes": ["yargi.read"]
"providers": ["google"],
"flow": "authorization_code"
}
})
+24 -25
View File
@@ -2,35 +2,34 @@ import os
from functools import lru_cache
from fastmcp import FastMCP
from fastmcp.server.auth import BearerAuthProvider
from oauth_middleware import ClerkOAuthMiddleware
@lru_cache
def create_app() -> FastMCP:
"""Return a FastMCP instance; Clerk JWT validation when ENABLE_AUTH=true."""
if os.getenv("ENABLE_AUTH", "false").lower() != "true":
return FastMCP(
name="Yargı MCP DEV",
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"]
)
# Public key'i environment variable'dan al, yoksa None
public_key_pem = os.environ.get("CLERK_PUBLIC_KEY")
"""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"]
}
if public_key_pem:
# Eğer public key varsa, onu kullan (production)
auth = BearerAuthProvider(
public_key=public_key_pem,
# issuer, audience ve required_scopes kontrollerini yapmıyoruz
if os.getenv("ENABLE_AUTH", "false").lower() != "true":
# Development mode - no authentication
app = FastMCP(
name="Yargı MCP DEV",
**app_config
)
else:
# Public key yoksa JWKS endpoint kullan (development/fallback)
clerk_issuer = os.environ.get("CLERK_ISSUER", "https://clerk.accounts.dev")
auth = BearerAuthProvider(
jwks_uri=f"{clerk_issuer}/.well-known/jwks.json",
# Production mode - OAuth authentication via middleware
app = FastMCP(
name="Yargı MCP PROD",
**app_config
)
return FastMCP(
name="Yargı MCP PROD",
auth=auth,
instructions="MCP server for TR legal databases (Yargitay, Danistay, Emsal, Uyusmazlik, Anayasa-Norm, Anayasa-Bireysel, KIK, Sayistay, Rekabet) with JWT authentication.",
dependencies=["httpx", "beautifulsoup4", "markitdown", "pydantic", "aiohttp", "playwright"]
)
# Add OAuth middleware instead of BearerAuthProvider
app.add_middleware(ClerkOAuthMiddleware())
# Update instructions to reflect OAuth
app.instructions += " with OAuth authentication via Clerk."
return app
+178
View File
@@ -0,0 +1,178 @@
"""
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
from clerk_backend_api import Clerk
from clerk_backend_api.errors import SDKError
from clerk_backend_api.security import authenticate_request
from clerk_backend_api.security.types import AuthenticateRequestOptions
from mcp import McpError
from mcp.types import ErrorData
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.clerk_secret = os.getenv("CLERK_SECRET_KEY")
if not self.clerk_secret:
raise ValueError("CLERK_SECRET_KEY environment variable is required")
self.clerk = Clerk(bearer_auth=self.clerk_secret)
self.enable_auth = os.getenv("ENABLE_AUTH", "false").lower() == "true"
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
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.
Returns user info if token is valid, None otherwise.
"""
try:
# 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 _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)
+270
View File
@@ -0,0 +1,270 @@
"""
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 clerk_backend_api import Clerk
from clerk_backend_api.errors import SDKError
from clerk_backend_api.security import authenticate_request
from clerk_backend_api.security.types import AuthenticateRequestOptions
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/auth")
# Initialize Clerk client
clerk_secret = os.getenv("CLERK_SECRET_KEY")
clerk_publishable = os.getenv("CLERK_PUBLISHABLE_KEY")
clerk_frontend_url = os.getenv("CLERK_FRONTEND_URL", "http://localhost:3000")
redirect_url = os.getenv("CLERK_OAUTH_REDIRECT_URL", "http://localhost:8000/auth/callback")
if not clerk_secret:
raise ValueError("CLERK_SECRET_KEY environment variable is required")
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
clerk_oauth_params = {
"redirect_url": redirect_uri or redirect_url,
}
# For Clerk, we typically use their hosted sign-in page
# or the Clerk.js frontend SDK
clerk_sign_in_url = f"https://{clerk_publishable.split('_')[1]}.clerk.accounts.dev/sign-in"
# Add redirect URL as a query parameter
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("/callback")
async def oauth_callback(
request: Request,
code: Optional[str] = None,
state: Optional[str] = None,
error: Optional[str] = None,
error_description: Optional[str] = None
):
"""
Handle OAuth callback from Clerk.
This endpoint receives the authorization code from Clerk
and exchanges it for an access token.
"""
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:
# In a typical OAuth flow, we would exchange the code for tokens here
# However, Clerk handles this differently - the frontend SDK manages tokens
# For server-side validation, we need to:
# 1. Use Clerk's session tokens (not raw OAuth tokens)
# 2. Or implement a custom session management system
# For now, we'll create a session token that can be validated by our middleware
# In production, you'd want to:
# - Store this in a database/cache
# - Set proper expiration
# - Link to user's Clerk ID
session_token = secrets.token_urlsafe(64)
# Return the session token to the client
# In a real app, you might:
# 1. Set this as an HTTP-only cookie
# 2. Redirect to the frontend with the token
# 3. Store in a secure session store
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 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.
"""
# Build Clerk sign-in URL with Google as the provider
clerk_domain = clerk_publishable.split('_')[1]
google_oauth_url = f"https://{clerk_domain}.clerk.accounts.dev/sign-in#/?strategy=oauth_google"
return RedirectResponse(url=google_oauth_url)
@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)})