From n00b to ZeroCool / Profesionalización

Autenticación vs autorización: el bug invisible que te rompe producción

Diferencia real entre autenticación y autorización, con ejemplos y un patrón práctico para implementarlas en tu backend sin abrir hoyos de seguridad.

Lo que vale la pena leer aquí

Te vas al endpoint. Trae JWT. Responde 200. Y tú: “pues está autenticado”.

Intro con gancho

Son las 11:47 pm. Ya casi cierras la laptop (la vieja, la que ya suena como avión) y te cae el mensaje: “Oye… cualquier usuario puede ver pedidos de otros. ¿Eso es normal?”

Te vas al endpoint. Trae JWT. Responde 200. Y tú: “pues está autenticado”.

Ahí está el pecado: confundiste autenticación con autorización.

En backend ese error no truena con un stacktrace bonito. Truena con screenshot en Slack, cliente encabronado, y el clásico “¿quién subió eso a production?”. Y si eres freelance, peor: el cambio “rápido” que cotizaste barato se vuelve una semana de rollback, parches y llamadas.

Qué te vas a llevar

  • Diferencia real (de la que te salva en el deploy) entre autenticación y autorización.
  • Cómo se ve el bug típico cuando mezclas ambas.
  • Un patrón sano para APIs: AuthN en middleware + AuthZ en policy.
  • Cómo modelar permisos: roles vs permisos finos vs ownership.
  • Checklist para no abrir hoyos (y no romperte el workflow del equipo).

Contexto práctico: lo que sí pasa en la chamba

Piensa en un sistema de pedidos:

  • AuthN (autenticación) responde: “¿Quién eres?”
  • AuthZ (autorización) responde: “¿Qué tienes permitido hacer?”

El bug clásico:

  • Proteges /orders/:id con requireAuth y ya.
  • En el handler haces Order.findById(id) y lo regresas.
  • Resultado: cualquier usuario autenticado puede leer cualquier order si adivina IDs o enumera.

Esto pasa un montón cuando:

  • hay prisa por sacar MVP,
  • se copia/pega un middleware “para que ya pida token”,
  • se asume que “si ya inició sesión, ya puede”.

Otra escena muy México-real: soporte necesita “ver pedidos” para ayudar. Creas un rol support en corto. Nadie define límites. De pronto soporte puede reembolsar, borrar o exportar media base “sin querer”. No porque sean malos, sino porque el backend no puso barandales.

Guía principal: separa AuthN y AuthZ sin complicarte la vida

Un patrón que aguanta crecimiento y evita sustos:

  1. Autenticación: verificar identidad (token/sesión) y cargar al usuario.
  2. Autorización: decidir si ese usuario puede hacer X sobre Y.
  3. Auditoría: dejar evidencia cuando se hace algo sensible.

1) Autenticación: “sí es quien dice ser”

Ejemplo con Node.js + Express + JWT (simplificado). No es para casarte con JWT; es para que quede clara la separación de responsabilidades.

// middleware/requireAuth.js
import jwt from "jsonwebtoken";

export function requireAuth(req, res, next) {
  const header = req.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing token" });
  }

  const token = header.slice("Bearer ".length);
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);

    // AuthN: identidad + claims básicos
    req.auth = {
      userId: payload.sub,
      roles: payload.roles || [],
      tenantId: payload.tenantId,
    };

    return next();
  } catch (e) {
    return res.status(401).json({ error: "Invalid token" });
  }
}

Decisiones que te evitan bugs raros:

  • No metas permisos detallados en el JWT si cambian seguido. Los tokens viven más de lo que crees. Revocas un permiso, el token sigue válido… y ya valió.
  • JWT jala bien para stateless, pero en B2B tarde o temprano te piden “logout global”, “bloquear usuario”, “rotación de llaves”, etc. Eso empuja a sesiones o a una denylist.

2) Autorización: “aunque seas tú, ¿puedes hacer esto?”

