Files
ORCID2SWORD/backend/app/security/oauth_state.py
T
Alexis dbd8bd5992 feat(ui): mejoras dashboard y entorno local con ngrok/ORCID sandbox
- Añade enlace Volver al inicio y márgenes max-w-7xl en dashboard y group
- Corrige hora de última sincronización (UTC en formatDate)
- Evita scroll horizontal en tabla de publicaciones
- Soporta backend/.env.local y compose opcional para sandbox/ngrok
- Cookie OAuth Secure en redirects HTTPS; README y .env.example
2026-05-19 12:06:54 +02:00

84 lines
2.4 KiB
Python

"""
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 urllib.parse import urlparse
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.
`Secure` debe ser True en cualquier flujo HTTPS (p. ej. ngrok en local);
no basta con `ENVIRONMENT=production`, o el navegador puede descartar
la cookie y el callback fallará con «OAuth state missing».
"""
redirect_https = urlparse(settings.ORCID_REDIRECT_URI).scheme == "https"
use_secure = settings.is_production or redirect_https
response.set_cookie(
key=settings.ORCID_OAUTH_STATE_COOKIE,
value=state,
max_age=settings.ORCID_OAUTH_STATE_TTL_SECONDS,
secure=use_secure,
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())