Saltar a contenido

📚 Unidad ISS-13 · Middlewares de acceso y las 3 modalidades — capa 🧠 APRENDER

🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación

Capa Página Para qué
🧠 Aprender esta página comprender, explicar y relacionar
🛠 Construir Middlewares de acceso y las 3 modalidades ejecutar, programar y verificar
✅ GATE Cierre de la unidad condición para pasar al bloque siguiente

Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas de la unidad; los enlaces apuntan a secciones reales del material (anclas de MkDocs, sin acentos).

Tema 🧠 Aprender (esta página) 🛠 Construir (ISS técnico)
Ruta y ficha de la unidad Ruta de aprendizaje · Ficha del ISS Contenido de la unidad
Mapas y estructura Mapa mental · Mapa del backend · Árbol de archivos Contenido de la unidad
Recorrido y comandos Comandos explicados · Recorrido paso a paso Contenido de la unidad
Diagnóstico Diagnóstico Criterios de aceptación
Evaluación y cierre Criterios · Evaluación · GATE Condiciones de cierre · Cierre

🖥 Presentación del ISS

Presentación de la unidad. Diapositivas de ISS-13 — Middlewares de Acceso y las Tres Modalidades en Rutas (8 diapositivas). Se visualiza aquí, dentro del sitio; no hace falta descargar nada para estudiarla.

⛶ Ver presentación completa ⬇ Archivo editable (.pptx)

8 diapositivas · se visualiza dentro del sitio (archivo editable .pptx como opción secundaria).


🎬 Video explicativo

Recorrido audiovisual de la unidad. Este video explica, bloque por bloque, los middlewares del ISS-13. authenticate y authorize cierran usuarios, roles, recursos, las dos pivotes y las cinco familias de negocio.

5:35 · narración en español · subtítulos activables desde el reproductor.


ISS-13 — Cuaderno de aprendizaje visual

Tema

Middlewares de acceso y las tres modalidades en rutas: montar dos middlewares componibles (authenticate y authorize) sobre las rutas de negocio para que la matriz RBAC construida en el ISS-12 empiece a decidir cada petición.

Fuente técnica autoritativa

Archivo fuente ../manual/15-ISS-13-auth-access.md
Estado Solo lectura — este cuaderno no modifica el ISS
Alcance 642 líneas, 6 apartados (18.1 … 18.6), 1 DoD

Este cuaderno es una capa pedagógica sobre ese archivo: su contenido técnico (código, criterios y comandos) aparece íntegro y verbatim en la sección Recorrido del ISS, paso a paso. El ISS manda; el cuaderno explica el por qué.

Pregunta que responde: ¿qué archivo es la fuente de verdad de este cuaderno y qué debo esperar de él?

Regla del ISS

Objetivo: materializar las tres modalidades mediante dos middlewares componibles y aplicarlos a las rutas existentes sin tocar controllers, services ni repositories. Bloqueado por: ISS-12 (la matriz debe existir para que authorize tenga algo que consultar).

La condición que el propio ISS exige para darse por terminado es de comportamiento observable de la API: sin token → 401; con token válido pero sin concesión → 403; con concesión → 200/201. Añade que las cinco features de negocio aplican authenticate, authorize y que npx tsc --noEmit pasa sin errores.

La frase clave del ISS es «sin tocar controllers, services ni repositories»: todo el trabajo de este ISS vive en las rutas y en dos archivos nuevos. Si tuvieras que modificar un service para proteger una ruta, el diseño estaría mal.

Cómo leer este cuaderno

Cada concepto se presenta tres veces, desde tres ángulos distintos:

                 CONCEPTO
                    │
        ┌───────────┼───────────┐
        ▼           ▼           ▼
   EXPLICACIÓN    CÓDIGO      VISUAL
        │           │           │
    ¿qué es?    ¿dónde está?  ¿cómo lo
    ¿por qué?   ¿qué hace?     visualizo?
    ¿para qué?  ¿cómo opera?  ¿con qué
                               se relaciona?

Pregunta que responde: ¿cómo está organizado este cuaderno y qué espero encontrar en cada parte?

El recorrido de lectura es siempre el mismo:

EXPLICACIÓN
    ↓
CÓDIGO (lo inserta el generador, verbatim)
    ↓
VISUAL

Y cada cuaderno contiene los mismos seis componentes:

Componente Dónde vive Para qué sirve
Texto todas las secciones entender el por qué
Código Recorrido del ISS, paso a paso ver el qué exacto (verbatim)
Diagramas Mapa mental, Mapa del backend, Las tres modalidades, Flujos ver el cómo se conecta
Preguntas Evaluación comprobar que entendiste
Evaluación Evaluación practicar y autoevaluarte
GATE GATE saber si puedes pasar al ISS-14

No basta con leer: los middlewares solo se entienden de verdad cuando ves el mismo endpoint responder 401, luego 403 y luego 200. El GATE de este ISS es exactamente eso.

Ruta de aprendizaje

Esta ruta es específica del ISS-13: no construye negocio, construye la puerta por la que entra el negocio.

Leer el contrato (18.1 … 18.6)
    ↓
Entender authenticate (¿quién eres?) → req.auth
    ↓
Entender authorize (¿puedes?) → matriz RBAC → 403
    ↓
Crear el barrel access/index.ts
    ↓
Parchear las 5 *.routes.ts de negocio
    ↓
Distinguir OPEN · JWT · JWT + RBAC
    ↓
Comprender deny by default
    ↓
Probar 401 · 403 · 200 con curl
    ↓
GATE (tsc + dev + verificación 18.6)

Fíjate en lo que no aparece: no hay «crear matriz RBAC» (eso fue el ISS-12) ni «emitir tokens» (eso llega en el ISS-15). Aquí solo se monta lo que ya existe.

Índice

  1. Ficha del ISS
  2. Mapa mental del ISS
  3. Mapa del backend
  4. Las tres modalidades de acceso
  5. Deny by default
  6. Árbol de archivos
  7. Anatomía del código
  8. Comandos explicados
  9. Flujos
  10. Recorrido del ISS, paso a paso
  11. Diagnóstico
  12. Conexión con el resto del curso
  13. Glosario
  14. Criterios de aceptación
  15. Evaluación
  16. GATE

Ficha del ISS

Campo Valor
ISS ISS-13
Título Middlewares de acceso y las tres modalidades en rutas
Objetivo Materializar las tres modalidades con dos middlewares componibles y aplicarlos a las rutas existentes sin tocar controllers, services ni repositories
Fase Fase II — Auth con RBAC
Tecnología principal Express 5 (middlewares) + JWT (HS256) + la matriz RBAC del ISS-12
Depende de ISS-12 — RoleUsers y ResourceRoles
Habilita ISS-14 — Feature RefreshTokens
Archivos creados access/authenticate.middleware.ts, access/authorize.middleware.ts, access/index.ts
Archivos parcheados clients.routes.ts, product-types.routes.ts, products.routes.ts, sales.routes.ts, product-sales.routes.ts
Componentes incorporados Middleware de autenticación (authenticate), middleware de autorización (authorize), barrel access
Verificación principal npx tsc --noEmit + la batería de curl de 18.6 (401 / 403 / 200 / 201)
Resultado esperado Las 5 features de negocio pasan de OPEN a JWT + RBAC; el RBAC tiene efecto real
GATE DoD del ISS-13: sin token → 401; seller creando → 403; admin creando → 201; tsc y dev OK