Aquí se gana o se pierde production. La idea es sacar esa lógica del handler y ponerla en políticas (policy/abilities) que puedas testear.

Modelo mental: tres niveles de autorización

  1. Por rol (rápida, medio burda): admin, support, user.
  2. Por ownership: el recurso es tuyo (order.user_id == me).
  3. Permisos finos / scopes: orders:read, orders:refund, users:export.

En sistemas reales casi siempre combinas 1 + 2, y metes 3 cuando ya duele (cuando empiezan los “solo Juan puede ver X pero no Y” y te piden cambios con deadline para ayer).

Ejemplo: policy para leer una orden

// authz/orderPolicy.js
export function canReadOrder(auth, order) {
  if (!auth?.userId) return false;

  // Admin ve todo
  if (auth.roles.includes("admin")) return true;

  // Soporte ve todo, pero solo dentro de su tenant
  if (auth.roles.includes("support")) {
    return auth.tenantId && auth.tenantId === order.tenantId;
  }

  // Usuario normal solo ve sus órdenes
  return order.userId === auth.userId;
}

Y en tu endpoint:

import { requireAuth } from "./middleware/requireAuth.js";
import { canReadOrder } from "./authz/orderPolicy.js";

app.get("/orders/:id", requireAuth, async (req, res) => {
  const order = await Orders.findById(req.params.id);
  if (!order) return res.status(404).json({ error: "Not found" });

  if (!canReadOrder(req.auth, order)) {
    // 403: autenticado pero sin permiso
    return res.status(403).json({ error: "Forbidden" });
  }

  return res.json(order);
});

Tradeoff real:

  • Sí: haces una consulta para traer order antes de autorizar. Si performance te aprieta, autoriza en DB:
    • SELECT * FROM orders WHERE id = ? AND tenant_id = ? (y/o user_id = ?) y si no regresa nada, respondes 404/403 según tu política.
  • ¿404 o 403? Depende si quieres evitar enumeración. Muchas apps regresan 404 para recursos que existen pero no te pertenecen. Solo sé consistente.

3) Reglas de oro para que no se te salga el diablo

A) AuthN vive en middleware; AuthZ vive pegado al caso de uso

Si metes autorización en un middleware genérico tipo requireRole('admin'), al rato tienes endpoints con 10 excepciones y “pero este sí lo puede hacer support si…”. Se vuelve talacha eterna.

Mejor:

  • Middleware: valida token y construye req.auth.
  • Handler/service: aplica policy con el contexto real del recurso.

B) No confíes en el frontend

El frontend puede esconder botones, pero un atacante pega directo al endpoint. Lo mismo para el “backoffice” de soporte: si está expuesto, cuenta como superficie de ataque.

C) Multi-tenant: el tenantId es frontera de seguridad

Si vendes SaaS B2B (aunque sea “chiquito”), el tenant se vuelve una pared.

  • AuthN: token trae tenantId.
  • AuthZ: cada query importante filtra por tenantId.

D) Admin no significa “todo para siempre”

He visto admin usado como comodín para soporte y para el CTO. Un día alguien corre un script “para ayudar” y borra 10k registros. Y no hay rollback que te regrese la confianza del cliente.

Recomendación pragmática:

  • admin: acceso total.
  • support: leer casi todo, mutar poco.
  • Acciones destructivas: permiso explícito (danger:write) o step-up (reautenticación/2FA/approval).
Autenticación vs autorización: el bug invisible que te rompe producción - visual explicativa 1
Visual de apoyo: Intro con gancho

Screenshots sugeridos

  • Captura de Postman/Insomnia mostrando:
    • 401 Missing token
    • 403 Forbidden con token válido
    • 200 OK cuando sí hay permiso
  • Captura de un log estructurado (JSON) con campos userId, role, tenantId, action, resourceId.
  • Captura de una tabla simple roles, permissions, role_permissions o un diagrama rápido de “User -> Role -> Permission”.

