seguridadautenticaciónjwtauth

Autenticación Moderna: JWT vs Session vs Magic Links

Por Binary Core

La autenticación es uno de los componentes más críticos de cualquier aplicación. En Binary Core, hemos implementado las tres estrategias principales: JWT, sessions y magic links. Aquí te mostramos cuándo usar cada una y cómo implementarlas correctamente.

JWT (JSON Web Tokens)

Cuándo Usar JWT

Los JWT son ideales para:

  • APIs stateless que escalan horizontalmente
  • Aplicaciones móviles y SPAs
  • Microservicios que necesitan compartir identidad
  • Sistemas con múltiples orígenes de autenticación

Implementación Básica

typescript
// lib/jwt.ts import { SignJWT, jwtVerify } from 'jose'; const SECRET_KEY = new TextEncoder().encode( process.env.JWT_SECRET || 'tu-secret-key-muy-segura' ); export async function createToken(payload: { userId: string; email: string }) { const token = await new SignJWT({ ...payload }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('1h') .sign(SECRET_KEY); return token; } export async function verifyToken(token: string) { try { const { payload } = await jwtVerify(token, SECRET_KEY); return payload; } catch (error) { return null; } }

API Route con JWT

typescript
// app/api/auth/login/route.ts import { createToken } from '@/lib/jwt'; import { NextResponse } from 'next/server'; export async function POST(req: Request) { const { email, password } = await req.json(); const user = await authenticateUser(email, password); if (!user) { return NextResponse.json({ error: 'Credenciales inválidas' }, { status: 401 }); } const token = await createToken({ userId: user.id, email: user.email }); return NextResponse.json({ token, user }); }

Middleware de Verificación

typescript
// middleware.ts import { verifyToken } from '@/lib/jwt'; import { NextResponse } from 'next/server'; export async function middleware(req: Request) { const token = req.headers.get('authorization')?.replace('Bearer ', ''); if (!token) { return NextResponse.json({ error: 'No autorizado' }, { status: 401 }); } const payload = await verifyToken(token); if (!payload) { return NextResponse.json({ error: 'Token inválido' }, { status: 401 }); } // Añadir user info al request const requestHeaders = new Headers(req.headers); requestHeaders.set('x-user-id', payload.userId as string); return NextResponse.next({ request: { headers: requestHeaders }, }); }

Ventajas y Desventajas

Ventajas:

  • Stateless: no requiere almacenamiento en servidor
  • Escalabilidad horizontal trivial
  • Funciona bien en arquitecturas de microservicios
  • Contiene información codificada (payload)

Desventajas:

  • No se pueden revocar fácilmente
  • Tamaño del token crece con el payload
  • Vulnerable si la secret key se compromete
  • Requiere HTTPS obligatorio

Sessions Tradicionales

Cuándo Usar Sessions

Las sessions son ideales para:

  • Aplicaciones SSR tradicionales
  • Cuando necesitas revocación inmediata
  • Control granular de sesiones activas
  • Aplicaciones con requisitos de seguridad estrictos

Implementación con Next.js

typescript
// lib/session.ts import { cookies } from 'next/headers'; import { SignJWT, jwtVerify } from 'jose'; const SESSION_SECRET = new TextEncoder().encode( process.env.SESSION_SECRET || 'tu-session-secret' ); export async function createSession(userId: string) { const session = await new SignJWT({ userId }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(SESSION_SECRET); cookies().set('session', session, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', maxAge: 60 * 60 * 24 * 7, // 7 días path: '/', }); } export async function getSession() { const session = cookies().get('session')?.value; if (!session) return null; try { const { payload } = await jwtVerify(session, SESSION_SECRET); return payload as { userId: string }; } catch { return null; } } export function deleteSession() { cookies().delete('session'); }

API Route con Sessions

typescript
// app/api/auth/login/route.ts import { createSession } from '@/lib/session'; import { NextResponse } from 'next/server'; export async function POST(req: Request) { const { email, password } = await req.json(); const user = await authenticateUser(email, password); if (!user) { return NextResponse.json({ error: 'Credenciales inválidas' }, { status: 401 }); } await createSession(user.id); return NextResponse.json({ success: true }); }

Almacenamiento en Base de Datos

