Autenticación Moderna: JWT vs Session vs Magic Links
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
Magic Links
Cuándo Usar Magic Links
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; } }
API Route para Solicitar Magic Link
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
| Aspecto | JWT | Sessions | Magic Links |
|---|---|---|---|
| Escalabilidad | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Seguridad | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| UX | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Revocación | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Complejidad | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Recomendaciones de Binary Core
En nuestra experiencia:
- Para APIs públicas: Usa JWT con refresh tokens
- Para aplicaciones web tradicionales: Sessions con almacenamiento en DB
- Para onboarding B2B: Magic links como primera opción
- Para máxima seguridad: Sessions + 2FA obligatorio
- 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