Next.js + Auth.js v5
Agrega autenticación a una app de Next.js 15 con Auth.js v5 (antes conocido como NextAuth), incluida OAuth de GitHub y protección de rutas basada en sesiones.
Receta
- Instala Auth.js v5 beta:
npm install next-auth@beta - Genera un secreto y agrégalo a
.env.local:npx auth secret - Crea
auth.tsen la raíz del proyecto exportando handlers configurados,auth,signIn, ysignOut. - Crea el route handler en
app/api/auth/[...nextauth]/route.tsque re-exporta los handlers. - Agrega credenciales OAuth (por ejemplo,
AUTH_GITHUB_ID,AUTH_GITHUB_SECRET) a.env.local. - Crea
middleware.tsque usa la exportaciónauthpara proteger rutas.
Ejemplo Funcional
.env.local
AUTH_SECRET=<32+ char string from `npx auth secret`>
AUTH_GITHUB_ID=<github oauth app client id>
AUTH_GITHUB_SECRET=<github oauth app client secret>
auth.ts (raíz del proyecto)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
export const \{ handlers, auth, signIn, signOut \} = NextAuth(\{
providers: [GitHub],
session: \{ strategy: "jwt" \},
callbacks: \{
authorized: async (\{ auth \}) => !!auth,
\},
\});app/api/auth/[...nextauth]/route.ts
export \{ GET, POST \} from "@/auth";Espera — en v5 exportas desde el objeto handlers en su lugar:
import \{ handlers \} from "@/auth";
export const \{ GET, POST \} = handlers;middleware.ts
export \{ auth as middleware \} from "@/auth";
export const config = \{
matcher: ["/dashboard/:path*"],
\};app/dashboard/page.tsx
import \{ auth, signOut \} from "@/auth";
export default async function DashboardPage() \{
const session = await auth();
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="mt-4">Welcome, \{session?.user?.name\}</p>
<form
action=\{async () => \{
"use server";
await signOut();
\}\}
>
<button type="submit" className="mt-4 rounded bg-black px-4 py-2 text-white">
Sign out
</button>
</form>
</main>
);
\}app/page.tsx (botón de inicio de sesión)
import \{ auth, signIn \} from "@/auth";
export default async function Home() \{
const session = await auth();
if (session?.user) \{
return <a href="/dashboard">Go to dashboard</a>;
\}
return (
<form
action=\{async () => \{
"use server";
await signIn("github", \{ redirectTo: "/dashboard" \});
\}\}
>
<button type="submit">Sign in with GitHub</button>
</form>
);
\}Inmersión Profunda
Cómo Funciona
Auth.js v5 centraliza la configuración en un archivo auth.ts raíz, que exporta todas las funciones que necesitas en otros lugares. La función auth() es isomórfica — lee la sesión desde cookies en Server Components, Server Actions, Route Handlers, y middleware. La importación middleware export \{ auth as middleware \} convierte Auth.js en un guardia consciente de matcher que redirige a usuarios no autenticados a la página de inicio de sesión.
Bajo el capó, Auth.js establece una cookie JWT firmada y encriptada por defecto (session.strategy: "jwt"). Puedes cambiar a sesiones de base de datos agregando un adaptador Prisma/Drizzle.
Variaciones
- Múltiples proveedores: agrega
Google,Credentials,Email, etc. al arrayproviders. - Sesiones de base de datos: instala
@auth/prisma-adapter, pasaadapter: PrismaAdapter(prisma), y establecesession: \{ strategy: "database" \}. - Página de inicio de sesión personalizada: establece
pages: \{ signIn: "/login" \}en la configuración de NextAuth y construye tu propia página/loginque llama asignIn(). - Acceso basado en roles: muta la sesión en los callbacks
jwtysessionpara incluir un claimrole, luego verifica en middleware o Server Components. auth()en RSC vsuseSession()en cliente: el código de servidor usaawait auth(); el código de cliente envuelve la app en<SessionProvider>y llama auseSession().- Middleware compatible con Edge: mantén
auth.config.tslibre de adaptadores solo para Node e importa solo la configuración ligera en middleware.
Notas de TypeScript
Extiende la interfaz Session usando module augmentation para que tus claims personalizados estén tipados en todos lados:
// types/next-auth.d.ts
import \{ DefaultSession \} from "next-auth";
declare module "next-auth" \{
interface Session \{
user: \{
id: string;
role: "admin" | "user";
\} & DefaultSession["user"];
\}
\}Después de esto, session.user.role está fuertemente tipado en todos lados donde llames a auth().
Trampa
AUTH_SECRETdebe ser lo suficientemente largo. Auth.js v5 requiere 32+ caracteres. Genera connpx auth secret— no lo escribas a mano.- El middleware se ejecuta en el runtime Edge. No puedes usar APIs de Node.js (
fs,crypto.randomBytesen forma heredada, controladores de base de datos). Mantén la lógica pesada fuera demiddleware.ts. - El matcher excluye activos estáticos. El matcher predeterminado puede coincidir con
/_next/statice imágenes si no tienes cuidado. Usa una negative lookahead como `/((?!api|_next/static|_next/image|favicon.ico).*)``. - La estrategia de sesión limita el almacenamiento. Las sesiones JWT viven en una cookie con un límite de ~4KB. No viertas todo el objeto de usuario en el token. Usa sesiones de base de datos o almacena un ID y obtén bajo demanda.
- Desajuste de URL de callback de OAuth. Las apps OAuth de GitHub/Google deben listar la URL de callback exacta, por ejemplo,
https://yourapp.com/api/auth/callback/github. Una barra final o protocolo incorrecto rompe el inicio de sesión con un error vago. - URLs de callback de dev vs prod. Típicamente necesitas dos apps OAuth (o dos URLs de callback) — una para
http://localhost:3000y otra para producción. auth.tsen la raíz del proyecto, no enapp/. Ponerlo dentro deapp/hace que Next.js lo trate como una ruta. Mantenlo en la raíz del proyecto o bajolib/y alias en consecuencia.
Alternativas
| Herramienta | Estilo | Mejor para |
|---|---|---|
| Auth.js (NextAuth v5) | OSS, integrado con framework | La mayoría de apps de Next.js, flujos OAuth |
| Clerk | SaaS pago | UI pulida, configuración rápida, orgs/teams |
| Lucia Auth | DIY ligero | Control total, aprendizaje, flujos personalizados |
| Supabase Auth | Incluido con Supabase DB | Apps que ya usan Supabase |
| Kinde | SaaS pago | Feature flags + auth combinado |
| Custom JWT | Hecho a tu medida | Requisitos especializados |
Preguntas Frecuentes
¿Dónde debería vivir auth.ts?
En la raíz del proyecto (al mismo nivel que next.config.js) o bajo lib/auth.ts. No lo pongas dentro del directorio app/ — Next.js interpretará archivos allí como rutas.
¿Cómo genero AUTH_SECRET?
Ejecuta npx auth secret. Escribe automáticamente un valor de 32+ caracteres criptográficamente aleatorio en .env.local. No reutilices secretos entre entornos.
¿Sesiones JWT o de base de datos?
JWT es sin estado, rápido, y el predeterminado — perfecto para la mayoría de apps. Las sesiones de base de datos requieren un adaptador pero te dan revocación instantánea, almacenamiento más grande, y fácil listado de sesiones entre dispositivos. Elige sesiones de base de datos para apps con cierre de sesión en todas partes o funciones de expulsión de usuarios del administrador.
¿Cómo agrego una página de inicio de sesión personalizada?
Establece pages: \{ signIn: "/login" \} en la configuración de NextAuth, luego construye app/login/page.tsx que renderiza tu propia UI y llama a signIn("github") desde una Server Action.
¿Cómo leo la sesión en un Server Component?
import \{ auth \} from "@/auth";
const session = await auth();
if (!session?.user) redirect("/login");En Client Components, envuelve la app en <SessionProvider> y llama a useSession() en su lugar.
Trampa: mi middleware importa un adaptador de base de datos y se rompe en tiempo de compilación
El bundle de middleware usa el runtime Edge, que no puede incluir código solo para Node como el adaptador Prisma. Divide tu configuración: pon la lista de proveedores ligera en auth.config.ts e importa solo eso en middleware.ts. Mantén el adaptador en el auth.ts completo que usan los Server Components.
Trampa: OAuth funciona localmente pero falla en producción
Casi siempre es un desajuste de URL de callback. Tu app OAuth de GitHub (o Google) debe listar la URL de producción exacta: https://yourapp.com/api/auth/callback/github. Cuidado con las barras finales, http vs https, y el subdominio www.
¿Cómo agrego control de acceso basado en roles?
Usa el callback jwt para grabar el rol en el token, luego reflújalo en la sesión:
callbacks: \{
async jwt(\{ token, user \}) \{
if (user) token.role = user.role;
return token;
\},
async session(\{ session, token \}) \{
session.user.role = token.role as "admin" | "user";
return session;
\},
\}TypeScript: ¿Cómo agrego campos personalizados al tipo Session?
Usa module augmentation en types/next-auth.d.ts:
declare module "next-auth" \{
interface Session \{
user: \{ id: string; role: string \} & DefaultSession["user"];
\}
\}Asegúrate de que types/next-auth.d.ts esté incluido en tu tsconfig.json.
TypeScript: session.user es posiblemente indefinido — ¿qué pasa?
auth() devuelve Session | null, e incluso cuando no es nulo, user es opcional. Siempre estrecha:
const session = await auth();
if (!session?.user) redirect("/login");Después de ese check, session.user se estrecha al tipo definido.
¿Cómo me desconecto?
Llama a signOut() desde Auth.js. En un Server Component lo envuelves en una Server Action:
<form action=\{async () => \{ "use server"; await signOut(); \}\}>
<button type="submit">Cerrar sesión</button>
</form>En un Client Component, importa signOut desde next-auth/react y llama directamente.
¿Debería usar Auth.js o Clerk?
Clerk es más rápido de configurar e incluye UI pulida, orgs, y MFA de caja — pero es de pago. Auth.js es gratis y totalmente personalizable pero requiere más cableado. Elige Clerk si la velocidad de entrada al mercado y las características importan más que el costo; elige Auth.js si quieres control total o cero vendor lock-in.