NextAuth.js / Auth.js v5 - Autenticación para Next.js App Router con proveedores, sesiones y middleware
Receta
npm install next-auth@beta// auth.ts (raíz del proyecto)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub({
clientId: process.env.AUTH_GITHUB_ID!,
clientSecret: process.env.AUTH_GITHUB_SECRET!,
}),
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
],
});// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;// .env.local
AUTH_SECRET=your-random-secret-at-least-32-chars
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...
AUTH_GOOGLE_ID=...
AUTH_GOOGLE_SECRET=...Cuándo usarlo: Necesitas inicio de sesión OAuth (GitHub, Google, etc.), gestión de sesiones y protección de rutas en una aplicación Next.js App Router.
Ejemplo Funcional
// app/components/AuthButtons.tsx
import { auth, signIn, signOut } from "@/auth";
export async function SignInButton() {
const session = await auth();
if (session?.user) {
return (
<div className="flex items-center gap-3">
{session.user.image && (
<img
src={session.user.image}
alt=""
className="w-8 h-8 rounded-full"
/>
)}
<span className="text-sm">{session.user.name}</span>
<form
action={async () => {
"use server";
await signOut();
}}
>
<button className="text-sm text-gray-500 hover:text-gray-700">
Cerrar Sesión
</button>
</form>
</div>
);
}
return (
<div className="flex gap-2">
<form
action={async () => {
"use server";
await signIn("github");
}}
>
<button className="bg-gray-900 text-white px-4 py-2 rounded text-sm">
Iniciar sesión con GitHub
</button>
</form>
<form
action={async () => {
"use server";
await signIn("google");
}}
>
<button className="bg-blue-600 text-white px-4 py-2 rounded text-sm">
Iniciar sesión con Google
</button>
</form>
</div>
);
}// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect("/api/auth/signin");
}
return (
<div className="max-w-2xl mx-auto p-6">
<h1 className="text-2xl font-bold">Panel de Control</h1>
<p className="mt-2">¡Bienvenido, {session.user?.name}!</p>
<pre className="mt-4 bg-gray-100 p-4 rounded text-sm">
{JSON.stringify(session, null, 2)}
</pre>
</div>
);
}// middleware.ts
import { auth } from "./auth";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
}
});
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};Lo que esto demuestra:
- Configuración de Auth.js v5 con proveedores GitHub y Google
- Acceso a sesión de Componentes de Servidor con
auth() - Formularios de Acciones de Servidor para iniciar/cerrar sesión
- Páginas protegidas con redirección
- Protección de rutas basada en middleware
Análisis Profundo
Cómo Funciona
- Auth.js v5 exporta
auth(),signIn(),signOut()yhandlersdesde una única configuración auth()devuelve la sesión actual en Componentes de Servidor, Acciones de Servidor, rutas de API y middleware- Las sesiones están basadas en JWT de forma predeterminada (no se requiere base de datos); agrega un adaptador de base de datos para sesiones persistentes
- La exportación
handlersproporciona manejadores de rutas GET y POST para la ruta catch-all/api/auth/[...nextauth] - Flujo OAuth: el usuario hace clic en iniciar sesión, se redirige al proveedor, el proveedor redirige de vuelta con un código, Auth.js intercambia el código por tokens y crea una sesión
AUTH_SECRETse requiere en producción para firmar JWTs; genéralo connpx auth secret
Variaciones
Proveedor Credentials (correo electrónico/contraseña):
import Credentials from "next-auth/providers/credentials";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
const parsed = z
.object({
email: z.string().email(),
password: z.string().min(8),
})
.safeParse(credentials);
if (!parsed.success) return null;
const user = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (!user || !user.password) return null;
const valid = await bcrypt.compare(parsed.data.password, user.password);
if (!valid) return null;
return { id: String(user.id), name: user.name, email: user.email };
},
}),
],
});Adaptador de base de datos Prisma:
npm install @auth/prisma-adapterimport { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [GitHub, Google],
session: { strategy: "database" }, // Usa sesiones de base de datos en lugar de JWT
});Extendiendo la sesión con campos personalizados:
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [GitHub],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role; // Agrega rol del usuario en la base de datos
}
return token;
},
async session({ session, token }) {
session.user.id = token.sub!;
session.user.role = token.role as string;
return session;
},
},
});// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
}Acceso a sesión del lado del cliente:
// app/providers.tsx
"use client";
import { SessionProvider } from "next-auth/react";
export function AuthProvider({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
// app/components/ClientProfile.tsx
"use client";
import { useSession } from "next-auth/react";
export function ClientProfile() {
const { data: session, status } = useSession();
if (status === "loading") return <p>Cargando...</p>;
if (!session) return <p>No iniciaste sesión</p>;
return <p>Iniciaste sesión como {session.user?.name}</p>;
}Notas de TypeScript
- Extiende los tipos
SessionyJWTmediante aumentación de módulo entypes/next-auth.d.ts auth()devuelveSession | null; siempre verifica null antes de acceder asession.user- Los tipos de perfil específicos del proveedor están disponibles pero rara vez son necesarios
- El tipo
Userse puede extender para campos de base de datos personalizados
import type { Session } from "next-auth";
async function getUser(): Promise<Session["user"] | null> {
const session = await auth();
return session?.user ?? null;
}Trampas Comunes
-
AUTH_SECRET faltante — La aplicación falla en producción sin
AUTH_SECRET. Solución: Genéralo connpx auth secrety agrégalo a tus variables de entorno. Requerido para firmar JWTs. -
Desajuste de URL de callback — Los proveedores OAuth rechazan redirecciones si la URL de callback no coincide. Solución: Registra
https://yourdomain.com/api/auth/callback/github(y/google) en la configuración del desarrollador de cada proveedor. -
Sesión nula en componentes cliente —
auth()solo funciona del lado del servidor. Solución: UsaSessionProvidery el hookuseSession()para componentes cliente. Envuelve tu layout con<SessionProvider>. -
El middleware se ejecuta en cada solicitud — Sin un
matcher, el middleware de autenticación se ejecuta en activos estáticos, rutas de API, etc. Solución: Siempre configuraconfig.matcherpara limitar el middleware a rutas protegidas. -
Limitaciones del proveedor Credentials — El proveedor Credentials no soporta sesiones de base de datos directamente (solo JWT). Solución: Usa la estrategia JWT con credenciales. Para sesiones de base de datos, usa proveedores OAuth con un adaptador de base de datos.
-
Compatibilidad de Edge runtime — Algunos adaptadores de base de datos y bcrypt no funcionan en Edge runtime. Solución: Usa
bcryptjsen lugar debcrypt. Verifica la compatibilidad del adaptador con Edge. Usaruntime: "nodejs"en el middleware si es necesario. -
Migración de v4 a v5 — Auth.js v5 tiene una API significativamente diferente a NextAuth v4. Solución: Sigue la guía de migración oficial. Cambios clave:
NextAuth()devuelveauthen lugar de usargetServerSession(), ynext-auth/reactsolo es para componentes cliente.
Alternativas
| Librería | Mejor Para | Compensación |
|---|---|---|
| Auth.js / NextAuth v5 | OAuth completo + gestión de sesiones para Next.js | Configuración compleja para flujos personalizados |
| Clerk | Componentes de IU de autenticación listos para usar | Servicio pago, bloqueo de proveedor |
| Supabase Auth | Integración del ecosistema Supabase | Vinculado a Supabase |
| Lucia | Autenticación ligera, DIY | Más configuración manual, menos abstracción |
| Kinde | Autenticación B2B con organizaciones | Servicio pago |
| Custom JWT | Control total | Debes manejar seguridad, actualización y revocación tú mismo |
Preguntas Frecuentes
¿Qué devuelve la función auth() y dónde puedo llamarla?
- Devuelve
Session | nullque contiene la información del usuario y la expiración - Se puede llamar en Componentes de Servidor, Acciones de Servidor, rutas de API y middleware
- No se puede llamar en componentes cliente -- usa
useSession()denext-auth/reacten su lugar - Siempre verifica
nullantes de acceder asession.user
¿Cómo funciona el flujo OAuth con Auth.js v5 y Next.js?
- El usuario hace clic en iniciar sesión, la acción del formulario llama a
signIn("github") - Next.js redirige a la página de autorización del proveedor OAuth
- El usuario otorga permiso, el proveedor redirige de vuelta con un código
- Auth.js intercambia el código por tokens y crea una sesión JWT
- Se establece la cookie de sesión y las llamadas posteriores a
auth()devuelven la sesión
Trampa Común: ¿Por qué mi aplicación falla en producción con "AUTH_SECRET is missing"?
AUTH_SECRETse requiere para firmar JWTs en producción- Genéralo con
npx auth secrety agrégalo a tus variables de entorno - Debe tener al menos 32 caracteres
- En desarrollo, Auth.js genera un secreto temporal automáticamente
¿Cómo protejo rutas con middleware?
// middleware.ts
import { auth } from "./auth";
export default auth((req) => {
if (!req.auth && req.nextUrl.pathname.startsWith("/dashboard")) {
return Response.redirect(new URL("/api/auth/signin", req.nextUrl));
}
});
export const config = {
matcher: ["/dashboard/:path*"],
};Siempre establece config.matcher para limitar el middleware a rutas protegidas.
¿Cómo accedo a la sesión en un componente cliente?
- Envuelve tu layout con
<SessionProvider>denext-auth/react - Usa el hook
useSession()en componentes cliente useSession()devuelve{ data: session, status }donde status es"loading","authenticated"o"unauthenticated"
¿Cómo agrego un proveedor Credentials para inicio de sesión por correo electrónico/contraseña?
- Importa
Credentialsdenext-auth/providers/credentials - Define campos
credentialsy una funciónauthorize - La función
authorizevalida las credenciales y devuelve un objeto de usuario onull - El proveedor Credentials solo soporta sesiones JWT, no sesiones de base de datos
¿Cómo extiendo la sesión con campos personalizados como role?
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role;
return token;
},
async session({ session, token }) {
session.user.role = token.role as string;
return session;
},
}También extiende el tipo Session mediante aumentación de módulo en types/next-auth.d.ts.
¿Cómo extiendo el tipo de la interfaz Session en TypeScript?
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
}Esto agrega id y role a session.user en toda tu aplicación.
Trampa Común: ¿Por qué auth() devuelve null en mi componente cliente?
auth()es una función solo para servidor y no se puede llamar en componentes cliente- Usa
<SessionProvider>y el hookuseSession()para acceso a sesión del lado del cliente - Asegúrate de que
SessionProviderenvuelva el árbol de componentes en tu layout
¿Cómo configuro un adaptador de base de datos Prisma para sesiones persistentes?
import { PrismaAdapter } from "@auth/prisma-adapter";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
providers: [GitHub, Google],
});Instala @auth/prisma-adapter y establece session.strategy en "database".
Trampa Común: ¿Por qué mi middleware se ejecuta en cada solicitud incluyendo activos estáticos?
- Sin una configuración
matcher, el middleware se ejecuta en todas las rutas incluyendo_next/static, imágenes y rutas de API - Siempre configura
config.matcherpara limitarlo a rutas protegidas - Ejemplo:
matcher: ["/dashboard/:path*", "/settings/:path*"]
¿Cuáles son las diferencias clave entre NextAuth v4 y Auth.js v5?
- v5 exporta
auth()en lugar de requerirgetServerSession(authOptions) - v5 usa una sola llamada
NextAuth()que devuelveauth,signIn,signOutyhandlers next-auth/react(useSession,SessionProvider) solo es para componentes cliente- El archivo de configuración generalmente es
auth.tsen la raíz del proyecto
Relacionado
- Prisma — Adaptador de base de datos para sesiones persistentes y almacenamiento de usuarios
- Next.js Middleware — Patrones de protección de rutas
- Next.js Server Actions — Flujos de inicio/cierre de sesión del lado del servidor
- TanStack Query — Obtención de datos específicos del usuario después de la autenticación