Qué implementamos AHORA

Dos middlewares y su montaje. En concreto:

  • authenticate: resuelve la identidad (¿quién eres?) y la deja en req.auth. Cualquier fallo es 401.
  • authorize: resuelve la concesión (¿puedes?) consultando la matriz RBAC del ISS-12. Cualquier ausencia de permiso es 403.
  • El barrel access/index.ts, que reexporta ambos.
  • El parche de las cinco familias de rutas de negocio para insertar authenticate, authorize entre la ruta y el controller.

Lo importante: la matriz RBAC del ISS-12 existía pero no se usaba. A partir de este ISS se consume en cada petición; es el momento en que el RBAC deja de ser datos y se convierte en comportamiento.

Qué todavía NO implementamos

Este ISS no crea tokens, no inicia sesión y no renueva credenciales. Para que no haya confusión:

No se implementa aquí Llega en
refresh_tokens: hash SHA-256, rotación, detección de reúso ISS-14
POST /api/sesion/refresh con rotación ISS-15
POST /api/sesion/login (emisión del par de tokens) ISS-15
POST /api/sesion/logout (revocación de sesión) ISS-15
GET /api/sesion/perfil y GET /api/permisos ISS-15
El agregador final routes/index.ts con las 7 features de auth CIERRE Fase II (18-cierre-auth.md)

Ojo con la trampa habitual: en la verificación de 18.6 se usa POST /api/sesion/login para obtener un token. Ese endpoint todavía no forma parte de este ISS (llega en el ISS-15); el ISS-13 lo da por disponible porque el laboratorio se ejecuta al final. Si estás siguiendo el curso ISS a ISS, ejecuta el GATE cuando llegues al ISS-15 o genera tú mismo un token con la clave de firma.

Mapa mental del ISS

mindmap
  root((ISS-13<br/>Acceso y rutas))
    Middlewares
      authenticate
      authorize
    Modalidades
      OPEN sin identidad
      JWT identidad
      JWT con RBAC
    Decision
      401 sin identidad
      403 sin concesion
      200 con concesion
    Archivos
      barrel access
      cinco rutas parcheadas
    Concepto clave
      deny by default
      permisos fuera del token
    GATE
      tsc y dev
      curl 401 403 200

Pregunta que responde: ¿de qué trata este ISS y qué piezas lo componen?

Mapa del backend

Esta representación responde siempre a la misma pregunta: ¿en qué parte del backend estamos?

IMPLEMENTADO HASTA ESTE ISS
───────────────────────────

   HTTP
     ↓
   ┌─────────────────────────────────────────────────┐
   │  MIDDLEWARES  ← NUEVO EN ISS-13                 │
   │    authenticate  ✅      authorize  ✅           │
   └─────────────────────────────────────────────────┘
     ↓
   Routes        ✅  (las 5 de negocio ya protegidas)
     ↓
   Controller    ✅
     ↓
   Service       ✅
     ↓
   Repository    ✅
     ↓
   Model         ✅
     ↓
   Sequelize     ✅
     ↓
   Base de datos ✅

   Seguridad base ....... ✅  JWT HS256 · bcrypt 12 · AppError · sendError · resource-match
   users ................ ✅
   roles ................ ✅
   resources ............ ✅  catálogo de 58 recursos
   role_users ........... ✅
   resource_roles ....... ✅  la matriz RBAC
   access (middlewares) . ✅  ← la matriz empieza a CONSUMIRSE


OBJETIVO DE ARQUITECTURA
────────────────────────

   refresh_tokens ....... 🎯  ISS-14: hash SHA-256, rotación, detección de reúso
   sesión ............... 🎯  ISS-15: login / refresh / logout / perfil / permisos

Pregunta que responde: ¿qué capas del backend existen ya y cuáles son todavía objetivo?

Léelo así: hasta el ISS-12 tenías la matriz construida pero nadie la consultaba. Aquí aparece la banda de middlewares entre HTTP y Routes. Ese es todo el cambio estructural del ISS.

Las tres modalidades de acceso

En el mismo backend conviven tres modalidades, y su elección es una decisión por ruta, que se toma en el montaje de los middlewares. No hay un interruptor global.

Modalidad Middleware en la ruta Qué exige Sin cumplir
OPEN — nada —
JWT authenticate access token válido y usuario activo 401
JWT + RBAC authenticate, authorize token válido y concesión activa de (method, path) 401 (sin token) / 403 (sin permiso)

La consecuencia práctica es que el contrato de negocio no cambia: el mismo controller sirve para una ruta OPEN y para una ruta JWT + RBAC. Lo único que cambia es qué middlewares declaras delante.

flowchart TD
    A["Petición HTTP"] --> B{"¿Trae Authorization: Bearer?"}
    B -- "No" --> C["401 Unauthorized"]
    B -- "Sí" --> D["authenticate"]
    D --> E{"¿Token válido y usuario activo?"}
    E -- "No" --> C
    E -- "Sí" --> F["req.auth = identidad"]
    F --> G{"¿La ruta monta authorize?"}
    G -- "No → modalidad JWT" --> H["Controller"]
    G -- "Sí → modalidad JWT + RBAC" --> I["authorize consulta la matriz"]
    I --> J{"¿Concesión activa para method + path?"}
    J -- "No" --> K["403 Forbidden"]
    J -- "Sí" --> H
    H --> L["Service → Repository → BD → respuesta 200/201"]

Pregunta que responde: ¿por dónde pasa una petición por los dos middlewares y en qué punto exacto nace un 401 o un 403?

401 vs 403. La diferencia es la que más se confunde y la que más importa:

  • 401 (Unauthorized) significa, en realidad, no autenticado: no sé quién eres. Falta el token, está mal formado, está caducado, la firma no cuadra o el usuario ya no está activo.
  • 403 (Forbidden) significa autenticado pero sin permiso: sé quién eres y aun así no puedes hacer esto concreto.

Por eso el 401 lo produce siempre authenticate y el 403 lo produce siempre authorize. Un 403 sin identidad previa sería un error de montaje: authorize empieza comprobando que exista req.auth y, si no, responde 401.

En el ISS-13, las cinco features de negocio se montan como JWT + RBAC; en el ISS-14 aparecerán rutas JWT puras (las sesiones propias), que es donde la modalidad intermedia cobra sentido.

Deny by default

La regla del authorize es binaria y pesimista: si no hay una concesión activa que cubra (method, path), la respuesta es 403. No hay permiso implícito, ni herencia por defecto, ni «si no encuentro nada, dejo pasar».

¿Por qué se diseña así y no al revés?

  • Fail-closed vs fail-open. Con deny by default, el peor caso de un fallo (una consulta vacía, un recurso que aún no existe, un rol sin concesiones) es negar el acceso: un usuario legítimo se queja y se arregla. Con allow by default, el peor caso es conceder acceso: nadie se queja y el agujero queda abierto.
  • Una ruta nueva nace cerrada. Al añadir un endpoint, lo natural es que aún no tenga fila en la matriz. Si el comportamiento por defecto fuera permitir, cada ruta nueva sería un hueco de seguridad hasta que alguien se acordara de restringirla.
  • Conceder es un dato, no un despliegue. Deny by default empuja en la dirección correcta: para dar acceso se insertan filas en resource_roles/role_users; no se toca el código.
authorize(method, path)
    ↓
consulta la cadena activa del usuario
    ↓
