update auth logic
This commit is contained in:
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user