Autenticación moderna: JWT, OAuth2 y sesiones explicados
Introducción
“¿Cómo sabe mi API quién está haciendo la petición?” Es la pregunta que aparece en cuanto pasas de un proyecto personal a algo con usuarios reales. Y la respuesta no es tan sencilla como parece.
Hay tres enfoques dominantes: sesiones con cookies, JWT y OAuth2. Cada uno resuelve un problema distinto, pero se confunden constantemente. Este artículo los separa, los explica y te ayuda a elegir.
Autenticación vs Autorización
Antes de entrar en mecanismos, hay que distinguir dos conceptos que se mezclan todo el tiempo:
- Autenticación (AuthN): verificar quién eres. Login con usuario y contraseña, huella digital, código OTP.
- Autorización (AuthZ): verificar qué puedes hacer. ¿Tienes permiso para borrar ese recurso? ¿Eres admin o usuario básico?
JWT y OAuth2 participan en ambos, pero de formas diferentes.
Sesiones: el enfoque clásico
El modelo que lleva décadas funcionando:
- El usuario envía credenciales (POST
/login). - El servidor verifica, crea una sesión en memoria o base de datos, y devuelve un Session ID como cookie.
- En cada petición siguiente, el navegador envía automáticamente esa cookie.
- El servidor busca el Session ID, recupera los datos del usuario y responde.
Ventajas
- El servidor tiene control total: puede invalidar la sesión en cualquier momento.
- La cookie se configura como
HttpOnly,SecureySameSite, lo que limita ataques XSS y CSRF. - Sencillo de implementar con cualquier framework (Django, Express, Laravel).
Desventajas
- Requiere almacenamiento en el servidor (memoria, Redis, base de datos).
- Escalar horizontalmente implica compartir ese almacenamiento entre instancias.
- No funciona bien para APIs consumidas por móviles o terceros que no manejan cookies.
JWT: tokens autocontenidos
JSON Web Token es un estándar (RFC 7519) que empaqueta información del usuario directamente en el token. El servidor no necesita consultar ninguna base de datos para validarlo.
Estructura de un JWT
Un JWT tiene tres partes separadas por puntos:
header.payload.signature
- Header: algoritmo de firma y tipo de token.
- Payload: los claims (datos). Por ejemplo:
sub(usuario),exp(expiración),role. - Signature: firma criptográfica que garantiza que nadie ha alterado el contenido.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzQyIiwiZXhwIjoxNzExMjAwMDAwfQ.firma_aqui
Nota importante: el payload está codificado en Base64, no cifrado. Cualquiera puede leerlo. Nunca pongas datos sensibles (contraseñas, tarjetas) dentro del payload.
Flujo típico
- El usuario envía credenciales.
- El servidor verifica y genera un JWT firmado con una clave secreta.
- El cliente almacena el token (localStorage, cookie, memoria).
- En cada petición, envía el token en el header
Authorization: Bearer <token>. - El servidor verifica la firma y la expiración sin consultar base de datos.
Ejemplo de verificación (pseudocódigo)
import jwt
SECRET = "clave-super-secreta"
def verificar_token(token: str):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
return payload # {"sub": "user_42", "role": "admin", "exp": ...}
except jwt.ExpiredSignatureError:
raise Error("Token expirado")
except jwt.InvalidTokenError:
raise Error("Token inválido")
Ventajas
- Stateless: el servidor no guarda estado. Escala horizontalmente sin esfuerzo.
- Portable: funciona para SPAs, apps móviles, microservicios, APIs de terceros.
- Flexible: puedes incluir claims personalizados (roles, permisos, tenant).
El gran problema: revocación
Si emites un JWT con 24 horas de vida y el usuario cambia su contraseña o es baneado, el token sigue siendo válido hasta que expire. No hay forma nativa de invalidarlo.
Soluciones comunes:
- Tokens de vida corta (15-30 minutos) + refresh token almacenado en servidor.
- Blacklist de tokens: guardas tokens revocados en Redis. Funciona, pero pierdes parte de la ventaja stateless.
OAuth2: delegar la autenticación
OAuth2 no es un mecanismo de login. Es un protocolo de autorización delegada. Resuelve un problema específico: “permitir que una aplicación acceda a recursos de un usuario en otro servicio, sin compartir contraseñas”.
Cuando haces “Login con Google” o “Conectar con GitHub”, estás usando OAuth2.
Los actores
- Resource Owner: el usuario (tú).
- Client: la aplicación que quiere acceder (tu app).
- Authorization Server: quien emite los tokens (Google, GitHub).
- Resource Server: donde están los datos protegidos (API de Google).
Flujo Authorization Code (el más común y seguro)
- Tu app redirige al usuario al Authorization Server (Google).
- El usuario se autentica directamente con Google.
- Google redirige de vuelta a tu app con un authorization code.
- Tu backend intercambia ese code por un access token (servidor a servidor).
- Con el access token, tu app accede a los recursos permitidos.
Usuario → Tu App → Google Auth → Usuario acepta
↓
Tu App (backend) ← code ← Google redirige
↓
Tu App → Google Token endpoint (envía code + client_secret)
↓
Tu App ← access_token + refresh_token
¿Y OpenID Connect?
OIDC es una capa sobre OAuth2 que añade autenticación. OAuth2 solo te da un access token para acceder a recursos; OIDC además te devuelve un ID token (un JWT) con datos del usuario como nombre, email y foto.
Si necesitas “Login con Google”, lo que realmente usas es OIDC sobre OAuth2.
Comparativa práctica
| Criterio | Sesiones | JWT | OAuth2/OIDC |
|---|---|---|---|
| Estado en servidor | Sí | No (stateless) | Depende |
| Revocación inmediata | Sí | Difícil | Sí (authorization server) |
| Ideal para | Web tradicional, SSR | APIs, microservicios, SPAs | Login social, acceso delegado |
| Complejidad | Baja | Media | Alta |
| Escalabilidad | Requiere store compartido | Nativa | Nativa |
Cuándo usar cada uno
Sesiones cuando:
- Tu app es un monolito web con server-side rendering.
- Necesitas invalidación instantánea.
- No expones API pública.
JWT cuando:
- Construyes una API REST consumida por múltiples clientes.
- Tienes arquitectura de microservicios que necesitan verificar identidad sin consultar un servicio central.
- Aceptas tokens de vida corta + refresh tokens.
OAuth2/OIDC cuando:
- Quieres login social (Google, GitHub, Microsoft).
- Tu plataforma permite a terceros acceder a datos de tus usuarios.
- Necesitas delegar autenticación a un proveedor de identidad externo.
Errores comunes
- Guardar JWT en localStorage sin protección: vulnerable a XSS. Preferir cookies
HttpOnlyo memoria. - JWT con vida de días o semanas: si lo comprometen, el atacante tiene acceso prolongado.
- No validar la firma del JWT: aceptar tokens sin verificar la clave es como no tener autenticación.
- Implementar OAuth2 con el flujo Implicit: está deprecado. Usar Authorization Code + PKCE.
- Confundir OAuth2 con autenticación: OAuth2 solo autoriza acceso a recursos. Para autenticación necesitas OIDC.
- Secrets en el frontend: el
client_secretde OAuth2 nunca debe estar en código JavaScript del cliente.
Checklist de seguridad
- Tokens de vida corta (15-30 min) con refresh token rotativo.
- Siempre HTTPS en producción.
- Cookies con flags
HttpOnly,Secure,SameSite=Strict. - Validar firma, expiración y audiencia (
aud) en cada petición. - Rotar secretos y claves de firma periódicamente.
- Implementar logout real: invalidar refresh token en servidor.
- Activar MFA como capa adicional.
Conclusión
No hay una solución universal. Las sesiones siguen siendo la opción más sólida para webs tradicionales. JWT brilla en arquitecturas distribuidas cuando se gestiona bien la vida del token. OAuth2/OIDC es imprescindible si delegas autenticación o expones tu plataforma a terceros.
Lo importante es entender qué resuelve cada uno y no mezclarlos por moda. La seguridad no se improvisa: se diseña.