¿alguna concesión activa casa con (method, path)?
    ├── SÍ  → next()   → 200/201
    └── NO  → 403       ← deny by default (ausencia = denegación)

Pregunta que responde: ¿por qué la ausencia de concesión es un 403 y nunca un permiso implícito?

Árbol de archivos

Leyenda: ★ = archivo creado en este ISS · △ = archivo existente que se parchea.

Estructura antes

src/
├── features/
│   ├── auth/
│   │   ├── users/                 (feature completo)
│   │   ├── roles/                 (feature completo)
│   │   ├── resources/             (feature completo + resource-catalog.ts)
│   │   ├── role-users/            (feature completo)
│   │   ├── resource-roles/        (feature completo)
│   │   └── rbac.associations.ts   (el grafo RBAC)
│   └── business/
│       ├── clients/               clients.routes.ts  (OPEN)
│       ├── product-types/         product-types.routes.ts (OPEN)
│       ├── products/              products.routes.ts (OPEN)
│       ├── sales/                 sales.routes.ts   (OPEN)
│       └── product-sales/         product-sales.routes.ts (OPEN)
└── ...

Archivos creados / modificados en este ISS

src/
├── features/
│   ├── auth/
│   │   └── access/                          ★ carpeta nueva
│   │       ├── authenticate.middleware.ts   ★
│   │       ├── authorize.middleware.ts      ★
│   │       └── index.ts                     ★
│   └── business/
│       ├── clients/clients.routes.ts               △ parcheada → JWT + RBAC
│       ├── product-types/product-types.routes.ts   △ parcheada → JWT + RBAC
│       ├── products/products.routes.ts             △ parcheada → JWT + RBAC
│       ├── sales/sales.routes.ts                   △ parcheada → JWT + RBAC
│       └── product-sales/product-sales.routes.ts   △ parcheada → JWT + RBAC
└── ...

Estructura después

src/
├── features/
│   ├── auth/
│   │   ├── access/                 ★ NUEVO
│   │   │   ├── authenticate.middleware.ts
│   │   │   ├── authorize.middleware.ts
│   │   │   └── index.ts
│   │   ├── users/
│   │   ├── roles/
│   │   ├── resources/
│   │   ├── role-users/
│   │   ├── resource-roles/
│   │   └── rbac.associations.ts
│   └── business/
│       ├── clients/               (routes ahora JWT + RBAC)
│       ├── product-types/         (routes ahora JWT + RBAC)
│       ├── products/              (routes ahora JWT + RBAC)
│       ├── sales/                 (routes ahora JWT + RBAC)
│       └── product-sales/         (routes ahora JWT + RBAC)
└── ...

El árbol es muy elocuente: tres archivos nuevos y cinco parches. Si el árbol creciera por cualquier otro lado, algo se estaría diseñando mal.

Pregunta que responde: ¿este ISS modifica la estructura del proyecto y en qué exactamente?

Anatomía del código

Archivo: src/features/auth/access/authenticate.middleware.ts

Propósito

Resolver la identidad de la petición. Responde a la pregunta ¿quién eres? y deja la respuesta en req.auth para el resto de la cadena. No sabe nada de permisos.

Explicación

El middleware hace cuatro cosas, en orden, y cada fallo desemboca en un 401:

  1. Extrae el token del encabezado Authorization: Bearer <token> (RFC 6750) con extractBearerToken. Si falta o está mal formado, lanza AppError(401).
  2. Verifica el JWT con verifyAccessToken, que exige algoritmo, emisor (iss) y audiencia (aud) fijos y respeta la expiración (exp), conforme a RFC 8725. Cualquier fallo → 401.
  3. Revalida contra la base de datos: usersRepository.findById(userId) y exige que el usuario exista y tenga status === "active". Este paso es la joya del diseño: un token firmado sigue siendo criptográficamente válido después de desactivar la cuenta, así que sin esta consulta la desactivación no tendría efecto hasta que caducara el token.
  4. Publica la identidad en req.auth con { id, username, email, tokenId } (este último, el jti del token) y llama a next().

Hay una defensa en profundidad deliberada: aunque verifyAccessToken ya garantiza que sub es un entero positivo, el middleware lo vuelve a comprobar (Number.isInteger(userId) && userId >= 1) para que un cambio futuro en la verificación no envíe un NaN al repositorio y convierta un 401 en un 500.

El comentario del propio archivo insiste en que no consulta roles ni permisos: mezclar autenticación y autorización impediría tener endpoints solo-JWT, que son justo la modalidad intermedia.

Se conecta con

  • Entrada: el router de Express (cada *.routes.ts lo monta antes del controller).
  • Salida: extractBearerToken/verifyAccessToken (shared/auth/jwt), UsersRepository.findById (feature users), sendError (shared/http/error-response) y req.auth.

Archivo: src/features/auth/access/authorize.middleware.ts

Propósito

Decidir la concesión. Responde a la segunda pregunta: ¿puede esta identidad ejecutar method + path? Debe montarse después de authenticate.

Explicación

  1. Exige identidad. Si no hay req.auth, responde 401 («authentication required»). Esto protege contra un montaje incorrecto: authorize sin authenticate delante nunca debe conceder por accidente.
  2. Normaliza la operación. Toma req.method.toUpperCase() y normalizePath(req.originalUrl). Normalizar es lo que permite que /api/productos/42 hable el mismo idioma que el patrón /api/productos/:id almacenado en la matriz.
  3. Consulta la cadena completa con resourceRolesRepository.findEffectiveForUser(req.auth.id): resource_roles → roles → role_users → resources, exigiendo que todos los eslabones estén activos.
  4. Decide. isOperationGranted(granted, method, path) compara la operación con las concesiones por patrón. Si no hay coincidencia → AppError(403) (deny by default). Si la hay → next().

El middleware no recibe parámetros: el recurso y la acción se derivan de la propia petición. Eso es lo que hace que añadir un permiso sea insertar filas en la base de datos y nunca tocar el código.

Como la consulta se hace en cada petición, revocar un permiso tiene efecto inmediato: los permisos no viajan dentro del token, así que no hay que esperar a que caduque.

Se conecta con

  • Entrada: el router, siempre detrás de authenticate.
  • Salida: isOperationGranted/normalizePath (shared/auth/resource-match), ResourceRolesRepository.findEffectiveForUser (feature resource-roles) y sendError.

Archivo: src/features/auth/access/index.ts

Propósito

Ser el barrel del submódulo de acceso: una sola puerta de importación para que las rutas de negocio no conozcan rutas internas.

Explicación

Reexporta authenticate.middleware y authorize.middleware. Gracias a él, cada *.routes.ts importa exactamente import { authenticate, authorize } from "../../auth/access";. Es el punto de acoplamiento estable: si mañana cambia la organización interna de access/, las rutas no se enteran.

Se conecta con

  • Entrada: las cinco features de negocio.
  • Salida: los dos middlewares.

