Saltar al contenido principal

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:

HeaderValor / OrigenDescripción
Portal-IDConfigurado en ShoriClientBuilderIdentificador del portal activo.
Tenant-IDConfigurado en ShoriClientBuilderIdentificador de la organización o tenant.
Api-KeyConfigurado en ShoriClientBuilderClave 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:

MecanismoPropósito
Token en memoriaEvitar autenticaciones innecesarias para cada petición.
Renovación preventivaRenovar el token antes de su expiración real.
threading.Lock + Double-CheckEvitar múltiples renovaciones simultáneas en escenarios concurrentes.
Retry ante 401Recuperarse 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.