list_users/stats llamaban al Hub (users-with-info) con el Bearer de la app, que con el patrón SIWEB es la sesión local (HS256) → el Hub la rechaza (401) → 0 usuarios. Nuevo core.hub_token.get_hub_access_token: toma el token KC de la sesión (valkey vía crm_sid) y lo refresca si está por expirar. list_users y stats lo usan para el Hub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
"""
|
|
Obtención de un access token de Keycloak VÁLIDO para llamar a la API del Hub.
|
|
|
|
Con el patrón de sesión local (SIWEB) el Bearer de la app es un JWT propio (HS256)
|
|
que el Hub NO entiende. Para las llamadas server→Hub se usa el token KC guardado en
|
|
la sesión (valkey, vía cookie crm_sid), refrescándolo si está por expirar.
|
|
"""
|
|
|
|
import logging
|
|
import time
|
|
from typing import Optional
|
|
|
|
import httpx
|
|
from jose import jwt
|
|
|
|
from core.config import settings
|
|
from core import session_store
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _kc_exp_ok(token: str, leeway_seconds: int = 30) -> bool:
|
|
"""True si el token KC no está expirado (con margen)."""
|
|
try:
|
|
claims = jwt.get_unverified_claims(token)
|
|
exp = claims.get("exp")
|
|
return isinstance(exp, (int, float)) and (int(exp) - int(time.time())) > leeway_seconds
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
async def get_hub_access_token(request) -> Optional[str]:
|
|
"""
|
|
Devuelve un access token KC válido tomado de la sesión (valkey vía crm_sid),
|
|
refrescándolo contra el Hub si está por expirar. None si no hay sesión.
|
|
Best-effort: si el refresh falla, devuelve el token guardado (puede estar vencido).
|
|
"""
|
|
sid = request.cookies.get("crm_sid") if request is not None else None
|
|
if not sid:
|
|
return None
|
|
sess = session_store.get_session(sid)
|
|
if not sess:
|
|
return None
|
|
|
|
access = sess.get("access_token")
|
|
refresh = sess.get("refresh_token")
|
|
|
|
if access and _kc_exp_ok(access):
|
|
return access
|
|
|
|
if refresh:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8.0) as client:
|
|
r = await client.post(
|
|
f"{settings.HUB_URL}api/v1/auth/refresh",
|
|
json={"refresh_token": refresh},
|
|
)
|
|
if r.status_code == 200:
|
|
data = r.json()
|
|
new_access = data.get("access_token") or access
|
|
session_store.update_session_tokens(sid, new_access, data.get("refresh_token") or refresh)
|
|
return new_access
|
|
logger.info("get_hub_access_token: Hub refresh devolvió %s", r.status_code)
|
|
except Exception as exc:
|
|
logger.warning("get_hub_access_token: refresh falló: %s", exc)
|
|
|
|
return access
|