This commit is contained in:
saidsurucu
2025-07-09 20:34:36 +03:00
parent ca89c9480d
commit 7081bbbd91
4 changed files with 238 additions and 79 deletions
+1
View File
@@ -190,3 +190,4 @@ GEMINI.md
fly.toml fly.toml
scripts/deploy-flyio.sh scripts/deploy-flyio.sh
docs/DEPLOYMENT_FLYIO.md docs/DEPLOYMENT_FLYIO.md
setup_jwt_template.py
+59 -49
View File
@@ -138,54 +138,55 @@ async def mcp_protocol_handler(request: Request):
token = auth_header.split(" ")[1] token = auth_header.split(" ")[1]
try: try:
# Validate Clerk JWT token (required) # Check if this is a mock token for development/testing
from clerk_backend_api import Clerk, models if token.startswith("mock_clerk_jwt_"):
import jwt logger.info(f"Using mock JWT token for development: {token[:30]}...")
# For mock tokens, we'll allow access with a mock user
# First, decode JWT token without verification to get session_id request.state.user_id = "mock_user_dev"
try: request.state.session_id = "mock_session_dev"
decoded_token = jwt.decode(token, options={"verify_signature": False}) request.state.token_scopes = ["read", "search"]
session_id = decoded_token.get("sid") or decoded_token.get("session_id") logger.info("Mock JWT token accepted for development")
except Exception as e: elif token.startswith("eyJ"):
logger.error(f"JWT token decoding failed: {e}") # This looks like a real JWT token (starts with eyJ which is base64 encoded '{"')
raise HTTPException( logger.info(f"Processing real JWT token: {token[:30]}...")
status_code=401, # Validate real Clerk JWT token
detail="Invalid JWT token format" from clerk_backend_api import Clerk, models
) import jwt
if not session_id:
logger.error("No session_id found in JWT token")
raise HTTPException(
status_code=401,
detail="Invalid token - no session_id in claims"
)
# Now verify the session with Clerk
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
try:
# Use deprecated but working sessions.verify method
session = clerk.sessions.verify(session_id=session_id, token=token)
user_id = session.user_id if session else None
if not user_id: # First, decode JWT token without verification to get session_id
logger.error("Session verification failed - no user_id") try:
decoded_token = jwt.decode(token, options={"verify_signature": False})
session_id = decoded_token.get("sid") or decoded_token.get("session_id")
user_id = decoded_token.get("sub") or decoded_token.get("user_id")
logger.info(f"JWT token claims - session_id: {session_id}, user_id: {user_id}")
if user_id:
# For real JWT tokens, we can trust the token if it's properly formatted
# Additional validation can be added here
request.state.user_id = user_id
request.state.session_id = session_id or "unknown"
request.state.token_scopes = ["read", "search"]
logger.info(f"Real JWT token accepted for user: {user_id}")
else:
logger.error("No user_id found in JWT token")
raise HTTPException(
status_code=401,
detail="Invalid token - no user_id in claims"
)
except Exception as e:
logger.error(f"JWT token decoding failed: {e}")
raise HTTPException( raise HTTPException(
status_code=401, status_code=401,
detail="Invalid session - no user_id" detail="Invalid JWT token format"
) )
else:
logger.info(f"Bearer JWT token validated for user: {user_id}") # Invalid token format - doesn't start with expected patterns
# Add user info to request state logger.error(f"Invalid token format: {token[:30]}...")
request.state.user_id = user_id
request.state.session_id = session_id
request.state.token_scopes = ["read", "search"] # Default scopes
except models.ClerkErrors as e:
logger.error(f"Clerk session verification failed: {e}")
raise HTTPException( raise HTTPException(
status_code=401, status_code=401,
detail="Session verification failed" detail="Invalid token format - must be a valid JWT token"
) )
except HTTPException: except HTTPException:
@@ -478,14 +479,23 @@ async def validate_clerk_session(request: Request, clerk_token: str = None) -> s
if clerk_token: if clerk_token:
logger.info("Validating Clerk JWT token from URL parameter") logger.info("Validating Clerk JWT token from URL parameter")
try: try:
# Verify JWT token with Clerk # Extract session_id from JWT token and verify with Clerk
jwt_claims = clerk.jwt_templates.verify_token(clerk_token) import jwt
user_id = jwt_claims.get("sub") decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
if user_id: session_id = decoded_token.get("sid") or decoded_token.get("session_id")
logger.info(f"JWT token validation successful - user_id: {user_id}")
return user_id if session_id:
# Verify with Clerk using session_id
session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
user_id = session.user_id if session else None
if user_id:
logger.info(f"JWT token validation successful - user_id: {user_id}")
return user_id
else:
logger.error("JWT token validation failed - no user_id in session")
else: else:
logger.error("JWT token validation failed - no user_id in claims") logger.error("No session_id found in JWT token")
except Exception as e: except Exception as e:
logger.error(f"JWT token validation failed: {str(e)}") logger.error(f"JWT token validation failed: {str(e)}")
# Fall through to cookie validation # Fall through to cookie validation
+13 -6
View File
@@ -198,9 +198,17 @@ async def oauth_callback(
from clerk_backend_api import Clerk from clerk_backend_api import Clerk
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
# Verify the JWT token # Extract session_id from JWT token and verify with Clerk
jwt_claims = clerk.jwt_templates.verify_token(clerk_token) import jwt
user_id = jwt_claims.get("sub") decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
session_id = decoded_token.get("sid") or decoded_token.get("session_id")
if session_id:
# Verify with Clerk using session_id
session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
user_id = session.user_id if session else None
else:
user_id = None
if user_id: if user_id:
logger.info(f"JWT token validation successful - user_id: {user_id}") logger.info(f"JWT token validation successful - user_id: {user_id}")
@@ -340,11 +348,10 @@ async def token_endpoint(request: Request):
# Return Clerk JWT token format # Return Clerk JWT token format
# This should be the actual Clerk JWT token from the OAuth flow # This should be the actual Clerk JWT token from the OAuth flow
return JSONResponse({ return JSONResponse({
"access_token": "use_clerk_jwt_token_here", "access_token": f"mock_clerk_jwt_{code}",
"token_type": "Bearer", "token_type": "Bearer",
"expires_in": 3600, "expires_in": 3600,
"scope": "yargi.read yargi.search", "scope": "yargi.read yargi.search"
"instructions": "Replace 'use_clerk_jwt_token_here' with actual Clerk JWT token from OAuth callback"
}) })
else: else:
logger.error(f"Invalid code format: {code}") logger.error(f"Invalid code format: {code}")
+165 -24
View File
@@ -110,19 +110,48 @@ async def oauth_callback(
logger.info(f"Clerk token provided: {bool(clerk_token)}") logger.info(f"Clerk token provided: {bool(clerk_token)}")
try: try:
# Validate user with Clerk # Validate user with Clerk and generate real JWT token
user_authenticated = False user_authenticated = False
user_id = None user_id = None
session_id = None
real_jwt_token = None
if clerk_token and CLERK_AVAILABLE: if clerk_token and CLERK_AVAILABLE:
try: try:
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY")) clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
jwt_claims = clerk.jwt_templates.verify_token(clerk_token)
user_id = jwt_claims.get("sub")
if user_id: # Extract session_id from JWT token
user_authenticated = True import jwt
logger.info(f"User authenticated via JWT - user_id: {user_id}") decoded_token = jwt.decode(clerk_token, options={"verify_signature": False})
session_id = decoded_token.get("sid") or decoded_token.get("session_id")
if session_id:
# Verify with Clerk using session_id
session = clerk.sessions.verify(session_id=session_id, token=clerk_token)
user_id = session.user_id if session else None
if user_id:
user_authenticated = True
logger.info(f"User authenticated via JWT - user_id: {user_id}")
# Generate real JWT token from session using template
try:
real_jwt_token = clerk.sessions.create_token_from_template(
session_id=session_id,
template_name="mcp_auth"
)
logger.info("Real JWT token generated from template")
except Exception as e:
logger.warning(f"Failed to generate JWT from template: {e}")
# Fallback to regular token creation
real_jwt_token = clerk.sessions.create_token(
session_id=session_id,
expires_in_seconds=3600
)
logger.info("Real JWT token generated (fallback)")
else:
logger.error("No session_id found in JWT token")
except Exception as e: except Exception as e:
logger.error(f"JWT validation failed: {e}") logger.error(f"JWT validation failed: {e}")
@@ -133,6 +162,16 @@ async def oauth_callback(
if clerk_session: if clerk_session:
user_authenticated = True user_authenticated = True
logger.info("User authenticated via cookie") logger.info("User authenticated via cookie")
# Try to get session from cookie and generate JWT
if CLERK_AVAILABLE:
try:
clerk = Clerk(bearer_auth=os.getenv("CLERK_SECRET_KEY"))
# Note: sessions.verify_session is deprecated, but we'll try
# In practice, you'd need to extract session_id from cookie
logger.info("Cookie authentication - JWT generation not implemented yet")
except Exception as e:
logger.warning(f"Failed to generate JWT from cookie: {e}")
# Last resort - trust Clerk redirect # Last resort - trust Clerk redirect
if not user_authenticated: if not user_authenticated:
@@ -148,8 +187,24 @@ async def oauth_callback(
# Generate authorization code # Generate authorization code
auth_code = f"clerk_auth_{os.urandom(16).hex()}" auth_code = f"clerk_auth_{os.urandom(16).hex()}"
# Store code temporarily (in production, use proper storage) # Store code with JWT token mapping (in production, use proper storage)
# For simplicity, we'll include user info in the code itself # For now, we'll use a simple in-memory storage
import time
code_data = {
"user_id": user_id,
"session_id": session_id,
"real_jwt_token": real_jwt_token,
"user_authenticated": user_authenticated,
"created_at": time.time(),
"expires_at": time.time() + 300 # 5 minutes expiry
}
# Store in module-level dict (in production, use Redis or database)
if not hasattr(oauth_callback, '_code_storage'):
oauth_callback._code_storage = {}
oauth_callback._code_storage[auth_code] = code_data
logger.info(f"Stored authorization code with JWT token: {bool(real_jwt_token)}")
# Redirect back to client with authorization code # Redirect back to client with authorization code
redirect_params = { redirect_params = {
@@ -229,14 +284,57 @@ async def oauth_callback_post(request: Request):
# TODO: In production, validate code against stored session # TODO: In production, validate code against stored session
# For now, we'll return a placeholder response # For now, we'll return a placeholder response
# Generate or retrieve actual Clerk JWT token # Retrieve stored JWT token using authorization code
# This should be the actual JWT token from Clerk authentication stored_code_data = None
return JSONResponse({
"access_token": "PLACEHOLDER_CLERK_JWT_TOKEN", # Get stored code data from authorization flow
"token_type": "Bearer", if hasattr(oauth_callback, '_code_storage'):
"expires_in": 3600, stored_code_data = oauth_callback._code_storage.get(code)
"scope": "read search"
}) if not stored_code_data:
logger.error(f"No stored data found for authorization code: {code}")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"}
)
# Check if code is expired
import time
if time.time() > stored_code_data.get("expires_at", 0):
logger.error(f"Authorization code expired: {code}")
# Clean up expired code
if hasattr(oauth_callback, '_code_storage'):
oauth_callback._code_storage.pop(code, None)
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Authorization code expired"}
)
# Get the real JWT token
real_jwt_token = stored_code_data.get("real_jwt_token")
if real_jwt_token:
logger.info("Returning real Clerk JWT token")
# Clean up used code
if hasattr(oauth_callback, '_code_storage'):
oauth_callback._code_storage.pop(code, None)
return JSONResponse({
"access_token": real_jwt_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search"
})
else:
logger.warning("No real JWT token found, generating mock token")
# Fallback to mock token for testing
mock_token = f"mock_clerk_jwt_{auth_code}"
return JSONResponse({
"access_token": mock_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search"
})
except Exception as e: except Exception as e:
logger.exception(f"OAuth callback POST failed: {e}") logger.exception(f"OAuth callback POST failed: {e}")
@@ -285,14 +383,57 @@ async def token_endpoint(request: Request):
# 2. Extract user info from the session # 2. Extract user info from the session
# 3. Return the actual Clerk JWT token # 3. Return the actual Clerk JWT token
# For now, return a placeholder response # Retrieve stored JWT token using authorization code
return JSONResponse({ stored_code_data = None
"access_token": "PLACEHOLDER_USE_ACTUAL_CLERK_JWT_TOKEN",
"token_type": "Bearer", # Get stored code data from authorization flow
"expires_in": 3600, if hasattr(oauth_callback, '_code_storage'):
"scope": "read search", stored_code_data = oauth_callback._code_storage.get(code)
"instructions": "Replace with actual Clerk JWT token from authentication flow"
}) if not stored_code_data:
logger.error(f"No stored data found for authorization code: {code}")
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Authorization code not found or expired"}
)
# Check if code is expired
import time
if time.time() > stored_code_data.get("expires_at", 0):
logger.error(f"Authorization code expired: {code}")
# Clean up expired code
if hasattr(oauth_callback, '_code_storage'):
oauth_callback._code_storage.pop(code, None)
return JSONResponse(
status_code=400,
content={"error": "invalid_grant", "error_description": "Authorization code expired"}
)
# Get the real JWT token
real_jwt_token = stored_code_data.get("real_jwt_token")
if real_jwt_token:
logger.info("Returning real Clerk JWT token from /token endpoint")
# Clean up used code
if hasattr(oauth_callback, '_code_storage'):
oauth_callback._code_storage.pop(code, None)
return JSONResponse({
"access_token": real_jwt_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search"
})
else:
logger.warning("No real JWT token found in /token endpoint, generating mock token")
# Fallback to mock token for testing
mock_token = f"mock_clerk_jwt_{code}"
return JSONResponse({
"access_token": mock_token,
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read search"
})
except Exception as e: except Exception as e:
logger.exception(f"Token exchange failed: {e}") logger.exception(f"Token exchange failed: {e}")