feat: Implement token issue detection and enhance license validation middleware; add tests for token issue message detection
This commit is contained in:
@@ -11,6 +11,30 @@ from .security import get_tenant_from_token, verify_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_text(value: str | None) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
return str(value).strip().lower()
|
||||
|
||||
|
||||
def _is_token_issue_message(*values: str | None) -> bool:
|
||||
text = " ".join(_normalize_text(v) for v in values if v)
|
||||
if not text:
|
||||
return False
|
||||
|
||||
token_markers = ["token", "jwt", "bearer", "access"]
|
||||
invalid_markers = [
|
||||
"invalido", "inválido", "invalid", "not valid", "malformed", "signature", "unauthorized"
|
||||
]
|
||||
expired_markers = ["expirado", "expirada", "expired", "has expired", "caducado", "vencido"]
|
||||
|
||||
has_token_context = any(marker in text for marker in token_markers)
|
||||
has_invalid_marker = any(marker in text for marker in invalid_markers)
|
||||
has_expired_marker = any(marker in text for marker in expired_markers)
|
||||
|
||||
return has_expired_marker or (has_token_context and has_invalid_marker)
|
||||
|
||||
class TenantMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware original para extraer tenant_id y user_info del token.
|
||||
@@ -94,12 +118,22 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
token = auth_header.split(" ")[1]
|
||||
|
||||
tenant_override = request.headers.get("X-Tenant-Override")
|
||||
if not tenant_override:
|
||||
# Fallback para flujos SSO cuando el override no viaja en header.
|
||||
tenant_override = request.cookies.get("sso_tenant_id") or request.cookies.get("sso_tenant_pub")
|
||||
|
||||
hub_headers = {"Authorization": f"Bearer {token}"}
|
||||
if tenant_override:
|
||||
hub_headers["X-Tenant-Override"] = str(tenant_override)
|
||||
logger.info("[license] tenant override propagated to Hub: %s", tenant_override)
|
||||
|
||||
try:
|
||||
# Validación contra el Hub Central
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(
|
||||
f"{settings.HUB_URL}api/v1/auth/verify-license",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
headers=hub_headers
|
||||
)
|
||||
|
||||
logger.info(f"🔑 verify-license → status={response.status_code} body={response.text[:300]}")
|
||||
@@ -114,7 +148,31 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
# Escenario 1: sin licencia asignada o licencia inactiva
|
||||
if not data.get("valid", False):
|
||||
message = data.get("message", "Sin licencia asignada para este tenant")
|
||||
logger.warning(f"🚫 License invalid for tenant: {data.get('tenant_slug')} — {message}")
|
||||
detail = data.get("detail")
|
||||
reason = data.get("reason")
|
||||
# Si el Hub reporta token inválido/expirado, devolver 401 para que
|
||||
# el frontend dispare el auto-refresh (solo se activa con 401/403, no 402).
|
||||
if _is_token_issue_message(message, detail, reason):
|
||||
logger.warning(
|
||||
"[license] token expirado/invalido detectado por verify-license; devolviendo 401 para silent refresh | message=%s detail=%s reason=%s",
|
||||
message,
|
||||
detail,
|
||||
reason,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": message,
|
||||
"status_code": 401,
|
||||
}
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
"[license] licencia invalida para tenant=%s | message=%s",
|
||||
data.get("tenant_slug"),
|
||||
message,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=402,
|
||||
content={
|
||||
@@ -132,7 +190,11 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
if expires_at.tzinfo is None:
|
||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
||||
if expires_at < datetime.now(timezone.utc):
|
||||
logger.warning(f"🚫 License expired for tenant: {data.get('tenant_slug')} — expired at {expires_at_str}")
|
||||
logger.warning(
|
||||
"[license] licencia expirada para tenant=%s | expires_at=%s",
|
||||
data.get("tenant_slug"),
|
||||
expires_at_str,
|
||||
)
|
||||
return JSONResponse(
|
||||
status_code=402,
|
||||
content={
|
||||
@@ -146,6 +208,17 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
request.state.license_info = data
|
||||
return await call_next(request) # <--- Único camino al éxito
|
||||
|
||||
elif response.status_code == 401:
|
||||
logger.warning("[license] Hub verify-license devolvio 401 (token invalido/expirado)")
|
||||
return JSONResponse(
|
||||
status_code=401,
|
||||
content={
|
||||
"error": "TOKEN_EXPIRED",
|
||||
"message": "Token inválido o expirado.",
|
||||
"status_code": 401,
|
||||
}
|
||||
)
|
||||
|
||||
elif response.status_code == 403:
|
||||
return JSONResponse(
|
||||
|
||||
Reference in New Issue
Block a user