SincroDev Logo SincroDev

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:

  1. El usuario envía credenciales (POST /login).
  2. El servidor verifica, crea una sesión en memoria o base de datos, y devuelve un Session ID como cookie.
  3. En cada petición siguiente, el navegador envía automáticamente esa cookie.
  4. 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, Secure y SameSite, 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

  1. El usuario envía credenciales.
  2. El servidor verifica y genera un JWT firmado con una clave secreta.
  3. El cliente almacena el token (localStorage, cookie, memoria).
  4. En cada petición, envía el token en el header Authorization: Bearer <token>.
  5. 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)

  1. Tu app redirige al usuario al Authorization Server (Google).
  2. El usuario se autentica directamente con Google.
  3. Google redirige de vuelta a tu app con un authorization code.
  4. Tu backend intercambia ese code por un access token (servidor a servidor).
  5. 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

CriterioSesionesJWTOAuth2/OIDC
Estado en servidorNo (stateless)Depende
Revocación inmediataDifícilSí (authorization server)
Ideal paraWeb tradicional, SSRAPIs, microservicios, SPAsLogin social, acceso delegado
ComplejidadBajaMediaAlta
EscalabilidadRequiere store compartidoNativaNativa

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

  1. Guardar JWT en localStorage sin protección: vulnerable a XSS. Preferir cookies HttpOnly o memoria.
  2. JWT con vida de días o semanas: si lo comprometen, el atacante tiene acceso prolongado.
  3. No validar la firma del JWT: aceptar tokens sin verificar la clave es como no tener autenticación.
  4. Implementar OAuth2 con el flujo Implicit: está deprecado. Usar Authorization Code + PKCE.
  5. Confundir OAuth2 con autenticación: OAuth2 solo autoriza acceso a recursos. Para autenticación necesitas OIDC.
  6. Secrets en el frontend: el client_secret de 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.

Lecturas relacionadas