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 | sí | nombre de usuario |
password |
string | sí | 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 | sí | el token temporal del paso 1 |
empresa_id |
BIGINT | sí | 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¶
- Credenciales —
username+passwordverificados con Argon2id contra la tablamae_usuarios(hash, nunca la clave). Si falla, el error es genérico ("credenciales inválidas") para no revelar si el username existe. - 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. - Empresa elegida — en el paso 2, el
empresa_iddebe 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. - Estado — el usuario debe estar
is_activo = true. Si no, 403. - 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_tokenscontoken_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 .env → JWT_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
kidactual 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_idviaja 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
kiden 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.mddel í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.