Saltar a contenido

0011. Login: firma JWT y gestión de claves

Estado: Propuesto Fecha: 2026-08-31 Autor: Duval Alcivar Módulo(s) afectado(s): svc-identidad, core/auth (compartido)

Actualización: el punto 3a (audience por servicio) fue reemplazado por la audiencia fija de plataforma del ADR 0020. El resto de este ADR (firma RS256, clave privada/JWKS, claims, refresh) sigue vigente.

Contexto

El ADR 0006 transversal establece que svc-identidad es el único que firma JWT (RS256) y que la clave privada vive exclusivamente dentro de este servicio. El ADR 0001 de identidad define los dominios (usuarios, empresas, sucursales, permisos) y su estructura interna. El ADR 0010 define el flujo de pantallas del login. Este ADR define la parte técnica: qué recibe, qué valida, qué firma, dónde guarda la clave, cómo se expone el JWKS y qué claims salen en el token.

Decisión

El login tiene dos pasos (ver ADR 0010 — flujo de acceso y panel): primero se validan las credenciales y se listan las empresas del usuario; después el usuario elige la empresa y se firma el token. Este ADR define la firma y las claves; el detalle de pantallas está en el ADR 0010.

1. Endpoints de login

POST /api/v1/auth/login            — paso 1: valida credenciales, devuelve empresas
POST /api/v1/auth/login/empresa    — paso 2: recibe la empresa elegida, firma el token

Paso 1 — Request:

Campo Tipo Obligatorio Descripción
username string nombre de usuario
password string clave en texto plano (nunca se almacena así)

Paso 1 — Response 200:

{
  "pre_token": "<token temporal de 5 min, solo para elegir empresa>",
  "empresas": [
    { "id": 1, "nombre": "Sigfa Alimentos S.A.", "nombre_comercial": "SIGFA" }
  ]
}

Paso 2 — Request:

Campo Tipo Obligatorio Descripción
pre_token string el token temporal del paso 1
empresa_id BIGINT empresa elegida del selector (catálogo pequeño, ADR 0011)

Paso 2 — Response 200:

{
  "access_token": "<JWT firmado>",
  "refresh_token": "<token opaco>",
  "token_type": "Bearer",
  "expires_in": 1800
}

Response 401: credenciales inválidas. Response 403: usuario deshabilitado o sin acceso a la empresa solicitada.

2. Qué se valida antes de firmar

  1. Credencialesusername + password verificados con Argon2id contra la tabla mae_usuarios (hash, nunca la clave). Si falla, el error es genérico ("credenciales inválidas") para no revelar si el username existe.
  2. Empresas del usuario — tras validar credenciales se listan las empresas activas del usuario (rel_usuario_empresa + mae_empresas.is_activo). Si no tiene ninguna, 403 ("sin empresas asignadas"). Las empresas solo se muestran después de validar la contraseña: nunca se exponen antes de autenticar.
  3. Empresa elegida — en el paso 2, el empresa_id debe estar en la lista del paso 1 (se revalida la relación activa). No se valida sucursal en el login: las sucursales no definen acceso inicial.
  4. Estado — el usuario debe estar is_activo = true. Si no, 403.
  5. Contraseña temporal — si password_temp = true, el paso 2 responde exitoso pero marca que debe renovarse (el cliente lo envía al flujo de cambio de contraseña); no se emite el access hasta renovarla.

3. Claims del access token

Se firman con RS256 (ADR 0006) y contienen:

Claim Valor Descripción
sub UUID del usuario identificador único
empresa_id BIGINT de la empresa elegida alimenta RLS (ADR 0002 global, ADR 0011)
username username del usuario útil para display en frontend
tipo_usuario usuario | root root ⇒ acceso implícito a todo (ADR 0007)
permisos array de códigos permisos del usuario en la empresa activa (rel_usuario_permiso + acciones de rel_usuario_menu_accion); vacío/omitido si tipo_usuario = root (ADR 0007/0014/0015)
iss com.sigfa.identidad emisor fijo de plataforma (ADR 0020); el validador lo exige
aud com.sigfa.plataforma audiencia fija de plataforma (ADR 0020), igual para todos los servicios
exp now + 30 min expiración obligatoria
iat now timestamp de emisión
jti UUID único id del token; para revocación y auditoría

