From e0bba7625f9d061ae31aa00835a499964b80dec8 Mon Sep 17 00:00:00 2001 From: saidsurucu Date: Tue, 1 Jul 2025 17:31:48 +0300 Subject: [PATCH] add clerk oauth --- .env.example | 133 ++++++++++++++-------- asgi_app.py | 21 +++- mcp_factory.py | 49 ++++---- oauth_middleware.py | 178 +++++++++++++++++++++++++++++ oauth_router.py | 270 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 571 insertions(+), 80 deletions(-) create mode 100644 oauth_middleware.py create mode 100644 oauth_router.py diff --git a/.env.example b/.env.example index 5f3b3e0..40ecaf7 100644 --- a/.env.example +++ b/.env.example @@ -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 \ No newline at end of file +# 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 \ No newline at end of file diff --git a/asgi_app.py b/asgi_app.py index 45e40be..0aca892 100644 --- a/asgi_app.py +++ b/asgi_app.py @@ -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" } }) diff --git a/mcp_factory.py b/mcp_factory.py index 9b5dd01..fe770c5 100644 --- a/mcp_factory.py +++ b/mcp_factory.py @@ -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"] - ) \ No newline at end of file + + # Add OAuth middleware instead of BearerAuthProvider + app.add_middleware(ClerkOAuthMiddleware()) + + # Update instructions to reflect OAuth + app.instructions += " with OAuth authentication via Clerk." + + return app \ No newline at end of file diff --git a/oauth_middleware.py b/oauth_middleware.py new file mode 100644 index 0000000..f9697d4 --- /dev/null +++ b/oauth_middleware.py @@ -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 " + )) + + # 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) \ No newline at end of file diff --git a/oauth_router.py b/oauth_router.py new file mode 100644 index 0000000..17d4b6c --- /dev/null +++ b/oauth_router.py @@ -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)}) \ No newline at end of file