Desacopla la sesión de la app del access token de Keycloak (60s). Tras el SSO/ refresh, el backend emite un JWT de sesión local (HS256) con vida por inactividad (SESSION_IDLE_MINUTES, cap SESSION_MAX_HOURS) y guarda los tokens KC en valkey. La app usa esa sesión local como bearer (cookie access_token); el token KC vive en kc_access_token solo para llamadas directas al Hub. Así el refresh KC contra el Hub solo se intenta al expirar la sesión local, no cada ~60s → se elimina el bucle causado por el bug "Token is not active" del relay. Se RESPETA la revocación central de Keycloak: si el Hub rechaza el refresh, la sesión termina (401). Sin re-emisión de fallback → sin bypass de revocación. Todo detrás de SESSION_STORE_ENABLED (default False = comportamiento idéntico). Backend: core/local_session.py, core/session_store.py (valkey), verify_token acepta la sesión local, refresh la emite/actualiza; DTOs con session_token/session_id; test unitario de local_session. Frontend: access_token=sesión local, kc_access_token para el Hub; sso/refresh/ silent-refresh/switch-tenant y pantallas de workspace cableadas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
75 lines
2.2 KiB
Python
75 lines
2.2 KiB
Python
"""
|
|
Pruebas de la sesión local del CRM (patrón SIWEB) — core.local_session.
|
|
|
|
Lógica pura (firma HS256 + claims); no requiere BD ni valkey.
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from jose import jwt
|
|
|
|
from core.config import settings
|
|
from core.local_session import mint_session_token, verify_session_token, session_start_of
|
|
|
|
|
|
def _now() -> int:
|
|
return int(datetime.now(timezone.utc).timestamp())
|
|
|
|
|
|
def test_round_trip_conserva_identidad():
|
|
claims = {
|
|
"sub": "kc-user-123",
|
|
"email": "user@example.com",
|
|
"tenant_id": 11,
|
|
"tenant_slug": "aduanasoft",
|
|
"is_hub_admin": True,
|
|
}
|
|
token = mint_session_token(claims)
|
|
out = verify_session_token(token)
|
|
|
|
assert out is not None
|
|
assert out["sub"] == "kc-user-123"
|
|
assert out["tenant_id"] == 11
|
|
assert out["tenant_slug"] == "aduanasoft"
|
|
assert out["is_hub_admin"] is True
|
|
assert out["source"] == "local"
|
|
assert out["crm_session"] is True
|
|
assert isinstance(out["sst"], int)
|
|
|
|
|
|
def test_cap_absoluto_rechaza_sesion_vieja():
|
|
# session_start más allá del cap absoluto → verify debe rechazar aunque no expiró por idle.
|
|
old_start = _now() - (settings.SESSION_MAX_HOURS * 3600 + 120)
|
|
token = mint_session_token({"sub": "x"}, session_start=old_start)
|
|
assert verify_session_token(token) is None
|
|
|
|
|
|
def test_preserva_session_start():
|
|
start = _now() - 60
|
|
token = mint_session_token({"sub": "x"}, session_start=start)
|
|
out = verify_session_token(token)
|
|
assert out is not None
|
|
assert session_start_of(out) == start
|
|
|
|
|
|
def test_firma_alterada_se_rechaza():
|
|
token = mint_session_token({"sub": "x"})
|
|
# Alterar el último carácter de la firma invalida el token.
|
|
tampered = token[:-1] + ("A" if token[-1] != "A" else "B")
|
|
assert verify_session_token(tampered) is None
|
|
|
|
|
|
def test_token_no_crm_se_rechaza():
|
|
# Un HS256 válido pero SIN los marcadores de sesión local no debe aceptarse.
|
|
other = jwt.encode(
|
|
{"sub": "x", "exp": _now() + 600},
|
|
settings.SECRET_KEY,
|
|
algorithm="HS256",
|
|
)
|
|
assert verify_session_token(other) is None
|
|
|
|
|
|
def test_token_basura_se_rechaza():
|
|
assert verify_session_token("no-es-un-jwt") is None
|
|
assert verify_session_token("") is None
|