Update asgi_app.py
This commit is contained in:
+116
-56
@@ -10,7 +10,6 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import jwt
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
@@ -33,7 +32,6 @@ 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")
|
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -97,19 +95,63 @@ app.mount("/mcp-server", mcp_app)
|
|||||||
async def mcp_protocol_handler(request: Request):
|
async def mcp_protocol_handler(request: Request):
|
||||||
"""Handle MCP protocol requests by forwarding to mounted app"""
|
"""Handle MCP protocol requests by forwarding to mounted app"""
|
||||||
|
|
||||||
# Optional: Validate Bearer JWT tokens for direct API access
|
# Optional: Validate Clerk Bearer JWT tokens for direct API access
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if auth_header and auth_header.startswith("Bearer "):
|
if auth_header and auth_header.startswith("Bearer "):
|
||||||
token = auth_header.split(" ")[1]
|
token = auth_header.split(" ")[1]
|
||||||
try:
|
try:
|
||||||
# Validate custom JWT token (for direct API access)
|
# Validate Clerk JWT token
|
||||||
user_payload = validate_mcp_token(token)
|
from clerk_backend_api import Clerk
|
||||||
logger.info(f"Bearer JWT token validated for user: {user_payload.get('user_id')}")
|
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
|
||||||
# Add user info to request state
|
|
||||||
request.state.user_id = user_payload["user_id"]
|
# Validate Clerk JWT token using authenticate_request
|
||||||
request.state.token_scopes = user_payload.get("scopes", ["read", "search"])
|
import httpx
|
||||||
except HTTPException as e:
|
from clerk_backend_api.security import authenticate_request
|
||||||
logger.warning(f"Bearer token validation failed: {e.detail}")
|
from clerk_backend_api.security.types import AuthenticateRequestOptions
|
||||||
|
|
||||||
|
# Create a mock request with the Bearer token
|
||||||
|
mock_request = httpx.Request(
|
||||||
|
method="POST",
|
||||||
|
url="https://api.yargimcp.com/mock",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
request_state = clerk.authenticate_request(
|
||||||
|
mock_request,
|
||||||
|
AuthenticateRequestOptions(
|
||||||
|
authorized_parties=['https://api.yargimcp.com']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if request_state.is_signed_in:
|
||||||
|
# Get user info from token payload
|
||||||
|
payload = getattr(request_state, 'payload', {})
|
||||||
|
|
||||||
|
# Extract user information from JWT payload
|
||||||
|
user_email = payload.get('email') # Primary identifier
|
||||||
|
user_name = payload.get('name')
|
||||||
|
user_plan = payload.get('plan', 'free') # Default to free plan
|
||||||
|
scopes = payload.get('scopes', ['yargi.read'])
|
||||||
|
|
||||||
|
# Use email as primary user identifier
|
||||||
|
user_id = user_email or request_state.user_id
|
||||||
|
|
||||||
|
logger.info(f"Clerk JWT token validated successfully")
|
||||||
|
logger.info(f"User: {user_email}")
|
||||||
|
logger.info(f"Plan: {user_plan}")
|
||||||
|
logger.info(f"Scopes: {scopes}")
|
||||||
|
|
||||||
|
# Add user info to request state
|
||||||
|
request.state.user_id = user_id
|
||||||
|
request.state.user_email = user_email
|
||||||
|
request.state.user_name = user_name
|
||||||
|
request.state.user_plan = user_plan
|
||||||
|
request.state.token_scopes = scopes
|
||||||
|
else:
|
||||||
|
logger.warning(f"Clerk JWT token validation failed. Reason: {getattr(request_state, 'reason', 'UNKNOWN')}")
|
||||||
|
user_id = None
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Clerk Bearer token validation failed: {str(e)}")
|
||||||
# Don't fail here - let MCP Auth Toolkit handle it
|
# Don't fail here - let MCP Auth Toolkit handle it
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -156,19 +198,59 @@ async def mcp_protocol_handler(request: Request):
|
|||||||
async def sse_protocol_handler(request: Request):
|
async def sse_protocol_handler(request: Request):
|
||||||
"""Handle SSE MCP protocol requests by forwarding to mounted SSE app"""
|
"""Handle SSE MCP protocol requests by forwarding to mounted SSE app"""
|
||||||
|
|
||||||
# Optional: Validate Bearer JWT tokens for direct API access
|
# Optional: Validate Clerk Bearer JWT tokens for direct API access
|
||||||
auth_header = request.headers.get("Authorization")
|
auth_header = request.headers.get("Authorization")
|
||||||
if auth_header and auth_header.startswith("Bearer "):
|
if auth_header and auth_header.startswith("Bearer "):
|
||||||
token = auth_header.split(" ")[1]
|
token = auth_header.split(" ")[1]
|
||||||
try:
|
try:
|
||||||
# Validate custom JWT token (for direct API access)
|
# Validate Clerk JWT token
|
||||||
user_payload = validate_mcp_token(token)
|
from clerk_backend_api import Clerk
|
||||||
logger.info(f"SSE Bearer JWT token validated for user: {user_payload.get('user_id')}")
|
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
|
||||||
# Add user info to request state
|
|
||||||
request.state.user_id = user_payload["user_id"]
|
# Validate Clerk JWT token using authenticate_request
|
||||||
request.state.token_scopes = user_payload.get("scopes", ["read", "search"])
|
import httpx
|
||||||
except HTTPException as e:
|
from clerk_backend_api.security import authenticate_request
|
||||||
logger.warning(f"SSE Bearer token validation failed: {e.detail}")
|
from clerk_backend_api.security.types import AuthenticateRequestOptions
|
||||||
|
|
||||||
|
# Create a mock request with the Bearer token
|
||||||
|
mock_request = httpx.Request(
|
||||||
|
method="POST",
|
||||||
|
url="https://api.yargimcp.com/mock",
|
||||||
|
headers={"Authorization": f"Bearer {token}"}
|
||||||
|
)
|
||||||
|
|
||||||
|
request_state = clerk.authenticate_request(
|
||||||
|
mock_request,
|
||||||
|
AuthenticateRequestOptions(
|
||||||
|
authorized_parties=['https://api.yargimcp.com']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if request_state.is_signed_in:
|
||||||
|
# Get user info from token payload
|
||||||
|
payload = getattr(request_state, 'payload', {})
|
||||||
|
|
||||||
|
# Extract user information from JWT payload
|
||||||
|
user_email = payload.get('email') # Primary identifier
|
||||||
|
user_plan = payload.get('plan', 'free')
|
||||||
|
scopes = payload.get('scopes', ['yargi.read'])
|
||||||
|
|
||||||
|
# Use email as primary user identifier
|
||||||
|
user_id = user_email or request_state.user_id
|
||||||
|
else:
|
||||||
|
user_id = None
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
logger.info(f"SSE Clerk Bearer JWT token validated for user: {user_id}")
|
||||||
|
# Add user info to request state
|
||||||
|
request.state.user_id = user_id
|
||||||
|
request.state.user_email = user_email
|
||||||
|
request.state.user_plan = user_plan
|
||||||
|
request.state.token_scopes = scopes
|
||||||
|
else:
|
||||||
|
logger.warning("SSE Clerk JWT token validation failed - no user_id in claims")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"SSE Clerk Bearer token validation failed: {str(e)}")
|
||||||
# Don't fail here - let MCP Auth Toolkit handle it
|
# Don't fail here - let MCP Auth Toolkit handle it
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -419,28 +501,8 @@ 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
|
# Note: JWT token validation is now handled entirely by Clerk
|
||||||
def generate_mcp_token(user_id: str, expires_in: int = 3600) -> str:
|
# All authentication flows use Clerk JWT tokens directly
|
||||||
"""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, clerk_token: str = None) -> str:
|
async def validate_clerk_session(request: Request, clerk_token: str = None) -> str:
|
||||||
"""Validate Clerk session from cookies or JWT token and return user_id"""
|
"""Validate Clerk session from cookies or JWT token and return user_id"""
|
||||||
@@ -498,9 +560,8 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
user_id = await validate_clerk_session(request, clerk_token)
|
user_id = await validate_clerk_session(request, clerk_token)
|
||||||
logger.info(f"User authenticated successfully - user_id: {user_id}")
|
logger.info(f"User authenticated successfully - user_id: {user_id}")
|
||||||
|
|
||||||
# Generate MCP token
|
# Use the Clerk JWT token directly (no need to generate custom token)
|
||||||
mcp_token = generate_mcp_token(user_id)
|
logger.info("User authenticated successfully via Clerk")
|
||||||
logger.info("MCP token generated successfully")
|
|
||||||
|
|
||||||
# Return success response
|
# Return success response
|
||||||
return HTMLResponse(f"""
|
return HTMLResponse(f"""
|
||||||
@@ -517,8 +578,8 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
<h1 class="success">✅ MCP Connection Successful!</h1>
|
<h1 class="success">✅ MCP Connection Successful!</h1>
|
||||||
<p>Your Yargı MCP integration is now active.</p>
|
<p>Your Yargı MCP integration is now active.</p>
|
||||||
<div class="token">
|
<div class="token">
|
||||||
<strong>Access Token:</strong><br>
|
<strong>Authentication:</strong><br>
|
||||||
<code>{mcp_token}</code>
|
<code>Use your Clerk JWT token directly with Bearer authentication</code>
|
||||||
</div>
|
</div>
|
||||||
<p>You can now close this window and return to your MCP client.</p>
|
<p>You can now close this window and return to your MCP client.</p>
|
||||||
<script>
|
<script>
|
||||||
@@ -526,7 +587,7 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
if (window.opener) {{
|
if (window.opener) {{
|
||||||
window.opener.postMessage({{
|
window.opener.postMessage({{
|
||||||
type: 'MCP_AUTH_SUCCESS',
|
type: 'MCP_AUTH_SUCCESS',
|
||||||
token: '{mcp_token}'
|
token: 'use_clerk_jwt_token'
|
||||||
}}, '*');
|
}}, '*');
|
||||||
setTimeout(() => window.close(), 3000);
|
setTimeout(() => window.close(), 3000);
|
||||||
}}
|
}}
|
||||||
@@ -581,21 +642,20 @@ async def mcp_oauth_callback(request: Request, clerk_token: str = Query(None)):
|
|||||||
</html>
|
</html>
|
||||||
""", status_code=500)
|
""", status_code=500)
|
||||||
|
|
||||||
# MCP Token Endpoint (for OAuth2 compatibility)
|
# OAuth2 Token Endpoint - Now uses Clerk JWT tokens directly
|
||||||
@app.post("/auth/mcp-token")
|
@app.post("/auth/mcp-token")
|
||||||
async def mcp_token_endpoint(request: Request):
|
async def mcp_token_endpoint(request: Request):
|
||||||
"""OAuth2 token endpoint for MCP clients"""
|
"""OAuth2 token endpoint for MCP clients - returns Clerk JWT token info"""
|
||||||
try:
|
try:
|
||||||
# For simplicity, we'll handle this as a redirect from callback
|
# Validate Clerk session
|
||||||
# In a full OAuth2 implementation, this would handle authorization codes
|
|
||||||
user_id = await validate_clerk_session(request)
|
user_id = await validate_clerk_session(request)
|
||||||
mcp_token = generate_mcp_token(user_id)
|
|
||||||
|
|
||||||
return JSONResponse({
|
return JSONResponse({
|
||||||
"access_token": mcp_token,
|
"message": "Use your Clerk JWT token directly with Bearer authentication",
|
||||||
"token_type": "Bearer",
|
"token_type": "Bearer",
|
||||||
"expires_in": 3600,
|
"scope": "yargi.read",
|
||||||
"scope": "read search"
|
"user_id": user_id,
|
||||||
|
"instructions": "Include 'Authorization: Bearer YOUR_CLERK_JWT_TOKEN' in your requests"
|
||||||
})
|
})
|
||||||
except HTTPException as e:
|
except HTTPException as e:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
Reference in New Issue
Block a user