Saltar a contenido

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)

  1. Emitir — solo svc-identidad firma los JWT con su clave privada (RS256).
  2. Enviar — el cliente manda el token en cada petición: Authorization: Bearer <token>.
  3. Publicar — identidad expone SOLO las claves públicas en GET /.well-known/jwks.json.
  4. Cachear — cada servicio baja ese JWKS una vez (al arrancar) y valida por firma en memoria. Cero HTTP por request.
  5. Validar — un paquete común core/auth chequea firma + exp + iss
  6. aud, y saca empresa_id.
  7. Aislar — el middleware setea app.current_empresa_id una vez por request a partir del empresa_id del token; RLS filtra en la base (ADR 0002 / ADR 0012). El repository no vuelve a filtrar: no escribe WHERE empresa_id ni SET.

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 (incluido none).
  • Clave privada: solo en svc-identidad, como secreto de su pipeline.
  • Transporte: header Authorization: Bearer; nunca en query, cuerpo o logs; en web, cookie HttpOnly; 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 mismo aud. La separación entre servicios la dan empresa_id (RLS) y permisos, no el aud.
  • Contexto: usuario e empresa_id se inyectan con Depends (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 mismo aud. No se pone uno por microservicio.
  • La aud no dice qué puedes hacer, solo dónde la credencial es válida. Lo que puedes hacer lo dicen empresa_id y permisos.
  • Visitantes externos (Aylen, Siigo, QuickBooks): no son empleados → se les da una credencial de visitante con su propia aud (com.sigfa.integraciones.<nombre>) y scope limitado, 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); iss y aud son 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_ISSUER y JWT_AUDIENCE (idénticos) + JWT_JWKS_URL. La clave privada solo la tiene svc-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.

  1. Al arrancar, el servicio hace GET a JWT_JWKS_URL una vez.
  2. Guarda el JSON en memoria (no en disco, no en el repo).
  3. Con cada request lee el kid del token, busca esa llave en el JSON y verifica la firma. Cero HTTP por request.
  4. Si se rota la clave, identidad publica la nueva en el mismo endpoint y el servicio la toma (por kid) sin tocar nada.

svc-identidad sí 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ún core/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/aud de 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 aud y scope (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_usuario y permisos viajan en el JWT → cada servicio valida permisos sin consultar BD. Un root tiene 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).