Errores comunes (los que te hacen perder horas) + solución

1) Responder 401 cuando en realidad es 403

Síntoma: el frontend te manda a login aunque el usuario sí está logueado.

  • 401 = no autenticado (no hay identidad válida)
  • 403 = autenticado pero no autorizado

Solución: estandariza tu manejo de errores.

// 401 si no hay token o es inválido
// 403 si token válido pero policy falla

2) Autorización “por UI”: esconder botones

Síntoma: “no sale el botón de borrar, entonces nadie puede borrar”.

Solución: valida siempre en backend. La UI es comodidad, no seguridad.

3) Poner permisos en el JWT y luego cambiarlos en DB

Síntoma: cambias roles/permisos y el usuario sigue pudiendo hacer cosas hasta que expire el token.

Solución:

  • tokens cortos + refresh tokens, o
  • guardar permisos en DB y consultarlos (con caché), o
  • versionar permisos (permVersion) y forzar re-login cuando cambie.

4) Olvidar el tenant en queries

Síntoma: fuga de datos entre empresas (“vi pedidos de otro cliente”). Eso ya es incidente serio.

Solución:

  • Añade tenantId a req.auth.
  • Revisa queries críticas: siempre filtra por tenant.
  • Si usas ORM, considera scopes/global filters.

5) Mezclar autorización con lógica de negocio

Síntoma: tu servicio de “refund” tiene 20 ifs: rol, ownership, estado, monto, canal… y cada pull request rompe algo.

Solución: separa:

  • Policy: quién puede intentar.
  • Validaciones de negocio: si procede (estado, reglas, etc.).
Autenticación vs autorización: el bug invisible que te rompe producción - visual explicativa 2
Visual de apoyo: Qué te vas a llevar

Checklist final (para antes del merge y antes del deploy)

  • Cada endpoint sensible distingue 401 vs 403.
  • AuthN solo valida identidad y carga req.auth (userId, roles, tenantId).
  • AuthZ existe como policy/ability y se llama en cada caso de uso.
  • Recursos multi-tenant filtran por tenantId en DB o en policy + query.
  • Endpoints de lectura validan ownership (no solo “está logueado”).
  • Acciones peligrosas (delete/refund/export) tienen permisos explícitos y logs.
  • Hay logs/auditoría mínima: who/what/when/resource/result.
  • Pruebas básicas: un usuario no puede leer/modificar recursos ajenos.

FAQ

1) ¿Autenticación y autorización pueden vivir en el mismo middleware?

Pueden, pero se vuelve un monstruo rápido. AuthN es genérico; AuthZ depende del recurso y del caso de uso. Separarlas te da claridad y menos “excepciones” escondidas.

2) ¿Qué es mejor: roles o permisos?

Para arrancar: roles + ownership. Cuando el producto crece (y soporte/ops pide accesos específicos), metes permisos finos. Si empiezas con permisos desde el día 1, te puedes ahogar en talacha.

3) ¿JWT siempre es mala idea?

No. JWT funciona bien si controlas expiración, rotación de llaves y no metes claims que cambian seguido. Lo peligroso no es JWT; es pensar que “token = acceso total”.

4) ¿Por qué a veces conviene responder 404 en vez de 403?

Para evitar enumeración de recursos (que alguien infiera qué IDs existen). Muchas APIs regresan 404 cuando el recurso no te pertenece. Solo mantén consistencia y define el criterio.

5) ¿Cómo pruebo autorización sin volverme loco?

Empieza con tests de casos críticos:

  • usuario A no lee orden de usuario B
  • support solo dentro de su tenant
  • admin sí puede
  • acciones destructivas requieren permiso

Con 10–15 tests bien elegidos se te caen la mayoría de fugas.

Siguiente episodio

Tu sistema ya sabe quién es quién y qué puede hacer.
Lo que sigue en Backend Underworld: sesiones vs tokens — cómo elegir sin arrepentirte cuando ya tienes usuarios pagando.