Archivo: src/features/business/*/*.routes.ts (las cinco parcheadas)

Propósito

Cambiar la modalidad de acceso de las rutas de negocio sin tocar el contrato de la API ni las capas de negocio.

Explicación

El parche es quirúrgico y se repite cinco veces:

  1. Se añade el import del barrel ../../auth/access.
  2. Se insertan authenticate, authorize entre la ruta y el controller en cada operación.
antes:   app.route(...).get(controller)
después: app.route(...).get(authenticate, authorize, controller)

La única diferencia entre features es qué rol recibe qué. Según los comentarios del ISS: en clients, SELLER recibe solo las lecturas y ADMIN las 7 operaciones; en sales, SELLER recibe las 3 concesiones del feature (GET /api/ventas, GET /api/ventas/:id, POST /api/ventas); en product-sales, solo ADMIN recibe GET /api/detalle-ventas. Nada de eso se escribe en el código de rutas: son datos de la matriz, o sea, filas de resource_roles.

El controller, el service y el repository no se tocan. Esa es la prueba de que la seguridad se implementó en el lugar correcto.

Se conecta con

  • Entrada: Routes (agregador de features) las instancia.
  • Salida: authenticate, authorize y los controllers de cada feature.

Comandos explicados

El ISS no introduce comandos de instalación: el proyecto ya existe. Su verificación es de ejecución (arrancar el servidor y golpear la API).

npm run dev

COMANDO
   ↓
npm run dev
   ↓
QUÉ HACE
   Arranca el servidor Express con recarga en caliente sobre el puerto configurado.
   ↓
POR QUÉ SE NECESITA
   Los middlewares solo se prueban con el servidor en marcha: hay que ver la API
   responder 401/403/200 de verdad.
   ↓
QUÉ CREA O MODIFICA
   Nada en disco. Levanta el proceso del servidor.
   ↓
RESULTADO ESPERADO
   El log de arranque de Express y la base de datos conectada (sync OK).
   ↓
CÓMO VERIFICARLO
   Que el puerto escuche y que un curl a una ruta protegida responda (aunque sea 401).

curl -i http://localhost:4000/api/clientes

COMANDO
   ↓
curl -i http://localhost:4000/api/clientes
   ↓
QUÉ HACE
   Lanza un GET sin cabecera Authorization y muestra la respuesta con cabeceras.
   ↓
POR QUÉ SE NECESITA
   Es la prueba mínima de que la ruta dejó de ser OPEN: antes daba 200, ahora 401.
   ↓
QUÉ CREA O MODIFICA
   Nada. Es una petición de lectura.
   ↓
RESULTADO ESPERADO
   HTTP/1.1 401 Unauthorized.
   ↓
CÓMO VERIFICARLO
   El código de estado en la primera línea de la respuesta.

npx tsc --noEmit

COMANDO
   ↓
npx tsc --noEmit
   ↓
QUÉ HACE
   Compila el proyecto TypeScript sin escribir archivos de salida.
   ↓
POR QUÉ SE NECESITA
   Los tipos de `req.auth` (extension de Request) y las firmas de los middlewares
   deben encajar; sin este chequeo, un error de tipos aparecería en runtime.
   ↓
QUÉ CREA O MODIFICA
   Nada (no emite salida).
   ↓
RESULTADO ESPERADO
   Salida vacía y código de salida 0.
   ↓
CÓMO VERIFICARLO
   Si no imprime errores, pasa.

La batería de curl de 18.6

El ISS define cinco verificaciones encadenadas: sin token (401), token manipulado (401), seller leyendo (200), seller creando (403) y admin creando (201). Los comandos exactos están en el GATE y, verbatim, en el cuerpo del ISS.

Pregunta que responde: ¿qué comandos cierran este ISS y qué demuestra cada uno?

Flujos

El flujo completo de una petición protegida, con todos los caminos de error:

sequenceDiagram
    autonumber
    participant C as Cliente
    participant R as Express Router
    participant A as authenticate
    participant Z as authorize
    participant U as UsersRepository
    participant G as ResourceRolesRepository
    participant H as Controller
    C->>R: GET /api/clientes con Bearer token
    R->>A: next
    A->>A: extrae el Bearer token
    alt Falta el token o el JWT no verifica
        A-->>C: 401 Unauthorized
    else Token valido
        A->>U: findById del sub
        alt Usuario inexistente o inactivo
            A-->>C: 401 Unauthorized
        else Usuario activo
            A->>A: escribe req.auth
            A->>Z: next
            Z->>G: findEffectiveForUser
            G-->>Z: concesiones activas
            alt Sin concesion para method y path
                Z-->>C: 403 Forbidden
            else Concesion presente
                Z->>H: next
                H-->>C: 200 OK
            end
        end
    end

Pregunta que responde: ¿en qué orden se ejecutan los middlewares y quién es responsable de cada código de error?

Observa la asimetría: authenticate consulta la tabla users, authorize consulta la cadena RBAC. Son dos viajes a la base de datos por petición, y eso es intencional: el precio de que revocar un permiso o desactivar un usuario tenga efecto inmediato.

Recorrido del ISS, paso a paso

A partir de aquí viene el contenido técnico completo del ISS, verbatim: sus objetivos, sus criterios, su código y sus comandos de verificación. Se reproduce sin resumir y sin reformatear; solo se han degradado los encabezados un nivel para que aniden bajo esta sección, y se han reescrito los enlaces relativos para que abran bien desde docs/aprendizaje/ (reescritos a ../manual/).

Fase II: Auth con RBAC — ISS-13 — Middlewares de acceso y las tres modalidades en rutas

Contexto mínimo (autosuficiente). Si abres este archivo aislado —o lo llevas a otra IA—, esto es lo que hay que saber antes de ejecutarlo. - Proyecto: app-storelab-express-ii — Express 5 + TypeScript + Sequelize, arquitectura por features. - Ya construido: Fase I, ISS-09 base, ISS-10 Users, ISS-11 Roles/Resources y ISS-12 pivotes. - Recorrido obligatorio de una petición: HTTP → Controller → Service → Repository → Model → Sequelize → BD. Además, en este ISS se insertan middlewares entre HTTP y Controller. - Diseño de datos (Fase II): ../bd-storelab.md §16 (autorización efectiva), §18 (tres formas de acceder a la BD) y §20 (deny by default). - Capas, convenciones y reglas transversales: 00-contexto.md.

Este ISS
Título Middlewares de acceso y las tres modalidades en rutas
Feature / tablas features/auth/access/ + PARCHE de las 5 rutas de negocio
API todas las de Fase I pasan a JWT + RBAC
Depende de ISS-12 — RoleUsers y ResourceRoles
Habilita ISS-14 — Feature RefreshTokens

Contenido de este ISS

  • 18.1 authenticate.middleware.ts (modalidad JWT)
  • 18.2 authorize.middleware.ts (modalidad JWT + RBAC)
  • 18.3 access/index.ts (barrel)
  • 18.4 PARCHE: las 5 rutas de negocio pasan a JWT + RBAC
  • 18.5 Las tres modalidades en una tabla
  • 18.6 Verificación de 401 y 403

Objetivo: materializar las tres modalidades mediante dos middlewares componibles y aplicarlos a las rutas existentes sin tocar controllers, services ni repositories.

OPEN            app.route(...).get(controller)
JWT             app.route(...).get(authenticate, controller)
JWT + RBAC      app.route(...).get(authenticate, authorize, controller)

Bloqueado por: ISS-12 (la matriz debe existir para que authorize tenga algo que consultar).

Criterios de aceptación (ISS-13) — consolidados

  • [ ] 18.1 authenticate valida el Bearer token, verifica algoritmo/issuer/audience/exp y carga el usuario activo en req.auth
  • [ ] 18.2 authorize resuelve (method, path) y busca concesión activa; deny by default → 403
  • [ ] 18.3 access/index.ts reexporta ambos middlewares
  • [ ] 18.4 las 5 features de negocio (clients, product-types, products, sales, product-sales) aplican authenticate, authorize
  • [ ] 18.5 documentadas las tres modalidades y qué códigos produce cada una
  • [ ] 18.6 sin token → 401; con token pero sin concesión → 403; con concesión → 200/201
  • [ ] npx tsc --noEmit OK

18.1 authenticate — modalidad JWT

Hace exactamente cuatro cosas, en este orden:

  1. Lee el encabezado Authorization: Bearer <token> (RFC 6750). Si falta o está mal formado → 401.
  2. Verifica el JWT con algoritmo, emisor y audiencia fijos (RFC 8725). Si falla → 401.
  3. Carga el usuario en BD y exige status = 'active'. Si no existe o está inactivo → 401.
  4. Deja la identidad en req.auth (tipado por auth-user.ts) y llama a next().

No consulta roles ni permisos. La autorización es responsabilidad del siguiente middleware: mezclar ambas impediría tener endpoints solo-JWT.

: > src/features/auth/access/authenticate.middleware.ts
cat >> src/features/auth/access/authenticate.middleware.ts << 'EOF'
import { NextFunction, Request, Response } from "express";
import { AppError } from "../../../shared/errors/app-error";
import { sendError } from "../../../shared/http/error-response";
import { extractBearerToken, verifyAccessToken } from "../../../shared/auth/jwt";
import { UsersRepository } from "../users/users.repository";

/**
 * **MODALIDAD 2 — JWT (identidad).** Middleware de autenticación.
 *
 * Responde únicamente a la pregunta **¿quién eres?**:
 *
 *  1. Lee el token de `Authorization: Bearer <token>` (RFC 6750).
 *  2. Verifica firma, algoritmo, `iss`, `aud`, `exp` (RFC 8725).
 *  3. **Revalida contra la base de datos** que el usuario sigue existiendo y con
 *     `status = active`. Un token firmado sigue siendo válido después de
 *     desactivar la cuenta; esta revalidación hace que la desactivación tenga
 *     efecto inmediato.
 *
 * NO consulta la matriz de permisos: eso es responsabilidad de `authorize`.
 * Si todo va bien, deja la identidad en `req.auth` y cede el paso.
 *
 * Cualquier fallo se responde con **401 (no autenticado)**.
 */
