lurien-matrix / src /api /middleware.py
imDrizzle's picture
Super-squash branch 'main' using huggingface_hub
e84e8e5
Raw
History Blame Contribute Delete
5.52 kB
"""
API Middleware — Authentication & Rate Limiting
Validates X-API-Key header on every request.
Applies rate limiting via Redis.
"""
import logging
from typing import Optional
from fastapi import Request, HTTPException, Depends
from fastapi.security import APIKeyHeader
from src.db import mongo, redis as redis_db
from src.utils.hashing import hash_api_key
logger = logging.getLogger("llm_firewall.middleware")
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
async def validate_api_key(
request: Request,
api_key: Optional[str] = Depends(api_key_header),
) -> dict:
"""
Validate the API key from X-API-Key header.
Returns the key document from MongoDB.
Raises HTTPException on auth failure.
"""
# Check if key is in URL params (security violation)
client_host = request.client.host if request.client else "unknown"
if request.query_params.get("api_key") or request.query_params.get("apikey"):
logger.warning(f"API key in URL params from {client_host}")
raise HTTPException(
status_code=401,
detail="API key must be in X-API-Key header, not URL parameters",
)
if not api_key:
raise HTTPException(status_code=401, detail="API key required")
# Validate format
if not api_key.startswith("fw_live_") or len(api_key) != 72:
raise HTTPException(status_code=401, detail="Invalid API key format")
# Look up in database
key_hash = hash_api_key(api_key)
keys_collection = mongo.get_keys_collection()
key_doc = await keys_collection.find_one({"key_hash": key_hash})
if not key_doc:
raise HTTPException(status_code=401, detail="API key not found")
if not key_doc.get("is_active", False):
raise HTTPException(status_code=401, detail="API key has been revoked")
# Check rate limits
rate_status = await redis_db.check_rate_limit(
api_key_id=str(key_doc["_id"])
)
if not rate_status.allowed:
raise HTTPException(
status_code=429,
detail={
"error": "rate_limit_exceeded",
"limit_type": rate_status.limit_type,
"retry_after_seconds": rate_status.retry_after_seconds,
},
headers={
"Retry-After": str(rate_status.retry_after_seconds),
"X-RateLimit-Limit": str(rate_status.limit),
"X-RateLimit-Remaining": "0",
"X-RateLimit-Reset": str(rate_status.reset_at),
},
)
# Update last_used_at
from datetime import datetime, timezone
await keys_collection.update_one(
{"_id": key_doc["_id"]},
{"$set": {"last_used_at": datetime.now(timezone.utc)}},
)
# Attach rate limit info for response headers
request.state.rate_limit = rate_status
request.state.api_key_doc = key_doc
return key_doc
async def resolve_check_auth(request: Request) -> dict:
"""
Dual-auth resolver for /v1/check endpoints.
External flow: X-API-Key header → standard hash validation.
Dashboard flow: Authorization: Bearer <jwt> + X-Dashboard-Key-ID: <key_id>
→ validates JWT, looks up key by ID, asserts ownership.
ponytail: no new files, reuses validate_api_key and auth_middleware's JWT logic.
"""
api_key = request.headers.get("X-API-Key")
if api_key:
# External flow — existing path unchanged
return await validate_api_key(request, api_key)
# Dashboard flow
dashboard_key_id = request.headers.get("X-Dashboard-Key-ID")
auth_header = request.headers.get("Authorization", "")
if not dashboard_key_id or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="API key or dashboard session required")
token = auth_header.split(" ", 1)[1]
# Reuse JWT logic from auth_middleware inline to avoid circular imports
import os
from jose import JWTError, jwt as _jwt
from bson import ObjectId
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "fallback_secret_key_please_change")
ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
try:
payload = _jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = ObjectId(payload.get("sub"))
except (JWTError, Exception):
raise HTTPException(status_code=401, detail="Invalid session token")
try:
key_obj_id = ObjectId(dashboard_key_id)
except Exception:
raise HTTPException(status_code=400, detail="Invalid key ID")
keys_collection = mongo.get_keys_collection()
key_doc = await keys_collection.find_one({"_id": key_obj_id, "user_id": user_id, "is_active": True})
if not key_doc:
raise HTTPException(status_code=404, detail="API key not found or access denied")
# Rate limiting still applies
rate_status = await redis_db.check_rate_limit(api_key_id=str(key_doc["_id"]))
if not rate_status.allowed:
raise HTTPException(status_code=429, detail={"error": "rate_limit_exceeded"})
from datetime import datetime, timezone
await keys_collection.update_one(
{"_id": key_doc["_id"]},
{"$set": {"last_used_at": datetime.now(timezone.utc)}},
)
request.state.rate_limit = rate_status
request.state.api_key_doc = key_doc
return key_doc