dbd8bd5992
- 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
84 lines
2.4 KiB
Python
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())
|