"¿JWT u OAuth?" es una de esas preguntas que se responden mal casi siempre, porque no son dos sabores del mismo mecanismo: resuelven problemas distintos. Elegir el patrón equivocado —o elegir el correcto pero implementarlo mal— es una de las formas más comunes en que las APIs quedan expuestas.
JWT: simple, pero con límites que hay que conocer
Un JWT no es en sí mismo "autenticación": es un contenedor de datos (claims) que el servidor acepta como válido porque viene firmado. Su estructura es header.payload.signature.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
A favor: es stateless (el servidor no necesita guardar sesiones), es compacto, y funciona bien entre dominios distintos.
En contra: una vez emitido, no se puede revocar antes de que expire. Si alguien deja la empresa o una clave se compromete, el token sigue siendo válido hasta que caduca. Tampoco existe un "logout real": el cliente simplemente borra el token localmente, pero el servidor no se entera.
OAuth 2.0: más complejo, pero pensado para poder revocar
OAuth 2.0 es delegación de autenticación: en vez de darle tu contraseña a una app, dejas que un proveedor de identidad (Google, Auth0, tu propio servidor de identidad) confirme quién eres.
Authorization Code Flow (el habitual en apps web y móvil): el usuario se autentica en el proveedor de identidad, este devuelve un código, y tu backend intercambia ese código por un token. La contraseña nunca pasa por tu aplicación.
1. Usuario: inicia sesión
2. Tu app redirige al proveedor de identidad
3. El proveedor autentica al usuario (la contraseña se queda ahí, no en tu app)
4. El proveedor devuelve un código a tu app
5. Tu backend intercambia ese código por un access token
6. El usuario queda autenticado en tu app
Client Credentials Flow es la variante para comunicación servidor-a-servidor: dos sistemas intercambian credenciales directamente para obtener un token, sin que haya un usuario humano de por medio.
La ventaja central de OAuth frente a un JWT plano es que el proveedor de identidad puede revocar tokens al instante, soporta scopes granulares ("este token puede leer pero no escribir"), y separa el access token (vida corta, minutos) del refresh token (vida larga), acotando la ventana de exposición si algo se filtra.
¿Cuál usar, entonces?
- API interna con usuarios de confianza: un JWT de vida corta suele bastar.
- API pública con usuarios externos: OAuth, porque la capacidad de revocar pesa más que la simplicidad.
- App móvil: lo habitual es un híbrido — OAuth para el login, JWT de corta duración para las peticiones posteriores.
- Comunicación servidor a servidor (por ejemplo, webhooks de un proveedor de pagos): Client Credentials Flow, porque es auditable y revocable.
- Dispositivos embebidos con recursos limitados: a veces una API key con whitelist de IP es más razonable que cargar con el overhead de OAuth.
Los errores que de verdad importan
Secretos hardcodeados en el código:
// Nunca
const SECRET = "super-secret-key-123";
// Así
const SECRET = process.env.JWT_SECRET;
No rotar los secretos. Si la clave de firma se filtra —en un repositorio, en un log— y nunca se rota, cualquiera puede forjar tokens válidos indefinidamente.
Meter datos sensibles dentro del JWT. El payload de un JWT es visible para quien tenga el token, aunque esté firmado no está cifrado:
// Mal: el cliente puede leer esto (y modificarlo si no verificas la firma)
{ "user_id": "123", "is_admin": true, "salary": "100000" }
// Mejor: el servidor sigue siendo la fuente de verdad
{ "user_id": "123", "permissions": ["read:posts", "write:comments"] }
Expiración demasiado larga. Un token con expires_in de 24 horas significa que, si se compromete, el atacante tiene acceso durante 24 horas. Con 15 minutos y refresh tokens, esa ventana se reduce drásticamente.
Servir esto sin HTTPS. Un token viajando en texto plano por HTTP es capturable con cualquier ataque man-in-the-middle. HTTPS no es opcional.
Ejemplo de implementación razonable
const jwt = require('jsonwebtoken');
const SECRET = process.env.JWT_SECRET;
function generateToken(userId) {
return jwt.sign({ user_id: userId }, SECRET, { expiresIn: '15m' });
}
function verifyToken(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token' });
try {
const decoded = jwt.verify(token, SECRET);
req.userId = decoded.user_id;
next();
} catch (e) {
res.status(403).json({ error: 'Invalid token' });
}
}
app.post('/login', (req, res) => {
// Verificar user/password contra la base de datos
const token = generateToken(user.id);
res.json({ token });
});
app.get('/profile', verifyToken, (req, res) => {
res.json(db.users.findById(req.userId));
});
Herramientas para testear y depurar
Postman permite importar flujos OAuth y probar endpoints autenticados sin escribir código.
jwt.io es el decodificador de referencia para inspeccionar qué claims lleva un token.
OWASP ZAP es un escáner de seguridad gratuito que detecta automáticamente fallos comunes de autenticación.
Checklist antes de ir a producción
- El secreto de firma vive en variables de entorno, nunca en el código
- Expiración corta (15 minutos o menos) con refresh tokens
- HTTPS obligatorio en todos los endpoints
- Rotación periódica de secretos
- Registro de logins y accesos para auditoría
Fuentes:






Comentarios (0)
Deja un comentario
No hay comentarios aún. ¡Sé el primero en comentar!