const usersRepository = new UsersRepository();

export async function authenticate(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    const token = extractBearerToken(req.headers.authorization);
    if (!token) {
      throw new AppError(401, "Missing Bearer token");
    }

    const payload = verifyAccessToken(token);

    // Defensa en profundidad: `verifyAccessToken` ya garantiza que `sub` es un
    // entero positivo. Se vuelve a comprobar para que ningún cambio futuro en la
    // verificación pueda enviar un `NaN` al repositorio (500 en vez de 401).
    const userId = Number(payload.sub);
    if (!Number.isInteger(userId) || userId < 1) {
      throw new AppError(401, "Invalid or expired access token");
    }

    const user = await usersRepository.findById(userId);

    if (!user || user.status !== "active") {
      throw new AppError(401, "User is not active");
    }

    req.auth = {
      id: user.id,
      username: user.username,
      email: user.email,
      tokenId: payload.jti,
    };
    next();
  } catch (error) {
    sendError(res, error);
  }
}
EOF

18.2 authorize — modalidad JWT + RBAC

Recibe la petición, normaliza el (method, path) real y comprueba si el usuario autenticado alcanza ese recurso por el grafo:

req.auth.user
  → role_users (active)
  → roles (active)
  → resource_roles (active)
  → resources (active, method = req.method, path ≈ req.path)

Si no hay concesión → 403 (deny by default). Como la consulta se hace en cada petición, revocar un permiso tiene efecto inmediato (no hay que esperar a que caduque el token, porque los permisos no viajan en él).

: > src/features/auth/access/authorize.middleware.ts
cat >> src/features/auth/access/authorize.middleware.ts << 'EOF'
import { NextFunction, Request, Response } from "express";
import { AppError } from "../../../shared/errors/app-error";
import { sendError } from "../../../shared/http/error-response";
import { isOperationGranted, normalizePath } from "../../../shared/auth/resource-match";
import { ResourceRolesRepository } from "../resource-roles/resource-roles.repository";

/**
 * **MODALIDAD 3 — RBAC (identidad + autorización granular).** Middleware de
 * autorización.
 *
 * Debe montarse **después** de `authenticate`. Responde a la segunda pregunta:
 * *¿puede esta identidad ejecutar `method + path`?*
 *
 * Cómo resuelve la decisión:
 *  1. Toma la identidad ya resuelta en `req.auth`.
 *  2. Consulta la **cadena completa** de autorización en la base de datos
 *     (`resource_roles → roles → role_users → resources`, todos los eslabones
 *     activos) para ese `user_id`.
 *  3. Compara el par `(method, path)` de la petición con las concesiones,
 *     por patrón (`/api/productos/:id` casa con `/api/productos/42`).
 *
 * Reglas:
 *  - **Deny by default**: sin concesión activa que cubra la operación -> 403.
 *  - **401** si no hay identidad (falta `authenticate` o el token no valió).
 *  - **403** si hay identidad válida pero no hay permiso.
 *
 * No recibe parámetros: el recurso y la acción se derivan de la propia petición.
 * Añadir un permiso es insertar filas en la base de datos, nunca tocar el código.
 */
const resourceRolesRepository = new ResourceRolesRepository();

