update auth logic

This commit is contained in:
saidsurucu
2025-07-08 22:34:02 +03:00
parent 5509380cef
commit b86c18c842
2 changed files with 160 additions and 5 deletions
+3
View File
@@ -56,6 +56,9 @@ LOG_LEVEL=info
# Base URL for the application (used for OAuth callbacks and API URLs) # Base URL for the application (used for OAuth callbacks and API URLs)
BASE_URL=http://localhost:8000 BASE_URL=http://localhost:8000
# JWT Secret for MCP token generation
JWT_SECRET_KEY=your_jwt_secret_key_here
# ============================================================================= # =============================================================================
# MCP SERVER SETTINGS # MCP SERVER SETTINGS
# ============================================================================= # =============================================================================
+157 -5
View File
@@ -10,8 +10,11 @@ Usage:
""" """
import os import os
import jwt
import time
from datetime import datetime, timedelta
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.exception_handlers import http_exception_handler from fastapi.exception_handlers import http_exception_handler
from starlette.middleware import Middleware from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
@@ -29,6 +32,7 @@ from mcp_auth_http_adapter 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")
BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com") BASE_URL = os.getenv("BASE_URL", "https://yargimcp.com")
JWT_SECRET = os.getenv("JWT_SECRET_KEY", "your-secret-key-here")
# Configure CORS middleware # Configure CORS middleware
cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",") cors_origins = os.getenv("ALLOWED_ORIGINS", "*").split(",")
@@ -83,10 +87,28 @@ async def custom_401_handler(request: Request, exc: HTTPException):
# Mount MCP app as sub-application # Mount MCP app as sub-application
app.mount("/mcp", mcp_app) app.mount("/mcp", mcp_app)
# Add POST handler for /mcp to forward to mounted app # Add POST handler for /mcp to forward to mounted app with Bearer token validation
@app.post("/mcp") @app.post("/mcp")
async def mcp_post_handler(request: Request): async def mcp_post_handler(request: Request):
"""Forward POST /mcp requests to mounted MCP app""" """Forward POST /mcp requests to mounted MCP app with Bearer token validation"""
# Validate Bearer token
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=401,
detail="Authorization header with Bearer token required"
)
# Extract and validate token
token = auth_header.split(" ")[1]
try:
user_payload = validate_mcp_token(token)
# Add user info to request state for potential use in tools
request.state.user_id = user_payload["user_id"]
request.state.token_scopes = user_payload.get("scopes", ["read", "search"])
except HTTPException:
raise
# Forward to the mounted app by calling it directly # Forward to the mounted app by calling it directly
async def receive(): async def receive():
return await request.receive() return await request.receive()
@@ -209,8 +231,8 @@ async def mcp_info():
"authentication_required": True, "authentication_required": True,
"authentication": { "authentication": {
"type": "oauth2", "type": "oauth2",
"authorization_url": f"{BASE_URL}/auth/login", "authorization_url": "https://yargimcp.com/sign-in",
"token_url": f"{BASE_URL}/auth/callback", "token_url": f"{BASE_URL}/auth/token",
"scopes": ["read", "search"], "scopes": ["read", "search"],
"provider": "clerk" "provider": "clerk"
}, },
@@ -322,6 +344,136 @@ async def status():
"auth_status": "enabled" if os.getenv("ENABLE_AUTH", "false").lower() == "true" else "disabled" "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")
async def validate_clerk_session(request: Request) -> str:
"""Validate Clerk session from cookies and return user_id"""
try:
# Try to import Clerk SDK
from clerk_backend_api import Clerk
# Get Clerk session from cookies
clerk_session = request.cookies.get("__session")
if not clerk_session:
raise HTTPException(status_code=401, detail="No Clerk session found")
# Validate session with Clerk
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
session = clerk.sessions.verify_session(clerk_session)
return session.user_id
except ImportError:
# Fallback for development without Clerk SDK
return "dev_user_123"
except Exception as e:
raise HTTPException(status_code=401, detail=f"Session validation failed: {str(e)}")
# MCP OAuth Callback Endpoint
@app.get("/auth/callback")
async def mcp_oauth_callback(request: Request):
"""Handle OAuth callback for MCP token generation"""
try:
# Validate Clerk session
user_id = await validate_clerk_session(request)
# Generate MCP token
mcp_token = generate_mcp_token(user_id)
# Return success response
return HTMLResponse(f"""
<html>
<head>
<title>MCP Connection Successful</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; padding: 50px; }}
.success {{ color: #28a745; }}
.token {{ background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; word-break: break-all; }}
</style>
</head>
<body>
<h1 class="success">✅ MCP Connection Successful!</h1>
<p>Your Yargı MCP integration is now active.</p>
<div class="token">
<strong>Access Token:</strong><br>
<code>{mcp_token}</code>
</div>
<p>You can now close this window and return to your MCP client.</p>
<script>
// Try to close the popup if opened as such
if (window.opener) {{
window.opener.postMessage({{
type: 'MCP_AUTH_SUCCESS',
token: '{mcp_token}'
}}, '*');
setTimeout(() => window.close(), 3000);
}}
</script>
</body>
</html>
""")
except HTTPException as e:
return HTMLResponse(f"""
<html>
<head>
<title>MCP Connection Failed</title>
<style>
body {{ font-family: Arial, sans-serif; text-align: center; padding: 50px; }}
.error {{ color: #dc3545; }}
</style>
</head>
<body>
<h1 class="error">❌ MCP Connection Failed</h1>
<p>{e.detail}</p>
<p>Please try again or contact support.</p>
<a href="https://yargimcp.com/sign-in">Return to Sign In</a>
</body>
</html>
""", status_code=e.status_code)
# MCP Token Endpoint (for OAuth2 compatibility)
@app.post("/auth/token")
async def mcp_token_endpoint(request: Request):
"""OAuth2 token endpoint for MCP clients"""
try:
# For simplicity, we'll handle this as a redirect from callback
# In a full OAuth2 implementation, this would handle authorization codes
user_id = await validate_clerk_session(request)
mcp_token = generate_mcp_token(user_id)
return JSONResponse({
"access_token": mcp_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search"
})
except HTTPException as e:
return JSONResponse(
status_code=e.status_code,
content={"error": "invalid_request", "error_description": e.detail}
)
# Alternative: SSE transport (for compatibility) # Alternative: SSE transport (for compatibility)
sse_app = mcp_server.http_app( sse_app = mcp_server.http_app(
path="/sse", path="/sse",