//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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*"],
};// 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}</>;
}// 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;
}
}// 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");
}server-only garantiza que lib/auth.ts nunca pueda empaquetarse en código cliente, previniendo la filtración de secretos.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 un return explícito después de redirect().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}</>;
}cookies().get() devuelve { name: string; value: string } | undefined. Siempre usa encadenamiento opcional.{ error: string }) no pueden llamar a redirect() en la misma ruta de código, porque redirect() lanza una excepción.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.cookies() es asincrónica en Next.js 15+. Olvidar await produce un error en tiempo de ejecución.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 especial NEXT_REDIRECT. O bien relanzarlo o llamar a redirect() fuera del try/catch.export const dynamic = "force-dynamic" o realiza la autenticación en middleware para páginas generadas estáticamente.| 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 |
cookies() es asincrónica en Next.js 15+ y devuelve una Promise.await significa que llamas a .get() en una Promise, que devuelve undefined.AUTH_SECRET y la lógica de firma JWT se filtren al bundle del navegador.redirect() lanza un error especial NEXT_REDIRECT internamente.NEXT_REDIRECT o llama a redirect() fuera del try/catch./login.pathname original se añade como ?callbackUrl=/dashboard (o sea cual sea la ruta solicitada).httpOnly evita que JavaScript lea la cookie, mitigando ataques XSS.secure asegura que la cookie se envíe solo sobre HTTPS en producción.sameSite: "lax" proporciona protección CSRF básica.config.matcher debe ser analizable estáticamente en tiempo de compilación.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;
}
}JWTPayload de jose proporciona el tipo base con iss, sub, exp, etc.null en caso de fallo para que los llamadores puedan hacer una simple verificación de veracidad.export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
// ...
}React.ReactNode para el prop children, que cubre elementos, strings, números, fragmentos, y null.export const dynamic = "force-dynamic" o protege la ruta a través de middleware en su lugar.auth as middleware directamente, que maneja las verificaciones de sesión automáticamente.AUTH_SECRET de forma seguraRevisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