export async function authorize(
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> {
  try {
    if (!req.auth) {
      throw new AppError(401, "Authentication required");
    }

    const method = req.method.toUpperCase();
    const path = normalizePath(req.originalUrl);

    const granted = await resourceRolesRepository.findEffectiveForUser(req.auth.id);

    if (!isOperationGranted(granted, method, path)) {
      throw new AppError(403, `Forbidden: no grant for ${method} ${path}`);
    }

    next();
  } catch (error) {
    sendError(res, error);
  }
}
EOF

18.3 Barrel de acceso

: > src/features/auth/access/index.ts
cat >> src/features/auth/access/index.ts << 'EOF'
export * from "./authenticate.middleware";
export * from "./authorize.middleware";
EOF

18.4 PARCHE: las 5 rutas de negocio pasan a JWT + RBAC

El cambio es quirúrgico: se importa authenticate, authorize desde el barrel de auth y se insertan entre la ruta y el controller. Ni el contrato de la API ni las capas de negocio cambian.

import { authenticate, authorize } from "../../auth/access";
// ...
app.route("/api/clientes/:id").get(
  authenticate,
  authorize,
  this.clientsController.getOne.bind(this.clientsController)
);

PARCHE en clients.routes.ts:

: > src/features/business/clients/clients.routes.ts
cat >> src/features/business/clients/clients.routes.ts << 'EOF'
import { Application } from "express";
import { ClientsController } from "./clients.controller";
import { authenticate, authorize } from "../../auth/access";

/**
 * Rutas del feature Clients.
 *
 * **Modalidad 3 — JWT + RBAC**: `authenticate` resuelve la identidad (401 si no
 * hay token válido o el usuario está inactivo) y `authorize` decide sobre el par
 * `(method, path)` (403 si no hay concesión activa). El rol `SELLER` recibe solo
 * las lecturas; `ADMIN`, las 7 operaciones.
 */
export class ClientsRoutes {
  public clientsController: ClientsController = new ClientsController();

  public routes(app: Application): void {
    // getAll
    app
      .route("/api/clientes")
      .get(authenticate, authorize, this.clientsController.getAll.bind(this.clientsController));

    // getOne
    app
      .route("/api/clientes/:id")
      .get(authenticate, authorize, this.clientsController.getOne.bind(this.clientsController));

    // create
    app
      .route("/api/clientes")
      .post(authenticate, authorize, this.clientsController.create.bind(this.clientsController));

    // update (PUT / PATCH)
    app
      .route("/api/clientes/:id")
      .put(authenticate, authorize, this.clientsController.updatePut.bind(this.clientsController))
      .patch(
        authenticate,
        authorize,
        this.clientsController.updatePatch.bind(this.clientsController)
      );

    // delete físico
    app
      .route("/api/clientes/:id")
      .delete(
        authenticate,
        authorize,
        this.clientsController.deletePhysical.bind(this.clientsController)
      );

    // delete lógico
    app
      .route("/api/clientes/:id/deactivate")
      .patch(
        authenticate,
        authorize,
        this.clientsController.deleteLogical.bind(this.clientsController)
      );
  }
}
EOF

PARCHE en product-types.routes.ts:

: > src/features/business/product-types/product-types.routes.ts
cat >> src/features/business/product-types/product-types.routes.ts << 'EOF'
import { Application } from "express";
import { ProductTypesController } from "./product-types.controller";
import { authenticate, authorize } from "../../auth/access";

/**
 * Rutas del feature ProductTypes.
 *
 * **Modalidad 3 — JWT + RBAC.** Cada operación exige, en este orden:
 *  1. `authenticate` -> valida el access token y revalida que el usuario siga activo (401 si no);
 *  2. `authorize`    -> comprueba la concesión del par `(method, path)` en la matriz RBAC (403 si no);
 *  3. el handler del controller.
 *
 * El catálogo de recursos incluye estas 7 operaciones
 * (`/api/tipos-producto`, `/api/tipos-producto/:id`, ...): concederlas a un rol
 * no requiere tocar este archivo.
 */
export class ProductTypesRoutes {
  public productTypesController: ProductTypesController = new ProductTypesController();

  public routes(app: Application): void {
    // getAll
    app
      .route("/api/tipos-producto")
      .get(
        authenticate,
        authorize,
        this.productTypesController.getAll.bind(this.productTypesController)
      );

    // getOne
    app
      .route("/api/tipos-producto/:id")
      .get(
        authenticate,
        authorize,
        this.productTypesController.getOne.bind(this.productTypesController)
      );

    // create
    app
      .route("/api/tipos-producto")
      .post(
        authenticate,
        authorize,
        this.productTypesController.create.bind(this.productTypesController)
      );

    // update (PUT / PATCH)
    app
      .route("/api/tipos-producto/:id")
      .put(
        authenticate,
        authorize,
        this.productTypesController.updatePut.bind(this.productTypesController)
      )
      .patch(
        authenticate,
        authorize,
        this.productTypesController.updatePatch.bind(this.productTypesController)
      );

    // delete físico
    app
      .route("/api/tipos-producto/:id")
      .delete(
        authenticate,
        authorize,
        this.productTypesController.deletePhysical.bind(this.productTypesController)
      );

    // delete lógico
    app
      .route("/api/tipos-producto/:id/deactivate")
      .patch(
        authenticate,
        authorize,
        this.productTypesController.deleteLogical.bind(this.productTypesController)
      );
  }
}
EOF

PARCHE en products.routes.ts:

: > src/features/business/products/products.routes.ts
cat >> src/features/business/products/products.routes.ts << 'EOF'
import { Application } from "express";
import { ProductsController } from "./products.controller";
import { authenticate, authorize } from "../../auth/access";

/**
 * Rutas del feature Products.
 *
 * **Modalidad 3 — JWT + RBAC**: `authenticate` (401 si no hay identidad válida)
 * seguido de `authorize` (403 si la matriz no concede el par `(method, path)`).
 */
export class ProductsRoutes {
  public productsController: ProductsController = new ProductsController();

  public routes(app: Application): void {
    // getAll
    app
      .route("/api/productos")
      .get(authenticate, authorize, this.productsController.getAll.bind(this.productsController));

    // getOne
    app
      .route("/api/productos/:id")
      .get(authenticate, authorize, this.productsController.getOne.bind(this.productsController));

    // create
    app
      .route("/api/productos")
      .post(authenticate, authorize, this.productsController.create.bind(this.productsController));

    // update (PUT / PATCH)
    app
      .route("/api/productos/:id")
      .put(authenticate, authorize, this.productsController.updatePut.bind(this.productsController))
      .patch(
        authenticate,
        authorize,
        this.productsController.updatePatch.bind(this.productsController)
      );

    // delete físico
    app
      .route("/api/productos/:id")
      .delete(
        authenticate,
        authorize,
        this.productsController.deletePhysical.bind(this.productsController)
      );

    // delete lógico
    app
      .route("/api/productos/:id/deactivate")
      .patch(
        authenticate,
        authorize,
        this.productsController.deleteLogical.bind(this.productsController)
      );
  }
}
EOF

PARCHE en sales.routes.ts:

: > src/features/business/sales/sales.routes.ts
cat >> src/features/business/sales/sales.routes.ts << 'EOF'
import { Application } from "express";
import { SalesController } from "./sales.controller";
import { authenticate, authorize } from "../../auth/access";

/**
 * Rutas del feature Sales.
 *
 * **Modalidad 3 — JWT + RBAC**. El rol `SELLER` recibe las 3 concesiones de este
 * feature (`GET /api/ventas`, `GET /api/ventas/:id`, `POST /api/ventas`); el
 * resto de verbos quedan solo para `ADMIN`.
 */
export class SalesRoutes {
  public salesController: SalesController = new SalesController();

  public routes(app: Application): void {
    // getAll
    app
      .route("/api/ventas")
      .get(authenticate, authorize, this.salesController.getAll.bind(this.salesController));

    // getOne
    app
      .route("/api/ventas/:id")
      .get(authenticate, authorize, this.salesController.getOne.bind(this.salesController));

    // create (registrar venta)
    app
      .route("/api/ventas")
      .post(authenticate, authorize, this.salesController.create.bind(this.salesController));

    // update (PUT / PATCH)
    app
      .route("/api/ventas/:id")
      .put(authenticate, authorize, this.salesController.updatePut.bind(this.salesController))
      .patch(authenticate, authorize, this.salesController.updatePatch.bind(this.salesController));

    // delete físico
    app
      .route("/api/ventas/:id")
      .delete(
        authenticate,
        authorize,
        this.salesController.deletePhysical.bind(this.salesController)
      );

    // delete lógico
    app
      .route("/api/ventas/:id/deactivate")
      .patch(authenticate, authorize, this.salesController.deleteLogical.bind(this.salesController));
  }
}
EOF

PARCHE en product-sales.routes.ts:

: > src/features/business/product-sales/product-sales.routes.ts
cat >> src/features/business/product-sales/product-sales.routes.ts << 'EOF'
import { Application } from "express";
import { ProductSalesController } from "./product-sales.controller";
import { authenticate, authorize } from "../../auth/access";

/**
 * Rutas del feature ProductSales (tabla `product_sales`).
 *
 * **Modalidad 3 — JWT + RBAC**. Solo `ADMIN` recibe la concesión de
 * `GET /api/detalle-ventas`; el detalle de una venta concreta se consulta a
 * través de `GET /api/ventas/:id` (que sí concede `SELLER`).
 */
export class ProductSalesRoutes {
  public productSalesController: ProductSalesController = new ProductSalesController();

  public routes(app: Application): void {
    // getAll
    app
      .route("/api/detalle-ventas")
      .get(
        authenticate,
        authorize,
        this.productSalesController.getAll.bind(this.productSalesController)
      );

    // getOne
    app
      .route("/api/detalle-ventas/:id")
      .get(
        authenticate,
        authorize,
        this.productSalesController.getOne.bind(this.productSalesController)
      );

    // create
    app
      .route("/api/detalle-ventas")
      .post(
        authenticate,
        authorize,
        this.productSalesController.create.bind(this.productSalesController)
      );

    // update (PUT / PATCH)
    app
      .route("/api/detalle-ventas/:id")
      .put(
        authenticate,
        authorize,
        this.productSalesController.updatePut.bind(this.productSalesController)
      )
      .patch(
        authenticate,
        authorize,
        this.productSalesController.updatePatch.bind(this.productSalesController)
      );

    // delete físico
    app
      .route("/api/detalle-ventas/:id")
      .delete(
        authenticate,
        authorize,
        this.productSalesController.deletePhysical.bind(this.productSalesController)
      );

    // delete lógico
    app
      .route("/api/detalle-ventas/:id/deactivate")
      .patch(
        authenticate,
        authorize,
        this.productSalesController.deleteLogical.bind(this.productSalesController)
      );
  }
}
EOF

18.5 Las tres modalidades en una tabla

Modalidad Middleware en la ruta Qué exige Sin cumplir
OPEN — nada —
JWT authenticate access token válido y usuario activo 401
JWT + RBAC authenticate, authorize token válido y concesión activa de (method, path) 401 (sin token) / 403 (sin permiso)
Petición Resultado
GET /api/clientes sin Authorization 401
GET /api/clientes con token de seller 200 (SELLER tiene esa lectura)
POST /api/clientes con token de seller 403 (SELLER no tiene esa concesión)
POST /api/clientes con token de admin 201 (ADMIN tiene los 58)
GET /api/clientes/abc con token válido 400 (paramId)
Cualquier ruta con token caducado o manipulado 401

18.6 Verificación de 401 y 403

npm run dev
# 401 — sin token
curl -i http://localhost:4000/api/clientes

# 401 — token manipulado
curl -i -H "Authorization: Bearer no.es.un.jwt" http://localhost:4000/api/clientes

# 200 — seller lee
TOKEN=$(curl -s -X POST http://localhost:4000/api/sesion/login \
  -H "Content-Type: application/json" \
  -d '{"identifier":"seller","password":"Seller123!"}' | node -pe "JSON.parse(require('fs').readFileSync(0)).access_token")
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/clientes

# 403 — seller intenta crear (no tiene la concesión)
curl -i -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"x","phone":"1","email":"x@x.com","password":"x"}' \
  http://localhost:4000/api/clientes

