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/:idconrequireAuthy 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:
- Autenticación: verificar identidad (token/sesión) y cargar al usuario.
- Autorización: decidir si ese usuario puede hacer X sobre Y.
- 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
- Por rol (rápida, medio burda): admin, support, user.
- Por ownership: el recurso es tuyo (
order.user_id == me). - 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
orderantes de autorizar. Si performance te aprieta, autoriza en DB:SELECT * FROM orders WHERE id = ? AND tenant_id = ?(y/ouser_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).

Screenshots sugeridos
- Captura de Postman/Insomnia mostrando:
401 Missing token403 Forbiddencon token válido200 OKcuando sí hay permiso
- Captura de un log estructurado (JSON) con campos
userId,role,tenantId,action,resourceId. - Captura de una tabla simple
roles,permissions,role_permissionso 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
tenantIdareq.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.).

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
tenantIden 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.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


