0006. JWT: cómo se autentican los 14 servicios¶
Estado: Propuesto Fecha: 2026-08-28 Autor: Duval Alcivar
Contexto¶
svc-identidad emite los tokens; los otros 13 servicios validan sin
duplicar lógica ni exponer secretos. La empresa viaja en el token
(ADR 0002), nunca como parámetro del cliente.
Cómo se usa el JWT (6 pasos)¶
- Emitir — solo
svc-identidadfirma los JWT con su clave privada (RS256). - Enviar — el cliente manda el token en cada petición:
Authorization: Bearer <token>. - Publicar — identidad expone SOLO las claves públicas en
GET /.well-known/jwks.json. - Cachear — cada servicio baja ese JWKS una vez (al arrancar) y valida por firma en memoria. Cero HTTP por request.
- Validar — un paquete común
core/authchequea firma +exp+iss aud, y sacaempresa_id.- Aislar — el middleware setea
app.current_empresa_iduna vez por request a partir delempresa_iddel token; RLS filtra en la base (ADR 0002 / ADR 0012). El repository no vuelve a filtrar: no escribeWHERE empresa_idniSET.
Conexión de las claves entre servicios¶
flowchart LR
subgraph ID["svc-identidad"]
PRIV["clave PRIVADA<br>(secreto del pipeline)"]
JWK["publica solo públicas: /.well-known/jwks.json"]
SIG["firma el token"]
end
subgraph SVC["los otros 13 servicios"]
CACHE["bajan el JWKS 1 vez<br>y lo cachean"]
VAL["validan firma en memoria<br>sin consultar a nadie"]
end
PRIV --> SIG
JWK -->|"claves públicas"| CACHE --> VAL
classDef priv fill:#fde2e2,stroke:#c0392b;
classDef pub fill:#d5f0fb,stroke:#2471a3;
class PRIV priv;
class JWK,CACHE,VAL pub;
Regla: la privada existe solo en svc-identidad. A los demás solo les
llega el JWKS público (inocuo si se filtra). Rotación: identidad publica la
nueva y mantiene la vieja unos días por el kid.
Decisión (normas)¶
- Firma: RS256. Se rechaza cualquier otro
alg(incluidonone). - Clave privada: solo en
svc-identidad, como secreto de su pipeline. - Transporte: header
Authorization: Bearer; nunca en query, cuerpo o logs; en web, cookieHttpOnly; Secure; SameSite=Lax. - Claims (mínimos):
| Claim | Qué es |
|---|---|
sub |
id del usuario (UUID) |
empresa_id |
empresa activa (BIGINT) → alimenta RLS (ADR 0002) |
tipo_usuario |
usuario | root | externo (cat_tipos_usuario, ADR 0007/0022 de svc-identidad) |
permisos |
array de códigos — permisos del usuario en la empresa activa |
exp |
expiración. Obligatorio; token sin exp se rechaza |
iss / aud |
emisor y audiencia fijos de plataforma; el validador exige ambos |
jti |
id único del token |
- Sesión: access de 15–30 min (stateless) + refresh opaco de 7–30 días con rotación y revocación (logout efectivo).
- Audiencia: fija para toda la plataforma (
com.sigfa.plataforma, ADR 0020): todos los servicios validan el mismoaud. La separación entre servicios la danempresa_id(RLS) ypermisos, no elaud. - Contexto:
usuarioeempresa_idse inyectan conDepends(ADR 0005); la empresa nunca se toma de un parámetro del request.
Cómo entenderlo (edificio y departamentos)¶
La plataforma es un edificio y cada microservicio es un departamento
(ERP, CRM, RRHH, pedidos…). Al entrar, recepción (que es svc-identidad) te da
una credencial (el token) con:
| Dato | Qué significa | Valor |
|---|---|---|
iss (issuer) |
el sello de quién emitió la credencial | com.sigfa.identidad |
aud (audience) |
para qué edificio es válida | com.sigfa.plataforma |
scope / permisos |
qué puertas abre dentro del edificio | según el usuario |
- Un solo edificio, una sola
aud. Como todos los departamentos están dentro del mismo edificio (la plataforma), todos aceptan el mismoaud. No se pone uno por microservicio. - La
audno dice qué puedes hacer, solo dónde la credencial es válida. Lo que puedes hacer lo dicenempresa_idypermisos. - Visitantes externos (Aylen, Siigo, QuickBooks): no son empleados → se les
da una credencial de visitante con su propia
aud(com.sigfa.integraciones.<nombre>) yscopelimitado, para que no entren a donde no deben. (Ver el ADR de integraciones externas.) com.es solo una convención de nombre (tipo dominio de internet);issyaudson etiquetas de texto que se pueden cambiar siempre que coincidan entre quien firma (svc-identidad) y quien valida (los demás).
Cada microservicio pone lo mismo en su
.env:JWT_ISSUERyJWT_AUDIENCE(idénticos) +JWT_JWKS_URL. La clave privada solo la tienesvc-identidad.
Cómo se distribuye la clave pública (JWKS)¶
El endpoint GET /.well-known/jwks.json devuelve un JSON con la clave pública
en formato JWK (campos n, e, kid, alg, kty). Ese JSON no se
copia ni se pega en ningún archivo: cada servicio lo consume por HTTP y lo
mantiene en memoria.
- Al arrancar, el servicio hace
GETaJWT_JWKS_URLuna vez. - Guarda el JSON en memoria (no en disco, no en el repo).
- Con cada request lee el
kiddel token, busca esa llave en el JSON y verifica la firma. Cero HTTP por request. - Si se rota la clave, identidad publica la nueva en el mismo endpoint y el
servicio la toma (por
kid) sin tocar nada.
svc-identidadsí puede tener su pública en un.pem(JWT_PUBLIC_KEY_PATH=./keys/public.pem) porque es dueño de la privada y la deriva. Los demás no la tienen: la bajan del JWKS. Ese código vivirá en el paquete comúncore/auth(aún no implementado).
Qué configura cada servicio (.env)¶
| Variable | svc-identidad (emisor) |
Servicio interno (svc-rrhh, svc-produccion, …) |
Integración externa |
|---|---|---|---|
JWT_PRIVATE_KEY_PATH |
✅ sí | ❌ no | ❌ no |
JWT_PUBLIC_KEY_PATH |
✅ (o se deriva) | ❌ no | ❌ no |
JWT_CURRENT_KID |
✅ sí | ❌ no | ❌ no |
JWT_ISSUER |
com.sigfa.identidad |
com.sigfa.identidad |
com.sigfa.identidad |
JWT_AUDIENCE |
com.sigfa.plataforma |
com.sigfa.plataforma (el mismo) |
su propia aud (ej. com.sigfa.integraciones.aylen) |
JWT_JWKS_URL |
❌ (es el que lo publica) | ✅ https://svc-identidad/.well-known/jwks.json |
✅ (valida sus propios tokens) |
- Claves: la privada solo en
svc-identidad; la pública se obtiene del JWKS, nunca se pega. iss/audde tokens de usuario: idénticos en todos los servicios internos (com.sigfa.identidad/com.sigfa.plataforma).- Externo: no usa tokens de usuario; se le emite una credencial con su
propia
audyscope(ver ADR de integraciones, pendiente).
Anexo: plantillas .env.example¶
1) svc-identidad (emisor / firma):
# Firma JWT: única clave privada de la plataforma
JWT_PRIVATE_KEY_PATH=./keys/private.pem
JWT_PUBLIC_KEY_PATH=./keys/public.pem
JWT_CURRENT_KID=svc-identidad-1
JWT_ISSUER=com.sigfa.identidad
JWT_AUDIENCE=com.sigfa.plataforma
2) Servicio interno (svc-rrhh, svc-produccion, svc-pedidos, …):
# NO define claves: valida contra el JWKS de identidad
JWT_ISSUER=com.sigfa.identidad
JWT_AUDIENCE=com.sigfa.plataforma
JWT_JWKS_URL=https://svc-identidad/.well-known/jwks.json
3) Integración externa (Aylen, Siigo, QuickBooks) — aud propia + scope:
No lleva un .env con las variables de arriba: no firma ni valida tokens de
la plataforma. El equipo de identidad le entrega credenciales de cliente
(client_id / client_secret) y con ellas pide su token. La aud y los
scope de esa integración se registran en svc-identidad, no en el .env
del externo.
# (referencia, flujo de integración pendiente de implementar)
SIGFA_CLIENT_ID=<client-id-de-la-integracion>
SIGFA_CLIENT_SECRET=<secreto>
SIGFA_TOKEN_URL=https://svc-identidad/api/v1/auth/token
Alternativas descartadas¶
- HS256 (secreto compartido) — cualquiera comprometido falsifica tokens.
- Introspection por request — latencia y punto único de fallo.
- Access largo sin refresh — logout no efectivo.
Consecuencias¶
empresa_id(BIGINT) y el usuario salen siempre del token ya verificado: el aislamiento del ADR 0002 es doble (código + RLS).tipo_usuarioypermisosviajan en el JWT → cada servicio valida permisos sin consultar BD. Unroottiene acceso implícito a todo (ADR 0007 de svc-identidad).- Ningún servicio emite tokens; si alguno lo hace, el PR se rechaza.
- Las tablas de soporte (
trx_refresh_tokens,log_sesiones) están definidas en el ADR 0005 de svc-identidad (modelo de datos: usuarios y sesiones).