from fastapi import Depends, Request, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jose import JWTError, jwt from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.config.config import settings from app.database import get_db from app.middleware.auth import get_current_user from app.models.api_token import ApiToken oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token", auto_error=False) async def static_token_auth(request: Request, db: AsyncSession = Depends(get_db)) -> dict: """Authenticate using static token + IP allowlist, or fallback to JWT. Returns a dict with user information. For static token auth, returns a minimal payload containing the token and allowed_ip. For JWT, returns the JWT payload as provided by `get_current_user`. """ auth_header = request.headers.get("Authorization") if auth_header and auth_header.lower().startswith("bearer "): token = auth_header[7:].strip() # Check static token table result = await db.execute(select(ApiToken).where(ApiToken.token == token)) api_token = result.scalars().first() if api_token: # Verify request originates from allowed IP (supporting reverse proxies / cloud load balancers) x_forwarded_for = request.headers.get("x-forwarded-for") cf_connecting_ip = request.headers.get("cf-connecting-ip") x_real_ip = request.headers.get("x-real-ip") if cf_connecting_ip: client_ip = cf_connecting_ip.strip() elif x_forwarded_for: # First IP in comma-separated list is the original client client_ip = x_forwarded_for.split(",")[0].strip() elif x_real_ip: client_ip = x_real_ip.strip() else: client_ip = request.client.host if request.client else "unknown" if client_ip == api_token.allowed_ip: return {"static_token": token, "allowed_ip": client_ip} else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=f"IP address not allowed for provided token. Detected IP: '{client_ip}'", ) # If token not found in static table, fall back to JWT validation # Fallback to JWT authentication (Bearer token may be JWT) # `oauth2_scheme` will return None if token not present due to auto_error=False token = await oauth2_scheme(request) if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized: No token provided", headers={"WWW-Authenticate": "Bearer"}, ) # Validate JWT using existing utility try: payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]) username: str = payload.get("sub") if not username: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid JWT payload", headers={"WWW-Authenticate": "Bearer"}, ) return {"username": username, "email": username if "@" in username else None, "name": payload.get("name")} except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid JWT token", headers={"WWW-Authenticate": "Bearer"}, ) # Dependency used across routers require_auth = Depends(static_token_auth)