typescript
// lib/session-store.ts import { db } from '@/lib/db'; export async function createSessionDB(userId: string) { const session = { id: crypto.randomUUID(), userId, createdAt: new Date(), expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), }; await db.sessions.create({ data: session }); return session.id; } export async function revokeSession(sessionId: string) { await db.sessions.delete({ where: { id: sessionId } }); } export async function revokeAllUserSessions(userId: string) { await db.sessions.deleteMany({ where: { userId } }); }

Ventajas y Desventajas

Ventajas:

  • Revocación inmediata posible
  • Control total sobre sesiones activas
  • Más seguro si se implementa correctamente
  • Tamaño de cookie pequeño (solo ID)

Desventajas:

  • Requiere almacenamiento en servidor
  • Escalabilidad más compleja
  • Latencia adicional en cada request
  • Requiere gestión de expiración

Los magic links son ideales para:

  • Onboarding sin fricción
  • Usuarios que olvidan contraseñas frecuentemente
  • Aplicaciones B2B donde la seguridad de contraseña es un problema
  • Primer contacto con el producto

Implementación

typescript
// lib/magic-links.ts import { SignJWT, jwtVerify } from 'jose'; import { sendEmail } from '@/lib/email'; const MAGIC_LINK_SECRET = new TextEncoder().encode( process.env.MAGIC_LINK_SECRET || 'tu-magic-link-secret' ); export async function createMagicLink(email: string) { const token = await new SignJWT({ email }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') // 15 minutos .sign(MAGIC_LINK_SECRET); const magicLink = `${process.env.NEXT_PUBLIC_APP_URL}/auth/magic?token=${token}`; await sendEmail({ to: email, subject: 'Tu enlace mágico de acceso', html: ` <p>Haz clic en el siguiente enlace para acceder:</p> <a href="${magicLink}">Acceder</a> <p>Este enlace expira en 15 minutos.</p> `, }); return magicLink; } export async function verifyMagicLink(token: string) { try { const { payload } = await jwtVerify(token, MAGIC_LINK_SECRET); return payload as { email: string }; } catch { return null; } }
typescript
// app/api/auth/magic-link/route.ts import { createMagicLink } from '@/lib/magic-links'; import { NextResponse } from 'next/server'; export async function POST(req: Request) { const { email } = await req.json(); if (!email) { return NextResponse.json({ error: 'Email requerido' }, { status: 400 }); } await createMagicLink(email); // Siempre retornar success para no revelar si el email existe return NextResponse.json({ success: true, message: 'Si el email existe, recibirás un enlace de acceso.' }); }

Página de Verificación

typescript
// app/auth/magic/page.tsx import { verifyMagicLink } from '@/lib/magic-links'; import { createSession } from '@/lib/session'; import { redirect } from 'next/navigation'; export default async function MagicPage({ searchParams, }: { searchParams: { token?: string }; }) { const token = searchParams.token; if (!token) { return <div>Enlace inválido</div>; } const payload = await verifyMagicLink(token); if (!payload) { return <div>Enlace expirado o inválido</div>; } // Crear o obtener usuario const user = await getOrCreateUser(payload.email); // Crear sesión await createSession(user.id); redirect('/dashboard'); }

Ventajas y Desventajas

Ventajas:

  • Sin contraseñas que gestionar
  • Experiencia de usuario fluida
  • Menor superficie de ataque
  • Ideal para onboarding

Desventajas:

  • Depende del email (puede ser lento)
  • Requiere infraestructura de email
  • No funciona offline
  • Puede ser confuso para usuarios no técnicos

Comparativa Rápida

AspectoJWTSessionsMagic Links
Escalabilidad⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Seguridad⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
UX⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Revocación⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Complejidad⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Recomendaciones de Binary Core

En nuestra experiencia:

  1. Para APIs públicas: Usa JWT con refresh tokens
  2. Para aplicaciones web tradicionales: Sessions con almacenamiento en DB
  3. Para onboarding B2B: Magic links como primera opción
  4. Para máxima seguridad: Sessions + 2FA obligatorio
  5. Para máxima escalabilidad: JWT con revocación vía blacklist

Conclusión

No existe una solución única para autenticación. La elección depende de tu caso de uso, requisitos de seguridad y arquitectura. En Binary Core, a menudo combinamos múltiples estrategias: magic links para onboarding, sessions para la aplicación web principal, y JWT para APIs móviles.

Lo más importante es implementar correctamente la opción que elijas: HTTPS obligatorio, secrets seguros, validación exhaustiva y monitoreo de intentos de acceso sospechosos.

Binary Core

Equipo Binary Core

← Volver al blog