Auth.js v5 en Next.js 15: guía completa con OAuth y magic links
Auth.js v5 lleva más de dos años en beta pero ya es el estándar de facto para autenticación en Next.js, con más de 3,3 millones de descargas semanales. El problema es que la migración de v4 a v5 tiene cambios breaking que no están bien documentados en español, y la mayoría de tutoriales siguen mostrando la API antigua.
Yo tuve que aprender esto por las malas construyendo el boilerplate de Arkeonix Labs: un SaaS boilerplate con Next.js 15, Stripe y autenticación completa. En este post te explico exactamente qué cambió, cómo configurarlo desde cero, y los errores que más tiempo me costaron.
Lo que cambió de v4 a v5 (lo que importa)
El cambio más grande es conceptual: v5 unifica todo en una sola función auth(). En v4 había al menos cinco formas distintas de obtener la sesión según dónde estuvieras. Eso acabó.
| Dónde necesitas la sesión | v4 | v5 |
|---|---|---|
| Server Component | getServerSession(authOptions) |
auth() |
| Client Component | useSession() |
useSession() (sin cambios) |
| Middleware | withAuth(middleware) |
export { auth as middleware } |
| Route Handler | no soportado directamente | auth() |
| Server Action | no existía | auth() |
Otros cambios breaking que afectan directamente:
- Variables de entorno:
NEXTAUTH_SECRET→AUTH_SECRET,NEXTAUTH_URL→AUTH_URL - Prefijo de cookies:
next-auth.session-token→authjs.session-token. Esto invalida todas las sesiones existentes al migrar - Paquetes de adapters:
@next-auth/prisma-adapter→@auth/prisma-adapter - Nombre del tipo de config:
NextAuthOptions→NextAuthConfig - Auto-inferencia de credenciales: si nombras tus vars
AUTH_GITHUB_IDyAUTH_GITHUB_SECRET, el provider GitHub las detecta solo, sin configuración explícita
Instalación y configuración inicial
El paquete sigue siendo next-auth@beta — la etiqueta latest aún apunta a v4:
pnpm add next-auth@beta
npx auth secret # Genera AUTH_SECRET automáticamente en .env.local
Los tres archivos que necesitas
auth.ts en la raíz del proyecto — toda la configuración vive aquí:
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, Google],
pages: {
signIn: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user
const isProtected = nextUrl.pathname.startsWith("/dashboard")
if (isProtected && !isLoggedIn) return false
return true
},
async jwt({ token, user }) {
if (user) token.id = user.id
return token
},
async session({ session, token }) {
session.user.id = token.id as string
return session
},
},
})
app/api/auth/[...nextauth]/route.ts — el route handler es ahora mínimo:
import { handlers } from "@/auth"
export const { GET, POST } = handlers
middleware.ts — protección de rutas, también mínimo:
export { auth as middleware } from "@/auth"
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
Variables de entorno
# .env.local
AUTH_SECRET=generado-con-npx-auth-secret
# Los providers se auto-detectan por convención de nombre
AUTH_GITHUB_ID=tu-github-client-id
AUTH_GITHUB_SECRET=tu-github-client-secret
AUTH_GOOGLE_ID=tu-google-client-id
AUTH_GOOGLE_SECRET=tu-google-client-secret
OAuth con GitHub y Google
Registro de las apps
GitHub → github.com/settings/developers → OAuth Apps → New OAuth App:
- Authorization callback URL:
http://localhost:3000/api/auth/callback/github - Importante: GitHub no permite múltiples callbacks en la misma app, así que necesitas una app separada para dev y otra para producción
Google → Google Cloud Console → APIs & Services → Credentials → Create OAuth client ID:
- Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google - Google sí permite múltiples redirect URIs en la misma credencial
El formato de callback URL es siempre: [origen]/api/auth/callback/[nombre-provider]
Acceder a la sesión
La forma recomendada en Next.js 15 App Router es auth() en server components — no necesitas useSession:
// 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("/login")
return <p>Bienvenido, {session.user?.name}</p>
}
Para client components que sí necesiten la sesión (un header con avatar, por ejemplo):
// app/providers.tsx
"use client"
import { SessionProvider } from "next-auth/react"
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>
}
// components/UserMenu.tsx
"use client"
import { useSession } from "next-auth/react"
export default function UserMenu() {
const { data: session, status } = useSession()
if (status === "loading") return null
if (!session) return <a href="/login">Iniciar sesión</a>
return <p>{session.user?.email}</p>
}
Augmentación de tipos TypeScript
Si añades campos personalizados al token o la sesión (como id o role), necesitas esto:
// types/next-auth.d.ts
import type { DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
id: string
role: "admin" | "user"
} & DefaultSession["user"]
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string
role?: string
}
}
Magic links con Resend
Auth.js v5 tiene un provider nativo para Resend que reemplaza el genérico EmailProvider de v4. Necesitas un adapter de base de datos — sin él, los tokens no se pueden almacenar y el flujo no funciona.
pnpm add @auth/prisma-adapter
// auth.ts
import NextAuth from "next-auth"
import Resend from "next-auth/providers/resend"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Resend({
from: "auth@tudominio.com",
}),
],
})
La variable AUTH_RESEND_KEY se auto-detecta. El flujo es simple: el usuario envía su email, Auth.js genera un token, lo guarda en la tabla VerificationToken, y envía el magic link. Al hacer clic, verifica el token (uso único, expira en 24h), crea la sesión y redirige.
Para personalizar el email:
Resend({
from: "auth@tudominio.com",
sendVerificationRequest: async ({ identifier: to, provider, url }) => {
const { host } = new URL(url)
await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${provider.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: provider.from,
to,
subject: `Inicia sesión en ${host}`,
html: `<p>Haz clic aquí para iniciar sesión: <a href="${url}">Iniciar sesión</a></p>`,
}),
})
},
}),
El schema de Prisma necesita la tabla VerificationToken:
model VerificationToken {
identifier String
token String
expires DateTime
@@unique([identifier, token])
}
Middleware de Edge para proteger rutas
El withAuth de v4 ya no existe. En v5 tienes dos opciones:
Opción 1 — export directo (más simple, usa el callback authorized):
export { auth as middleware } from "@/auth"
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
Opción 2 — wrapper con lógica personalizada:
import { NextResponse } from "next/server"
import { auth } from "@/auth"
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
const isPublicRoute = ["/", "/login", "/signup"].includes(nextUrl.pathname)
if (isLoggedIn && nextUrl.pathname === "/login") {
return NextResponse.redirect(new URL("/dashboard", nextUrl.origin))
}
if (!isLoggedIn && !isPublicRoute) {
const loginUrl = new URL("/login", nextUrl.origin)
loginUrl.searchParams.set("callbackUrl", nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
})
Importante: si usas el Método 2, el callback authorized de auth.ts no se ejecuta — el wrapper lo bypasea. Elige uno de los dos.
Patrón split-config cuando usas Prisma + edge
Prisma no puede ejecutarse en edge runtime. La solución es separar la config:
// auth.config.ts — edge-safe, sin adapter
import GitHub from "next-auth/providers/github"
import type { NextAuthConfig } from "next-auth"
export default { providers: [GitHub] } satisfies NextAuthConfig
// auth.ts — config completa con adapter
import NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import authConfig from "./auth.config"
export const { auth, handlers, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
...authConfig,
})
// middleware.ts — solo la config edge-safe
import NextAuth from "next-auth"
import authConfig from "./auth.config"
export default NextAuth(authConfig).auth
Los 5 errores que más veces veo
1. MissingSecret en producción
En desarrollo Auth.js genera un secreto automático. En producción lanza este error si AUTH_SECRET no está definido. Solución: npx auth secret y añadir el resultado al dashboard de Vercel.
2. AUTH_URL en Vercel
En Vercel no la configures — se auto-detecta desde VERCEL_URL. En self-hosted sí debes configurarla.
3. Magic links sin adapter El provider Resend/Email necesita una base de datos para guardar los tokens de verificación. Sin adapter, no funciona.
4. OAuthAccountNotLinked
Cuando un email ya existe vinculado a otro provider. Auth.js no vincula automáticamente por seguridad. Solución: Google({ allowDangerousEmailAccountLinking: true }).
5. Incompatibilidades con params en Next.js 15
Next.js 15 hizo params y searchParams asíncronos:
// ❌ Next.js 14
export default function Page({ params }: { params: { id: string } }) {
return <p>{params.id}</p>
}
// ✅ Next.js 15
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
return <p>{id}</p>
}
Puedes migrar automáticamente con: npx @next/codemod@latest next-async-request-api .
Montar esto desde cero tiene su complejidad, especialmente el patrón split-config con Prisma y el sistema de magic links. Si quieres saltarte la parte de configuración y tener todo esto funcionando en producción desde el primer día, el boilerplate de Arkeonix Labs incluye autenticación completa con OAuth, magic links con Resend, middleware de Edge, y augmentación de tipos TypeScript — junto con el resto del stack: Stripe, multi-tenancy, RBAC, y CI/CD con GitHub Actions.
