Patrones de Autenticación
Receta
Protege páginas y rutas API en una aplicación Next.js 15+ App Router usando verificaciones de sesión basadas en middleware, validación de autenticación del lado del servidor, y protección por página.
Ejemplo Funcional
Verificación de Sesión en Middleware
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const protectedPaths = ["/dashboard", "/settings", "/account"];
export function middleware(request: NextRequest) {
const sessionToken = request.cookies.get("session-token")?.value;
const { pathname } = request.nextUrl;
const isProtected = protectedPaths.some((path) =>
pathname.startsWith(path)
);
if (isProtected && !sessionToken) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*", "/account/:path*"],
};Autenticación del Lado del Servidor en un Layout
// app/dashboard/layout.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { verifySession } from "@/lib/auth";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const token = cookieStore.get("session-token")?.value;
if (!token) {
redirect("/login");
}
const session = await verifySession(token);
if (!session) {
redirect("/login");
}
return <>{children}</>;
}Utilidad de Autenticación (Solo Servidor)
// lib/auth.ts
import "server-only";
import { SignJWT, jwtVerify } from "jose";
const secret = new TextEncoder().encode(process.env.AUTH_SECRET);
export async function createSession(userId: string) {
return new SignJWT({ userId })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("7d")
.sign(secret);
}
export async function verifySession(token: string) {
try {
const { payload } = await jwtVerify(token, secret);
return payload;
} catch {
return null;
}
}Acción de Servidor de Inicio de Sesión
// app/login/actions.ts
"use server";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { createSession } from "@/lib/auth";
export async function login(formData: FormData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
// Valida credenciales contra tu base de datos
const user = await authenticateUser(email, password);
if (!user) {
return { error: "Credenciales inválidas" };
}
const token = await createSession(user.id);
const cookieStore = await cookies();
cookieStore.set("session-token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 60 * 60 * 24 * 7, // 7 días
path: "/",
});
redirect("/dashboard");
}Análisis Profundo
Cómo Funciona
- El middleware se ejecuta en el edge antes de que cualquier página o ruta API se renderice. Puede leer cookies y headers pero no puede acceder a una base de datos directamente. Úsalo para verificaciones rápidas y de grano grueso (¿está presente un token?).
- Los Server Components y layouts se ejecutan en el servidor Node.js. Pueden realizar una verificación de sesión completa contra una base de datos o un secreto JWT.
- La importación
server-onlygarantiza quelib/auth.tsnunca pueda empaquetarse en código cliente, previniendo la filtración de secretos. - Las cookies son el transporte de sesión principal en App Router. La función
cookies()es asincrónica en Next.js 15+ y debe ser esperada. redirect()lanza internamente, por lo que el código después de ella es inalcanzable. No se necesita unreturnexplícito después deredirect().
Variaciones
Usando NextAuth.js (Auth.js v5):
// auth.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
});
// middleware.ts
export { auth as middleware } from "./auth";Acceso Basado en Roles:
// app/admin/layout.tsx
import { verifySession } from "@/lib/auth";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const cookieStore = await cookies();
const token = cookieStore.get("session-token")?.value;
const session = await verifySession(token!);
if (session?.role !== "admin") {
redirect("/unauthorized");
}
return <>{children}</>;
}Notas de TypeScript
cookies().get()devuelve{ name: string; value: string } | undefined. Siempre usa encadenamiento opcional.- Las Server Actions que devuelven datos (como
{ error: string }) no pueden llamar aredirect()en la misma ruta de código, porqueredirect()lanza una excepción. - Usa
import "server-only"al principio de cualquier módulo que contenga secretos para obtener un error en tiempo de compilación si se importa desde un Client Component.
Errores Comunes
- El middleware no puede acceder a bases de datos ni a APIs de Node.js. Se ejecuta en el Edge Runtime por defecto. Solo verifica la presencia de token; verifica los tokens en Server Components o Route Handlers.
cookies()es asincrónica en Next.js 15+. Olvidarawaitproduce un error en tiempo de ejecución.- El matcher de middleware debe ser estático. No puedes usar variables en tiempo de ejecución en el array
config.matcher. Usa lógica condicional dentro de la función middleware en su lugar. redirect()dentro de un try/catch será tragado porque lanza un error especialNEXT_REDIRECT. O bien relanzarlo o llamar aredirect()fuera del try/catch.- Las páginas estáticas no pueden verificar autenticación en tiempo de solicitud. Usa
export const dynamic = "force-dynamic"o realiza la autenticación en middleware para páginas generadas estáticamente.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Solo middleware | Rápido, se ejecuta antes del renderizado | No puede verificar tokens contra la BD |
| Autenticación basada en layout | Acceso completo al servidor, puede consultar BD | Se ejecuta después de middleware, latencia ligera |
| NextAuth.js (Auth.js) | Proveedores integrados, gestión de sesiones | Dependencia adicional, cambios de migración |
| Clerk o Supabase Auth | Servicio gestionado, menos código | Bloqueo de proveedor, costos a escala |
| Iron Session | Cookies cifradas, sin JWT | Comunidad más pequeña, configuración manual |
Preguntas Frecuentes
¿Por qué el middleware solo verifica la presencia de un token en lugar de verificarlo completamente?
- El middleware se ejecuta en el Edge Runtime, que no puede acceder a bases de datos ni a la mayoría de APIs de Node.js.
- Una verificación JWT completa con una búsqueda en la base de datos es demasiado pesada para el edge.
- El middleware realiza una verificación de grano grueso (¿está presente la cookie?), luego los Server Components hacen la verificación completa.
¿Qué sucede si olvidas hacer await a la llamada cookies() en Next.js 15+?
cookies()es asincrónica en Next.js 15+ y devuelve unaPromise.- Olvidar
awaitsignifica que llamas a.get()en una Promise, que devuelveundefined. - Esto rompe silenciosamente las verificaciones de autenticación, haciendo que las rutas protegidas aparezcan como si no existiera ningún token.
¿Por qué se usa import "server-only" al principio de lib/auth.ts?
- Garantiza un error en tiempo de compilación si algún Client Component importa este módulo.
- Esto evita que tu
AUTH_SECRETy la lógica de firma JWT se filtren al bundle del navegador.
¿Puedes llamar a redirect() dentro de un bloque try/catch?
redirect()lanza un error especialNEXT_REDIRECTinternamente.- Si se envuelve en un try/catch, el redirect se captura y se traga, por lo que el usuario nunca se redirige.
- O bien relanza los errores
NEXT_REDIRECTo llama aredirect()fuera del try/catch.
¿Cómo funciona el patrón callbackUrl en el redirect del middleware?
- Cuando un usuario no autenticado accede a una ruta protegida, el middleware redirige a
/login. - El
pathnameoriginal se añade como?callbackUrl=/dashboard(o sea cual sea la ruta solicitada). - Después del inicio de sesión exitoso, tu manejador de inicio de sesión puede leer este parámetro y redirigirse a la página original.
¿Cuál es la diferencia entre la autenticación basada en middleware y la autenticación basada en layout?
- El middleware se ejecuta antes de cualquier renderizado, en el edge, y solo puede hacer verificaciones ligeras.
- La autenticación basada en layout se ejecuta en el servidor Node.js, puede consultar una base de datos, y verifica completamente una sesión.
- La mejor práctica es combinar ambas: middleware para una puerta rápida, layout para verificación completa.
¿Por qué la Server Action de Inicio de Sesión establece httpOnly: true y secure: true en la cookie?
httpOnlyevita que JavaScript lea la cookie, mitigando ataques XSS.secureasegura que la cookie se envíe solo sobre HTTPS en producción.sameSite: "lax"proporciona protección CSRF básica.
Error Común: ¿Qué sucede si usas variables en tiempo de ejecución en el array config.matcher del middleware?
- El
config.matcherdebe ser analizable estáticamente en tiempo de compilación. - Usar variables o llamadas a funciones causa que el matcher falle silenciosamente o lance un error de compilación.
- Pon la lógica condicional dentro del cuerpo de la función middleware en su lugar.
¿Cómo escribirías el tipo de retorno de verifySession en TypeScript?
import { JWTPayload } from "jose";
export async function verifySession(
token: string
): Promise<JWTPayload | null> {
try {
const { payload } = await jwtVerify(token, secret);
return payload;
} catch {
return null;
}
}JWTPayloaddejoseproporciona el tipo base coniss,sub,exp, etc.- Devuelve
nullen caso de fallo para que los llamadores puedan hacer una simple verificación de veracidad.
¿Cómo escribes el tipo del prop children en un componente layout con TypeScript?
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// ...
}- Usa
React.ReactNodepara el propchildren, que cubre elementos, strings, números, fragmentos, ynull.
¿Por qué las páginas generadas estáticamente no pueden verificar autenticación en tiempo de solicitud?
- Las páginas estáticas se pre-renderizan en tiempo de compilación y se sirven como archivos HTML simples.
- No hay ejecución del lado del servidor en tiempo de solicitud para leer cookies o verificar sesiones.
- Usa
export const dynamic = "force-dynamic"o protege la ruta a través de middleware en su lugar.
¿Cómo simplifica NextAuth.js (Auth.js v5) la configuración del middleware?
- Exportas
auth as middlewaredirectamente, que maneja las verificaciones de sesión automáticamente. - Auth.js gestiona proveedores, almacenamiento de sesiones, y rotación de tokens fuera de la caja.
- La compensación es una dependencia adicional y mantenerse al día con los cambios de migración de Auth.js.
Relacionado
- Route Handlers - protege endpoints API con las mismas utilidades de autenticación
- Environment Variables - almacenando
AUTH_SECRETde forma segura - Error Handling - manejando fallos de autenticación elegantemente
- Next.js Middleware Docs