Las sucursales NO viajan en el token. La sucursal no es un dato de acceso inicial: se usa en las consultas de negocio para ubicar bodegas/almacenes o filtrar lo que se muestra. El alcance por sucursal se resuelve en cada consulta (rel_usuario_sucursal, ADR 0005), no en el JWT.

3a. Audience fija de plataforma. El aud no es por servicio: es un único valor de plataforma, com.sigfa.plataforma, fijado en el ADR 0020. svc-identidad lo pone al firmar y todos los servicios validan ese mismo valor (junto con iss). No se usa aud comodín (*) ni un aud distinto por microservicio; la separación entre servicios la dan empresa_id (RLS) y permisos. (Este punto reemplaza la versión anterior "audience por servicio".)

4. Refresh token

  • Formato: token opaco (no JWT), 64 bytes aleatorios en hex.
  • Almacenamiento: tabla trx_refresh_tokens con token_hash (SHA-256 del valor crudo), usuario_id (UUID), empresa_id (BIGINT, ADR 0011), expiracion, revocado, created_at. Esquema completo en el ADR 0005 de svc-identidad (usuarios y sesiones).
  • Rotación: cada uso del refresh genera uno nuevo y revoca el anterior. Si se usa un refresh ya revocado (replay), se revocan todos los refresh del usuario (fuga sospechosa).
  • Expiración: 30 días por defecto; configurable por variable de entorno. Los TTLs concretos de access/refresh/pre_token y los parámetros de Argon2id están en el ADR 0020; la purga de los tokens expirados/revocados está en el ADR 0021.

5. Clave privada y firma JWT

5a. Dónde vive la clave:

Entorno Ubicación
Desarrollo archivo .envJWT_PRIVATE_KEY_PATH=./keys/private.pem (nunca commiteado; .gitignore)
CI/Testing secret del pipeline inyectado como variable de entorno
Producción secreto del orquestador (Kubernetes Secret / Vault / secret manager del proveedor) montado como archivo o variable de entorno

5b. Formato de la clave:

  • RSA 2048 bits, formato PEM (RSA PRIVATE KEY).
  • Se genera una vez con openssl genrsa -out private.pem 2048.
  • La pública se deriva y publica en GET /.well-known/jwks.json.

5c. Rotación:

  • Cada clave tiene un kid (key id) único, incluido en el header del JWT.
  • Para rotar: generar la nueva, publicar ambas en JWKS (la vieja + la nueva) durante una ventana de gracia (48–72 h), y luego eliminar la vieja. La operativa completa (runbook, estados, revocación de emergencia) está en el ADR 0017.
  • El kid actual se lee de variable de entorno: JWT_CURRENT_KID.

5d. Firma en código:

# core/security.py
from jose import jwt
from argon2 import PasswordHasher

ph = PasswordHasher()

def sign_token(payload: dict, private_key: str, kid: str) -> str:
    headers = {"alg": "RS256", "kid": kid}
    return jwt.encode(payload, private_key, algorithm="RS256", headers=headers)

def hash_password(password: str) -> str:
    return ph.hash(password)

def verify_password(stored_hash: str, password: str) -> bool:
    try:
        return ph.verify(stored_hash, password)
    except Exception:
        return False

6. JWKS público

GET /.well-known/jwks.json

Retorna el JWK Set con todas las claves públicas activas. Los demás servicios lo consumen una vez al arrancar y lo cachean (ADR 0006).

7. Flujo técnico del login

Este ADR cubre el lado técnico: qué firma y cómo. El detalle de pantallas (login, modal de empresa, panel) está en el ADR 0010. Aquí solo se muestra qué pasa con el token al final del paso 2 y cómo lo validan los demás servicios:

sequenceDiagram
    participant U as Frontend (React)
    participant I as svc-identidad
    participant K as Clave privada (secreto)
    participant S as Otro servicio (ERP/CRM)

    U->>I: POST /api/v1/auth/login/empresa {pre_token, empresa_id}
    I->>I: valida empresa y genera payload (sub, empresa_id, tipo_usuario, permisos)
    I->>K: firma token con RS256 + kid
    K-->>I: JWT firmado
    I-->>U: {access_token, refresh_token, expires_in}

    U->>S: GET /api/v1/datos (Authorization: Bearer)
    S->>S: valida firma contra JWKS cacheado
    S-->>U: 200 / 401

El flujo completo con sus pantallas y la decisión modal/directo está en el ADR 0010. Este diagrama solo ilustra la firma y la validación.

8. Endpoints de sesión