DoD del ISS-13

  • [ ] Todos los criterios de aceptación (18.1 … 18.6) cumplidos
  • [ ] GET /api/clientes pasa de SIN AUTH a exigir token (401 sin él)
  • [ ] POST /api/clientes con seller → 403; con admin → 201
  • [ ] Los controllers, services y repositories de Fase I no fueron modificados
  • [ ] npx tsc --noEmit sin errores y npm run dev arranca

Diagnóstico

Síntoma Causa probable Solución
GET /api/clientes sigue dando 200 sin token La ruta no se parcheó o el parche se hizo en otro archivo Revisa el *.routes.ts de esa feature: debe importar authenticate, authorize y montarlos antes del controller
authorize responde 401 en vez de 403 authenticate no dejó req.auth (montaje invertido o ausente) Montar siempre authenticate antes de authorize
Con token válido de seller, POST /api/clientes da 401 El token está caducado, la firma no cuadra o el usuario fue desactivado Generar un token fresco; comprobar users.status = active
seller recibe 403 en una lectura que debería poder hacer Falta o está inactiva la concesión de (GET, /api/clientes) para SELLER Insertar/reactivar la fila en resource_roles y/o role_users (es dato, no código)
admin recibe 403 La cadena RBAC está rota en algún eslabón (role_users, roles, resource_roles o resources inactivos) Recorrer la cadena y activar el eslabón que falte
Un permiso revocado sigue funcionando Imposible si la ruta usa authorize: los permisos se consultan en cada petición Verificar que la ruta realmente monta authorize
/api/clientes/abc devuelve 400 con token válido El parámetro no es un id válido; es validación de negocio, no de acceso Es el comportamiento correcto (paramId)
npx tsc --noEmit falla tras el parche Import mal resuelto (ruta del barrel) o req.auth sin tipar Revisar el import ../../auth/access y la extensión de tipos de Request

Pregunta que responde: si algo falla aquí, ¿por dónde empiezo a mirar?

Conexión con el resto del curso

  • Lo habilita el ISS-12. Sin filas en role_users y resource_roles no hay nada que consultar; authorize respondería 403 a todo el mundo. Este ISS es el que da sentido a aquel.
  • Habilita el ISS-14. /api/sesiones… se monta en modalidad JWT (solo authenticate), así que necesita que el middleware exista. Es el primer consumidor de la modalidad intermedia.
  • Piezas que reutiliza: shared/auth/jwt (extractBearerToken, verifyAccessToken), shared/auth/resource-match (normalizePath, isOperationGranted), shared/http/error-response (sendError), AppError, UsersRepository y ResourceRolesRepository.
  • Lo que no toca: controllers, services, repositories, modelos ni DTOs de Fase I. El contrato de la API tampoco cambia: solo cambian los códigos de estado para quien no está autorizado.
  • El cierre. El agregador routes/index.ts con las 7 features de auth se consolida en 18-cierre-auth.md, no aquí.

Glosario

  • Middleware: función de Express que recibe (req, res, next) y decide si la petición continúa. Encadenar middlewares es el mecanismo con el que se montan las modalidades.
  • authenticate: middleware de autenticación. Resuelve la identidad y la publica en req.auth.
  • authorize: middleware de autorización. Consulta la matriz RBAC y concede o deniega según la concesión.
  • req.auth: espacio tipado donde viaja { id, username, email, tokenId } entre middlewares y controllers.
  • Modalidad de acceso: combinación de middlewares que una ruta declara (OPEN, JWT, JWT + RBAC). Se decide por ruta, en el montaje.
  • 401 vs 403: 401 = no sé quién eres (autenticación); 403 = sé quién eres pero no puedes (autorización).
  • Deny by default: ausencia de concesión activa ⇒ 403. No existe permiso implícito.
  • RBAC: control de acceso basado en roles. Aquí, la cadena resources ← resource_roles → roles ← role_users → users.
  • Concesión efectiva: par (method, path) que el usuario alcanza por al menos una concesión activa de un rol activo asignado de forma activa.
  • normalizePath: función que da forma canónica a la URL para poder casarla con el patrón almacenado en la matriz.

Criterios de aceptación

Los del ISS, textuales, como checklist:

  • [ ] 18.1 authenticate valida el Bearer token, verifica algoritmo/issuer/audience/exp y carga el usuario activo en req.auth
  • [ ] 18.2 authorize resuelve (method, path) y busca concesión activa; deny by default → 403
  • [ ] 18.3 access/index.ts reexporta ambos middlewares
  • [ ] 18.4 las 5 features de negocio (clients, product-types, products, sales, product-sales) aplican authenticate, authorize
  • [ ] 18.5 documentadas las tres modalidades y qué códigos produce cada una
  • [ ] 18.6 sin token → 401; con token pero sin concesión → 403; con concesión → 200/201
  • [ ] npx tsc --noEmit OK

