Saltar al contenido
Volver al Blog
Auth.js v5 en Next.js 15: guía completa con OAuth y magic links

Auth.js v5 en Next.js 15: guía completa con OAuth y magic links

12 de abril de 2026 David López

Compartir:

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_SECRETAUTH_SECRET, NEXTAUTH_URLAUTH_URL
  • Prefijo de cookies: next-auth.session-tokenauthjs.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: NextAuthOptionsNextAuthConfig
  • Auto-inferencia de credenciales: si nombras tus vars AUTH_GITHUB_ID y AUTH_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
  }
}

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.