feat: enhance backend security and configuration

- Updated Dockerfile to improve security with a non-root user and added health checks.
- Modified docker-compose.yml to set containers as read-only, restrict ports to localhost, and implement health checks.
- Enhanced .env.example with additional environment variables for security and configuration.
- Improved FastAPI application with middleware for security headers, CORS, and body size limits.
- Refactored authentication flow in auth.py to include state validation and improved error handling.
- Added rate limiting to various endpoints to prevent abuse.
- Updated researcher and publication handling to ensure better validation and error management.
This commit is contained in:
Mireya Cueto Garrido
2026-05-08 11:19:52 +02:00
parent 96e58dbd16
commit af1b8e9956
37 changed files with 1375 additions and 282 deletions
+76
View File
@@ -0,0 +1,76 @@
"""
OAuth state anti-CSRF para el flujo de login con ORCID.
El parámetro `state` se genera en `/auth/orcid/authorize`, se guarda en una
cookie HttpOnly + SameSite=Lax con TTL corto, y se valida en el callback.
Si el `state` falta, no coincide o ha expirado, el login se rechaza.
"""
from __future__ import annotations
import hmac
import secrets
from datetime import datetime, timezone
from fastapi import HTTPException, status
from starlette.requests import Request
from starlette.responses import Response
from app.core.config import settings
_STATE_BYTES = 32
def generate_state() -> str:
return secrets.token_urlsafe(_STATE_BYTES)
def attach_state_cookie(response: Response, state: str) -> None:
"""
Persiste el `state` en una cookie segura y devuelve el valor crudo.
"""
response.set_cookie(
key=settings.ORCID_OAUTH_STATE_COOKIE,
value=state,
max_age=settings.ORCID_OAUTH_STATE_TTL_SECONDS,
secure=settings.is_production,
httponly=True,
samesite="lax",
path="/",
)
def clear_state_cookie(response: Response) -> None:
response.delete_cookie(
key=settings.ORCID_OAUTH_STATE_COOKIE,
path="/",
)
def validate_state(request: Request, received_state: str | None) -> None:
"""
Compara el state recibido en el callback con el almacenado en cookie.
Lanza 400 si no coincide o falta. Comparación en tiempo constante.
"""
if not settings.ORCID_OAUTH_STATE_ENABLED:
return
cookie_value = request.cookies.get(settings.ORCID_OAUTH_STATE_COOKIE)
if not cookie_value or not received_state:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="OAuth state missing",
)
if not hmac.compare_digest(cookie_value.encode("utf-8"), received_state.encode("utf-8")):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="OAuth state mismatch",
)
def now_ts() -> int:
return int(datetime.now(timezone.utc).timestamp())