Evaluación

Preguntas de comprensión

  1. ¿Por qué authenticate no consulta roles ni permisos? Porque la autenticación y la autorización son responsabilidades distintas. Si authenticate también comprobara permisos, no podría existir la modalidad JWT pura (endpoints que solo exigen estar identificado, como las sesiones propias del ISS-14). Separarlas permite componer las tres modalidades reutilizando los mismos dos bloques.

  2. ¿Qué diferencia exacta hay entre un 401 y un 403 en este ISS? 401 lo emite authenticate y significa «no sé quién eres»: falta el token, es inválido/caducado o el usuario ya no está activo. 403 lo emite authorize y significa «sé quién eres, pero no tienes concesión para method + path». Un 403 siempre presupone identidad válida; por eso authorize responde 401 si no encuentra req.auth.

  3. ¿Por qué authenticate vuelve a consultar la base de datos si el token ya está firmado y verificado? Porque un JWT es autocontenido y sigue siendo criptográficamente válido después de que la cuenta se desactive. La revalidación (user.status === "active") es lo que hace que desactivar un usuario tenga efecto inmediato en lugar de esperar a que caduque el token.

  4. ¿Por qué revocar un permiso tiene efecto inmediato, sin esperar a que caduque el token? Porque los permisos no viajan dentro del token: authorize consulta la matriz en cada petición. Al cambiar el status de una fila, la siguiente petición ya ve el cambio. Si los permisos fueran claims del JWT, habría que esperar a la expiración.

  5. Explica el papel de normalizePath con un ejemplo. La matriz guarda patrones como /api/productos/:id, pero la petición real trae /api/productos/42. normalizePath deja la URL en una forma canónica para poder compararla con el patrón; sin normalizar, ninguna petición parametrizada casaría con su concesión y todo daría 403.

  6. ¿Por qué el parche de las rutas no toca controllers ni services? Porque en Express el acceso se decide con middlewares en el montaje de la ruta, no dentro del handler. El controller sigue recibiendo una Request y devolviendo una respuesta; qué se exige antes es una decisión de la capa de rutas. Conservar intactas las capas de negocio demuestra que la decisión se tomó en el lugar correcto.

  7. Un compañero añade una ruta nueva y, sin darse cuenta, todo el mundo puede usarla. ¿Qué revela eso? Que la ruta se montó en modalidad OPEN (sin middlewares). Con deny by default, la protección no es automática: hay que declarar authenticate, authorize. El diseño favorece cerrar por defecto si montas los middlewares, y este ISS hace explícito ese contrato de montaje.

Ejercicios

Ejercicio 1 — Clasifica por modalidad. De estas rutas, di cuál es OPEN, cuál JWT y cuál JWT + RBAC, y qué código esperarías al llamarlas sin token: POST /api/sesion/login, GET /api/sesiones, GET /api/clientes.

Respuesta razonada - `POST /api/sesion/login` → **OPEN**: es el punto de entrada; si exigiera token, nadie podría obtenerlo. - `GET /api/sesiones` → **JWT**: solo `authenticate`. Ver las propias sesiones deriva de estar autenticado, no de un permiso de la matriz (es el ISS-14). - `GET /api/clientes` → **JWT + RBAC**: `authenticate, authorize`, porque es una operación de negocio sujeta a la matriz. Sin token: las dos primeras protegidas devuelven 401; la OPEN responde a lo suyo (por ejemplo, 400 si faltan credenciales, pero nunca 401 por falta de token). Un `GET /api/clientes` con token válido sin concesión devolvería 403, no 401.

Ejercicio 2 — Diagnostica un 403 de más. Un usuario seller con token válido recibe 403 en GET /api/ventas, pero el ISS dice que SELLER tiene las 3 concesiones de sales. Enumera, en orden, los eslabones que revisarías.

Respuesta razonada La concesión es una cadena; un 403 significa que algún eslabón no está activo. Orden de revisión: 1. `role_users`: ¿existe la fila `SELLER`–usuario con `status = active`? 2. `roles`: ¿el rol está `active`? 3. `resource_roles`: ¿existe la concesión `SELLER`–`(GET, /api/ventas)` con `status = active`? 4. `resources`: ¿el recurso está `active` y su `method`/`path` casan con la petición (tras `normalizePath`)? Un detalle habitual: la petición es `GET` (mayúsculas ya normalizadas) y el `path` debe casar con el patrón almacenado. Si el recurso se sembró con otro verbo o con un `path` distinto, `isOperationGranted` no encuentra coincidencia y responde 403 aunque la intención fuera buena.

Ejercicio 3 — Justifica deny by default. Explica por qué denegar cuando no hay concesión es más seguro que permitir cuando no hay concesión, usando el caso de una ruta recién creada.

Respuesta razonada Una ruta recién creada todavía no tiene fila en la matriz. Con deny by default, esa ruta nace **cerrada**: el peor resultado de «no encontrar nada» es un 403, que es un fallo visible y corregible (se inserta la concesión). Con allow by default, esa misma ruta nacería **abierta**, y el peor resultado es un acceso indebido que nadie nota hasta que es tarde. Deny by default convierte el fallo en un problema de disponibilidad (molesto pero seguro) en lugar de un agujero de seguridad (silencioso e irreversible). Además, empuja la administración a la dirección correcta: conceder acceso es insertar una fila, nunca desplegar código.

GATE

Todos los comandos exactos del cierre del ISS. Empieza por arrancar el servidor:

npm run dev

Compila sin errores de tipos:

npx tsc --noEmit

Y ejecuta la batería de verificación de 18.6:

# 401 — sin token
curl -i http://localhost:4000/api/clientes

# 401 — token manipulado
curl -i -H "Authorization: Bearer no.es.un.jwt" http://localhost:4000/api/clientes

# 200 — seller lee
TOKEN=$(curl -s -X POST http://localhost:4000/api/sesion/login \
  -H "Content-Type: application/json" \
  -d '{"identifier":"seller","password":"Seller123!"}' | node -pe "JSON.parse(require('fs').readFileSync(0)).access_token")
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:4000/api/clientes

# 403 — seller intenta crear (no tiene la concesión)
curl -i -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"name":"x","phone":"1","email":"x@x.com","password":"x"}' \
  http://localhost:4000/api/clientes

Resultado esperado: 401 en las dos primeras, 200 en la tercera y 403 en la cuarta. Con un token de admin, el POST daría 201.

Checklist de cierre (DoD del ISS-13):

  • [ ] Todos los criterios de aceptación (18.1 … 18.6) cumplidos
  • [ ] GET /api/clientes pasa de SIN AUTH a exigir token (401 sin él)
  • [ ] POST /api/clientes con seller → 403; con admin → 201
  • [ ] Los controllers, services y repositories de Fase I no fueron modificados
  • [ ] npx tsc --noEmit sin errores y npm run dev arranca

Con el GATE en verde, el RBAC ha dejado de ser una matriz dormida y el proyecto está listo para el ISS-14 — Feature RefreshTokens, donde nace la gestión de la sesión persistida (hash SHA-256, rotación y detección de reúso).


Navegación de la ruta: ← ISS-12 · 🛠 Construir · ↑ Ruta Express · → ISS-13 · 🛠 Construir