Endpoint Método Descripción
/api/v1/auth/login POST paso 1: valida credenciales, devuelve empresas del usuario
/api/v1/auth/login/empresa POST paso 2: recibe empresa elegida, emite access + refresh
/api/v1/auth/refresh POST recibe refresh_token, emite nuevo access + refresh, revoca el viejo
/api/v1/auth/logout POST revoca el refresh token actual (requiere Bearer)
/api/v1/auth/logout-all POST revoca todos los refresh del usuario (requiere Bearer)

9. Librerías utilizadas

Área Librería Versión mín. Uso
JWT (firma y verificación) python-jose[cryptography] 3.3+ firma RS256, decodificación, manejo de JWKS
Hashing de contraseñas argon2-cffi 23.1+ Argon2id para hash y verificación de claves
Validación de schemas pydantic 2.5+ request/response del login
Hash de refresh tokens hashlib (stdlib) SHA-256 del token opaco antes de persistir

No se usa bcrypt — Argon2id es el ganador del Password Hashing Competition (PHC) y ofrece mejor resistencia a ataques GPU/ASIC con configuración de memoria, tiempo y paralelismo.

Alternativas descartadas

  • HS256 (secreto compartido) — descartado: cualquier servicio comprometido falsifica tokens (ADR 0006).
  • Clave privada en el código fuente — descartado: filtración en repos; la clave vive en secretos del pipeline/orquestador.
  • Access token sin refresh (solo cookies largas) — descartado: no permite logout efectivo ni rotación de tokens; un token largo es un token comprometido por más tiempo.
  • Introspection por request a identidad en cada request — descartado: latencia y punto único de fallo (ADR 0006).

Consecuencias

  • La clave privada nunca toca el código fuente ni los repositorios; vive en secretos del despliegue y se monta como variable de entorno o archivo.
  • El empresa_id viaja dentro del token firmado; ningún servicio lo acepta como parámetro libre (ADR 0002 global, ADR 0006).
  • La rotación de claves y la revocación de refresh viven en un solo lugar (svc-identidad); los demás servicios solo validan la firma.
  • Un refresh replay (token reusado) dispara revocación completa de la sesión del usuario — detección temprana de compromiso.
  • El kid en el header del JWT permite rotar claves sin interrumpir servicios que aún tienen la clave vieja cacheada.

Orden de lectura (cómo encajan los ADRs de identidad)

Estos ADRs no se pisan, cada uno cubre una capa del mismo flujo. Se leen así:

# ADR Qué aporta (una sola cosa)
0001 · 0002 Concepto y stack Qué es svc-identidad y con qué herramientas se construye.
0003 · 0005 Catálogos y modelos Catálogos generales, empresas y usuarios.
0006 · 0007 Navegación y permisos Aplicaciones/módulos/menú y tipos/permisos.
0008 · 0009 Gestión Alta de empresa; gestión de usuarios y asignación de permisos.
0010 Flujo de acceso y panel El flujo con pantallas: login → modal de empresa → panel → módulo. "Qué hace el usuario."
0011 (este) Login: firma JWT y claves La parte técnica de seguridad: RS256, clave privada/JWKS, claims, refresh. "Cómo se firma."
0012 Menú y permisos Qué se muestra al usuario según sus permisos (panel y sidebar).
flowchart LR
    A["0001 · 0002<br/>concepto y stack"] --> B["0003 · 0005<br/>catálogos y modelos"]
    B --> C["0006 · 0007<br/>navegación y permisos"]
    C --> D["0008 · 0009<br/>gestión"]
    D --> E["0010<br/>flujo y pantallas"]
    E --> F["0011<br/>firma y claves"]
    E --> G["0012<br/>menú y permisos"]

Regla: el 0010 narra el flujo de entrada del usuario; el 0011 solo explica cómo se firma el token que ese flujo produce; el 0012 explica qué se muestra al entrar. Los ADRs de datos (0003–0009) definen las tablas, los catálogos y el ciclo de vida. El orden lógico está en el README.md del índice.


Este ADR complementa el ADR 0006 transversal (autenticación global JWT) definiendo el flujo técnico del login dentro de svc-identidad. El modelo de datos de las tablas referenciadas está en los ADRs de svc-identidad: mae_usuarios, rel_usuario_empresa, rel_usuario_sucursal, trx_refresh_tokens, log_sesiones en el 0005, y mae_empresas en el 0004. El detalle de pantallas y navegación está en los 0010 y 0012.