Implement critical security fixes: secret management, XML parsing, auth, input validation, security headers
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -1,15 +1,179 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Union
|
||||
from typing import Any, Union, Optional
|
||||
import secrets
|
||||
import logging
|
||||
|
||||
from jose import jwt
|
||||
from fastapi import HTTPException, Security, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials, APIKeyHeader
|
||||
from jose import jwt, JWTError
|
||||
from passlib.context import CryptContext
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# Security schemes for authentication
|
||||
security_bearer = HTTPBearer(auto_error=False)
|
||||
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
||||
|
||||
# In-memory API keys storage (for MVP - should be moved to database in production)
|
||||
_api_keys = set()
|
||||
|
||||
|
||||
def generate_api_key() -> str:
|
||||
"""
|
||||
Generate a secure random API key.
|
||||
|
||||
Returns:
|
||||
A 32-character hexadecimal API key
|
||||
"""
|
||||
return secrets.token_hex(32)
|
||||
|
||||
|
||||
def add_api_key(api_key: str) -> bool:
|
||||
"""
|
||||
Add an API key to the valid keys set.
|
||||
|
||||
Args:
|
||||
api_key: The API key to add
|
||||
|
||||
Returns:
|
||||
True if key was added, False if it already existed
|
||||
"""
|
||||
if api_key in _api_keys:
|
||||
return False
|
||||
_api_keys.add(api_key)
|
||||
logger.info(f"API key added (ends with: ...{api_key[-8:]})")
|
||||
return True
|
||||
|
||||
|
||||
def verify_api_key(api_key: str) -> bool:
|
||||
"""
|
||||
Verify an API key is valid.
|
||||
|
||||
Args:
|
||||
api_key: The API key to verify
|
||||
|
||||
Returns:
|
||||
True if key is valid, False otherwise
|
||||
"""
|
||||
return api_key in _api_keys
|
||||
|
||||
|
||||
async def get_api_key(
|
||||
api_key_header: Optional[str] = Security(api_key_header)
|
||||
) -> str:
|
||||
"""
|
||||
Dependency to verify API key authentication.
|
||||
|
||||
Args:
|
||||
api_key_header: API key from X-API-Key header
|
||||
|
||||
Returns:
|
||||
The validated API key
|
||||
|
||||
Raises:
|
||||
HTTPException: If API key is missing or invalid
|
||||
"""
|
||||
if not api_key_header:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
if not verify_api_key(api_key_header):
|
||||
logger.warning(f"Invalid API key attempt: ...{api_key_header[-8:] if len(api_key_header) >= 8 else 'invalid'}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid API key",
|
||||
headers={"WWW-Authenticate": "ApiKey"},
|
||||
)
|
||||
|
||||
return api_key_header
|
||||
|
||||
|
||||
async def verify_token(
|
||||
credentials: Optional[HTTPAuthorizationCredentials] = Security(security_bearer)
|
||||
) -> dict:
|
||||
"""
|
||||
Dependency to verify JWT token authentication.
|
||||
|
||||
Args:
|
||||
credentials: Bearer token from Authorization header
|
||||
|
||||
Returns:
|
||||
Decoded token payload
|
||||
|
||||
Raises:
|
||||
HTTPException: If token is missing or invalid
|
||||
"""
|
||||
if not credentials:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
token = credentials.credentials
|
||||
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError as e:
|
||||
logger.warning(f"Invalid JWT token: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication token",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
|
||||
async def require_admin_auth(
|
||||
api_key: Optional[str] = Security(api_key_header),
|
||||
bearer: Optional[HTTPAuthorizationCredentials] = Security(security_bearer)
|
||||
) -> dict:
|
||||
"""
|
||||
Dependency to require either API key or JWT token authentication for admin endpoints.
|
||||
|
||||
Checks API key first, then falls back to JWT token.
|
||||
|
||||
Args:
|
||||
api_key: Optional API key from X-API-Key header
|
||||
bearer: Optional JWT token from Authorization header
|
||||
|
||||
Returns:
|
||||
Authentication context (api_key or token payload)
|
||||
|
||||
Raises:
|
||||
HTTPException: If no valid authentication is provided
|
||||
"""
|
||||
# Try API key first
|
||||
if api_key and verify_api_key(api_key):
|
||||
return {"auth_type": "api_key", "api_key": api_key}
|
||||
|
||||
# Try JWT token
|
||||
if bearer:
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
bearer.credentials,
|
||||
settings.SECRET_KEY,
|
||||
algorithms=[settings.ALGORITHM]
|
||||
)
|
||||
return {"auth_type": "jwt", "payload": payload}
|
||||
except JWTError as e:
|
||||
logger.warning(f"Invalid JWT token: {str(e)}")
|
||||
|
||||
# No valid authentication provided
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentication required. Provide either X-API-Key header or Bearer token.",
|
||||
headers={"WWW-Authenticate": "ApiKey, Bearer"},
|
||||
)
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
|
||||
Reference in New Issue
Block a user