Autenticación y Arquitectura Interna del Token
El SDK de Shori implementa un sistema automatizado de gestión de tokens Bearer diseñado para entornos de alta concurrencia. Abstractiza por completo las llamadas manuales a endpoints de login, renovaciones preventivas, manejo de expiración y concurrencia entre hilos (thread-safety).
Flujo Completo de Autenticación
El proceso de autenticación interactúa con los servidores de identidad de Shori siguiendo el siguiente flujo:
Encabezados Requeridos por el Endpoint /auth
Para obtener un JWT válido, el TokenProvider envía una petición HTTP POST al endpoint de autenticación.
La solicitud incluye las siguientes cabeceras:
| Header | Valor / Origen | Descripción |
|---|---|---|
Portal-ID | Configurado en ShoriClientBuilder | Identificador del portal activo. |
Tenant-ID | Configurado en ShoriClientBuilder | Identificador de la organización o tenant. |
Api-Key | Configurado en ShoriClientBuilder | Clave secreta de la aplicación. |
Estas credenciales son configuradas al construir el cliente:
```python
client = (
ShoriClientBuilder()
.environment(os.environ["SHORI_ENVIRONMENT"])
.portal_id(os.environ["SHORI_PORTAL_ID"])
.tenant_id(os.environ["SHORI_TENANT_ID"])
.api_key(os.environ["SHORI_API_KEY"])
.build()
)
```
El SDK utiliza estas credenciales internamente para gestionar el ciclo de vida del token sin que la aplicación tenga que realizar manualmente las llamadas al endpoint /auth.
Comportamiento Concurrente
Multi-Threading y Single-Flight Lock
En arquitecturas multihilo, por ejemplo:
- FastAPI con múltiples workers.
- Flask operando con threads.
ThreadPoolExecutor.- Aplicaciones que ejecutan múltiples operaciones concurrentes.
puede ocurrir que varios hilos intenten realizar una llamada a la API de Shori exactamente al mismo tiempo cuando el token:
- No existe.
- Ha expirado.
- Ha sido invalidado.
Sin un mecanismo de sincronización, cada hilo podría realizar su propia solicitud de autenticación.
Por ejemplo, con 30 hilos concurrentes:
30 hilos
|
+--> Hilo 1 --> POST /auth
+--> Hilo 2 --> POST /auth
+--> Hilo 3 --> POST /auth
+--> ...
+--> Hilo 30 --> POST /auth
Esto puede generar un patrón conocido como Thundering Herd, donde múltiples ejecuciones intentan renovar el mismo recurso simultáneamente.
El TokenProvider evita este comportamiento mediante un threading.Lock combinado con el patrón Double-Check Lock.
Cerrojo Sincronizado (threading.Lock)
Únicamente el primer hilo que detecta la falta o caducidad del token adquiere el bloqueo (Lock) e inicia la petición HTTP de autenticación.
Los demás hilos esperan hasta que el hilo que posee el bloqueo finalice la renovación.
Double-Check Lock Pattern
Cuando un hilo adquiere el Lock, vuelve a verificar la validez del token.
Esto es importante porque otro hilo podría haber renovado el token mientras el hilo actual esperaba para adquirir el bloqueo.
Si el token ya fue renovado, el hilo simplemente reutiliza el token existente y no realiza otra llamada a /auth.
Flujo Concurrente
El resultado es que, ante múltiples hilos concurrentes, la renovación del token se realiza una sola vez y los demás hilos reutilizan el token obtenido.
Implementación Conceptual
El mecanismo interno puede representarse conceptualmente mediante la siguiente implementación:
import threading
class TokenProvider:
def __init__(self, ...):
self._token: str | None = None
self._expires_at: float = 0
self._lock = threading.Lock()
def get_token(self) -> str:
# Check 1:
# ¿Tengo token válido en memoria?
# Acceso rápido sin adquirir el Lock.
if self._is_token_valid():
return self._token
# Esperar/adquirir Lock si no hay token válido.
with self._lock:
# Check 2 (Double-Check):
# ¿Otro hilo ya renovó el token mientras yo esperaba?
if self._is_token_valid():
return self._token
# Solo un hilo ejecuta esta operación.
self._token, self._expires_at = (
self._fetch_new_token_from_api()
)
return self._token
El punto fundamental del patrón es la segunda validación:
with self._lock:
if self._is_token_valid():
return self._token
Esta comprobación evita que cada hilo que estaba esperando el Lock vuelva a realizar una solicitud de autenticación.
Escenarios de Expiración y Manejo de Errores
1. Renovación Preventiva
Margen de Tolerancia
Para prevenir problemas de latencia de red donde un token expira en tránsito hacia los servidores de Shori, el SDK descuenta un margen de seguridad por defecto al tiempo de expiración reportado por el JWT.
Buffer Margin = 60 segundos
Por ejemplo, si un token vence a las 12:00:00, el SDK lo considerará inválido a partir de las 11:59:00.
De esta forma, la renovación se realiza antes de la expiración real del token en el servidor.
Flujo de Renovación Preventiva
El objetivo es reducir errores provocados por:
- Latencia de red.
- Tiempo de procesamiento de la petición.
- Diferencias temporales entre cliente y servidor.
- Expiración del token mientras una petición se encuentra en tránsito.
2. Manejo de Respuestas HTTP 401 Unauthorized
Si por alguna desincronización de reloj, revocación remota u otra condición una petición regular del negocio falla con estado HTTP 401 Unauthorized, el HttpExecutor activa un flujo de recuperación automático.
Detección del 401
El cliente intercepta la respuesta HTTP:
HTTP 401 Unauthorized
y analiza el estado del token utilizado en la petición original.
Validación de Token Reciente
Antes de solicitar una nueva autenticación, el cliente verifica si otro hilo ya renovó el token mientras esta petición particular estaba en tránsito.
Por ejemplo:
En este caso, el hilo A no necesita realizar otra autenticación porque el token ya fue actualizado por el hilo B.
Invalidación Forzada
Si el token no cambió, el SDK considera que el token actual puede estar inválido.
El flujo es:
El SDK permite un único reintento automático.
Propagación del Error
Si la petición vuelve a recibir 401 Unauthorized después del reintento:
En este escenario, el SDK eleva una:
AuthenticationException
hacia la aplicación cliente.
Esto evita realizar reintentos indefinidos ante un problema real de autenticación.
Ciclo Completo del Token
El ciclo de vida simplificado del token puede representarse de la siguiente manera:
Ejemplo de Prueba de Concurrencia
El siguiente script permite validar el comportamiento del TokenProvider cuando múltiples hilos ejecutan operaciones concurrentes utilizando el mismo cliente.
import os
import logging
from concurrent.futures import ThreadPoolExecutor
from shori_sdk import (
ShoriClientBuilder,
CaseFilter,
)
logging.basicConfig(level=logging.DEBUG)
client = (
ShoriClientBuilder()
.environment(os.environ["SHORI_ENVIRONMENT"])
.portal_id(os.environ["SHORI_PORTAL_ID"])
.tenant_id(os.environ["SHORI_TENANT_ID"])
.api_key(os.environ["SHORI_API_KEY"])
.max_workers(10)
.build()
)
def consulta_paralela(id_hilo: int):
result = client.caso().search(
CaseFilter.builder()
.page_size(1)
.build()
)
return (
f"Hilo {id_hilo}: "
f"OK (Total casos: {result.count})"
)
try:
# Ejecutar 30 operaciones concurrentes.
with ThreadPoolExecutor(max_workers=30) as pool:
resultados = list(
pool.map(
consulta_paralela,
range(30)
)
)
for resultado in resultados:
print(resultado)
finally:
client.close()
Resultado Esperado
Si el token no se encuentra inicializado al momento de iniciar las 30 operaciones, se espera que únicamente uno de los hilos realice la autenticación.
Conceptualmente:
En los logs se debería observar conceptualmente:
POST /auth
POST /caso/search
POST /caso/search
POST /caso/search
...
y no:
POST /auth
POST /auth
POST /auth
...
La cantidad exacta y el orden de los logs de negocio pueden variar debido a la planificación de los hilos y al procesamiento concurrente.
Resumen del Diseño
El mecanismo de autenticación del SDK se basa principalmente en cuatro principios:
| Mecanismo | Propósito |
|---|---|
| Token en memoria | Evitar autenticaciones innecesarias para cada petición. |
| Renovación preventiva | Renovar el token antes de su expiración real. |
threading.Lock + Double-Check | Evitar múltiples renovaciones simultáneas en escenarios concurrentes. |
Retry ante 401 | Recuperarse automáticamente ante tokens revocados, expirados o desincronizados. |
El flujo general puede resumirse de la siguiente manera:
De esta manera, la aplicación cliente no necesita gestionar directamente el ciclo de vida del token. El SDK centraliza la autenticación, renovación, sincronización entre hilos y recuperación ante errores de autenticación.