Autenticación versus autorización: dónde encaja cada una en su código
La autenticación demuestra quién eres, la autorización decide qué puedes hacer. Dónde se ubica cada una en el middleware, 401 frente a 403, y los errores que surgen al confundirlas.
Fuentes: Authentication vs Authorization: The Distinction Most Developers Get Wrong
Dos palabras, casi intercambiables al leerlas rápidamente, pero cada una regula una parte completamente diferente de la lógica de su sistema.
Si alguna vez ha devuelto un código 401 en una situación que en realidad requería un 403, o ha implementado un flujo de inicio de sesión y declarado que la autenticación estaba completada sin tocar en absoluto la lógica de permisos, ya se ha encontrado con las dificultades que surgen al confundir estas dos conceptos. Vamos a aclararlos de una vez por todas.
Autenticación: Verificación de identidad
Autenticación es el paso en el que el sistema confirma que un usuario realmente es quien dice ser. Responde a una sola pregunta: ¿esta identidad es válida?
Es un evento único que suele ocurrir al iniciar sesión y genera algún tipo de credencial: una sesión, un token o una cookie, que permite al resto de la aplicación confiar en quien llama sin tener que volver a validar la contraseña en cada solicitud.
Las formas típicas de implementar la autenticación incluyen:
- Autenticación basada en contraseña — las credenciales enviadas se comparan con un valor hashado almacenado
- Autenticación basada en token (JWT) — se genera un token firmado tras un inicio de sesión exitoso, el cual se verifica en cada llamada posterior
- OAuth 2.0 / OpenID Connect — la verificación de identidad se delega a un proveedor externo como Google o GitHub
- Clausulas API — una cadena secreta fija que identifica a un cliente o servicio específico
Así es como se ve un intercambio de autenticación estándar:
POST /api/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "hashed_and_verified_serverside"
}
Response: 200 OK
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
El servidor valida las credenciales enviadas y, una vez que estas son correctas, devuelve un token. A partir de ese momento, este token sirve como prueba de identidad del usuario en cada solicitud:
GET /api/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
El middleware encargado de validar ese token generalmente se ve así:
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) return res.status(401).json({ error: 'Invalid or expired token' });
req.user = decoded;
next();
});
}
Preste atención a lo que este middleware deja deliberadamente de incluir. En ningún momento decide qué acciones puede realizar el usuario. Simplemente confirma que el token es legítimo y adjunta la carga útil decodificada al objeto de solicitud. Nada más entra dentro del alcance de las tareas de autenticación.
Autorización: Verificación de permisos
Autorización entra en juego solo después de que la autenticación ya haya tenido éxito. Responde a una pregunta completamente distinta: ¿esta identidad verificada tiene permiso para llevar a cabo esta acción en particular?
Algunos de los enfoques comunes para estructurar la autorización:
- Control de acceso basado en roles (RBAC) — los permisos se asignan a roles como
admin,editoroviewer
read:messages o write:filesfunction authorize(requiredRole) {
return (req, res, next) => {
if (req.user.role !== requiredRole) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
res.json({ data: 'sensitive admin data' });
});
Estas son dos funciones de middleware distintas que manejan dos tareas diferentes, ejecutadas en un orden fijo: authenticate determina quién es el solicitante, y authorize decide si a ese solicitante se le permite continuar. Una solicitud puede superar la primera verificación y aún así ser rechazada por la segunda; dicho rechazo no tiene nada que ver con la identidad del usuario.
Los códigos de estado que realmente reflejan esta diferencia
Aquí es donde la distinción teórica se convierte en algo que se puede verificar literalmente en el código.
401 Unauthorized — el nombre es engañoso, pero este código indica en realidad un problema de autenticación. Significa que el servidor no recibió ninguna credencial, o que las que recibió (incluido un token) son inválidas o han expirado. En este punto, el servidor no tiene idea confirmada de quién está realizando la solicitud.
403 Forbidden — este código indica un problema de autorización. El servidor ya ha determinado quién es el solicitante; simplemente no permite que esa identidad realice la acción solicitada o acceda al recurso pedido.
// Authentication failure — identity unknown or invalid
res.status(401).json({ error: 'Invalid credentials' });
// Authorization failure — identity known, permission denied
res.status(403).json({ error: 'You do not have access to this resource' });
Enviar un 401 cuando en realidad se trata de un problema de permisos es un error que ocurre constantemente. Esto indica al cliente que inicie sesión nuevamente, cuando el problema real es que el inicio de sesión está perfectamente bien: la cuenta simplemente carece del rol requerido. Solo ese error puede convertir lo que debería ser una solución rápida en una larga y errónea sesión de depuración del lado del cliente, persiguiendo un problema de inicio de sesión que nunca existió.
Dónde se presenta esto en arquitecturas reales
Los payloads de JWT suelen combinar estos aspectos sin que nadie lo intente. Un token está diseñado para contener datos de identidad, como sub, email, iat y exp. Determinar si también debe incluir datos de permisos, como role o scope, es una decisión de diseño que depende del sistema. Incluir directamente los roles en el token es práctico y evita búsquedas adicionales, pero conlleva un costo real: cualquier cambio en los permisos de un usuario no se aplicará hasta que se emita nuevamente ese token. Se trata de un verdadero compromiso, no de una solución rápida sin inconvenientes.
{
"sub": "user_12345",
"email": "user@example.com",
"role": "editor",
"iat": 1710000000,
"exp": 1710003600
}
Los sistemas construidos alrededor de sesiones del lado servidor tienden a mantener estas dos funciones lo más separadas posible. El identificador de sesión se encarga de la autenticación, mientras que los permisos se obtienen directamente de una base de datos en cada solicitud. Esto evita el problema de obsolescencia mencionado anteriormente, pero a costa de una llamada adicional a la base de datos por solicitud.
En arquitecturas de microservicios, la autenticación suele estar centralizada: se gestiona una sola vez por un gateway o un proveedor de identidades como Auth0, Keycloak o AWS Cognito, el cual emite un token. Sin embargo, la autorización generalmente se deja en manos de cada servicio individual, el cual lee los datos contenidos en ese token y aplica sus propias reglas. Como resultado, un mismo evento de autenticación puede ser evaluado de manera diferente por diez servicios separados, cada uno decidiendo de forma independiente qué acciones está permitido realizar con esa identidad.
La regla que previene la mayoría de los errores de seguridad
La autenticación debe realizarse antes que la autorización, y las dos nunca deben combinarse en una sola verificación.
// Correct: sequential, separate concerns
app.post('/api/posts/:id/delete', authenticate, authorize('editor'), deleteHandler);
// Wrong: conflating identity with permission in one step
app.post('/api/posts/:id/delete', (req, res) => {
if (req.headers.authorization === 'valid-token-and-also-admin') {
// fragile, unscalable, and impossible to audit correctly
}
});
Un sistema que autentica de manera rigurosa pero omite la autorización confirma la identidad sin errores y luego permite que cualquier identidad verificada haga lo que desee; por lo tanto, el compromiso de una sola cuenta con pocos privilegios equivale efectivamente a un compromiso total. Por el contrario, un sistema con una autorización finamente ajustada sobre una autenticación débil presenta el problema opuesto: permisos cuidadosamente definidos que protegen una puerta a través de la cual cualquiera puede pasar sin ser detenido.
Ambas capas deben ser sólidas, aplicarse en el orden correcto y tratarse como aspectos separados. Esa es toda la diferencia; una vez que se refleje adecuadamente en sus códigos de estado, su cadena de middleware y en la forma en que diseña sus tokens, toda una categoría de errores de seguridad simplemente desaparece.