DriversRecommendedOutdated drivers can make a good PC feel brokenScan driver issues before chasing fixes manually.Scan NowOctober DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan Now×
Skip to content
World desk11 min

Cómo implementar autenticación en Next.js 14 con Auth.js (NextAuth.js), shadcn/ui, React Hook Form y Zod

Construye una autenticación coherente en Next.js 14: Credentials, Prisma opcional, Zod en cliente y servidor, formulario accesible, sesiones JWT, middleware y autorización.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Este tutorial construye un flujo completo para Next.js 14 con App Router y TypeScript: login con email y contraseña, validación compartida con Zod, formulario accesible de shadcn/ui, sesiones JWT mediante Auth.js (el nombre actual de NextAuth.js), protección de /dashboard, lectura de sesión en servidor y cliente, registro opcional y cierre de sesión.

La implementación se dirige a Auth.js/NextAuth.js v5, cuya documentación para Next.js 14 suele instalar next-auth@beta. No mezcles sus APIs con tutoriales v4: v4 usa habitualmente pages/api/auth, NextAuthOptions y otros patrones. Auth.js separa autenticación y sesiones de la autorización que debes aplicar en cada operación protegida. Consulta la guía oficial de autenticación de Next.js y la documentación de Auth.js.

As an Amazon Associate I earn from qualifying purchases.

Qué hace cada herramienta

Herramienta Responsabilidad
Next.js 14 App Router, Server Components, Route Handlers, middleware y Server Actions.
Auth.js/NextAuth.js Proveedor Credentials, cookies, sesiones, callbacks, auth(), signIn() y signOut().
shadcn/ui El CLI copia el código de los componentes al proyecto; puedes inspeccionarlo y personalizarlo.
React Hook Form Estado, registro de campos, envío, errores y estados de validación.
Zod Esquemas, validación y tipos TypeScript compartidos.
Prisma Persistencia opcional de usuarios, cuentas y sesiones en PostgreSQL.

La validación del navegador mejora la experiencia, pero no protege nada por sí sola: el servidor debe validar de nuevo cada entrada.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Crear el proyecto e instalar dependencias

npx create-next-app@14 auth-demo
cd auth-demo
npm install next-auth@beta react-hook-form zod @hookform/resolvers bcryptjs
npm install @prisma/client
npm install -D prisma

El asistente debe usar TypeScript, ESLint, Tailwind CSS, App Router y el alias @/*. Node.js debe ser compatible con tu versión concreta de Next.js; la guía actual de Prisma utiliza Node.js 20 o superior como ejemplo, no como requisito universal de todas las instalaciones de Next.js 14.

Configurar shadcn/ui

npx shadcn@latest init
npx shadcn@latest add button card input label form

El comando puede variar con npm, pnpm o yarn. La instalación oficial de shadcn/ui para Next.js explica las preguntas del CLI. Para patrones de formularios, consulta React Hook Form con Zod y sus ejemplos de autenticación.

Variables de entorno y estructura

AUTH_SECRET=una-clave-larga-y-aleatoria
DATABASE_URL=postgresql://usuario:password@localhost:5432/auth_demo

Genera el secreto con openssl rand -base64 32 o npx auth secret. No lo publiques, no lo guardes en Git, no lo reutilices entre entornos y configúralo también en producción. Los tutoriales v4 pueden usar NEXTAUTH_SECRET; utiliza el nombre que espera la versión instalada.

app/
├── api/auth/[...nextauth]/route.ts
├── dashboard/page.tsx
├── login/page.tsx
└── actions/auth.ts
components/auth/login-form.tsx
lib/auth.ts
lib/db.ts
lib/validations/auth.ts
prisma/schema.prisma
middleware.ts

Definir esquemas con Zod

import { z } from "zod"

export const loginSchema = z.object({
  email: z.string().email("Introduce un email válido").toLowerCase().trim(),
  password: z.string().min(8, "La contraseña debe tener al menos 8 caracteres"),
})

export type LoginInput = z.infer<typeof loginSchema>

export const registerSchema = z.object({
  name: z.string().min(2, "El nombre debe tener al menos 2 caracteres").max(80, "El nombre es demasiado largo"),
  email: z.string().email("Introduce un email válido").toLowerCase().trim(),
  password: z.string().min(8, "La contraseña debe tener al menos 8 caracteres"),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Las contraseñas no coinciden",
  path: ["confirmPassword"],
})

Usa estos esquemas en el cliente y vuelve a ejecutar safeParse en authorize, Server Actions o Route Handlers. Un atacante puede ignorar completamente el formulario.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Persistir usuarios con Prisma (opcional)

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String    @unique
  passwordHash  String?
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

model Account {
  userId String
  type String
  provider String
  providerAccountId String
  refresh_token String?
  access_token String?
  expires_at Int?
  token_type String?
  scope String?
  id_token String?
  session_state String?
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
  @@id([provider, providerAccountId])
}

model Session {
  sessionToken String @unique
  userId String
  expires DateTime
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
  identifier String
  token String @unique
  expires DateTime
  @@unique([identifier, token])
}
npx prisma generate
npx prisma migrate dev --name init

La guía de Prisma para Auth.js y Next.js documenta el adaptador. Para este tutorial se elige JWT, por lo que la tabla Session no participa en cada inicio de sesión; si cambias a sesiones de base de datos, revisa la compatibilidad exacta entre Credentials, adaptador y estrategia.

Cliente Prisma reutilizable

import { PrismaClient } from "@prisma/client"

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined }

export const db = globalForPrisma.prisma ?? new PrismaClient({ log: ["error", "warn"] })
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db

El patrón evita crear múltiples clientes durante el hot reload de desarrollo.

Configurar Auth.js con Credentials

import NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
import { db } from "@/lib/db"
import { loginSchema } from "@/lib/validations/auth"

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Contraseña", type: "password" },
      },
      async authorize(credentials) {
        const parsed = loginSchema.safeParse(credentials)
        if (!parsed.success) return null
        const { email, password } = parsed.data
        const user = await db.user.findUnique({ where: { email } })
        if (!user?.passwordHash) return null
        const matches = await bcrypt.compare(password, user.passwordHash)
        if (!matches) return null
        return { id: user.id, name: user.name, email: user.email, image: user.image }
      },
    }),
  ],
  session: { strategy: "jwt" },
  pages: { signIn: "/login" },
})

authorize valida, busca el usuario, compara el hash y devuelve solo campos necesarios. Nunca devuelvas passwordHash, contraseñas, tokens privados ni datos internos. Credentials no registra usuarios automáticamente. El valor 12 de bcrypt.hash(password, 12) es un punto de partida que debes ajustar al rendimiento de tu entorno, no una cifra universal.

Exponer el Route Handler

import { handlers } from "@/lib/auth"
export const { GET, POST } = handlers

Guárdalo en app/api/auth/[...nextauth]/route.ts. La separación entre configuración central y handlers es el patrón mostrado en la documentación de Prisma/Auth.js.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Construir el formulario con shadcn/ui

"use client"

import { useState } from "react"
import { signIn } from "next-auth/react"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { loginSchema, type LoginInput } from "@/lib/validations/auth"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"

export function LoginForm() {
  const router = useRouter()
  const [serverError, setServerError] = useState<string | null>(null)
  const form = useForm<LoginInput>({ resolver: zodResolver(loginSchema), defaultValues: { email: "", password: "" } })

  async function onSubmit(values: LoginInput) {
    setServerError(null)
    const result = await signIn("credentials", { ...values, redirect: false })
    if (!result || result.error) {
      setServerError("El email o la contraseña no son correctos")
      return
    }
    router.push("/dashboard")
    router.refresh()
  }

  return (
    <Card className="w-full max-w-md">
      <CardHeader><CardTitle>Iniciar sesión</CardTitle><CardDescription>Introduce tus credenciales para continuar.</CardDescription></CardHeader>
      <CardContent>
        <Form {...form}>
          <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
            <FormField control={form.control} name="email" render={({ field }) => (
              <FormItem><FormLabel>Email</FormLabel><FormControl><Input {...field} type="email" autoComplete="email" aria-invalid={!!form.formState.errors.email} /></FormControl><FormMessage /></FormItem>
            )} />
            <FormField control={form.control} name="password" render={({ field }) => (
              <FormItem><FormLabel>Contraseña</FormLabel><FormControl><Input {...field} type="password" autoComplete="current-password" aria-invalid={!!form.formState.errors.password} /></FormControl><FormMessage /></FormItem>
            )} />
            {serverError && <p role="alert" className="text-sm text-destructive">{serverError}</p>}
            <Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>{form.formState.isSubmitting ? "Iniciando sesión..." : "Iniciar sesión"}</Button>
          </form>
        </Form>
      </CardContent>
    </Card>
  )
}

Usar redirect: false permite mostrar un error genérico sin revelar si el email existe. isSubmitting evita dobles envíos, autoComplete ayuda a los gestores de contraseñas y role="alert" anuncia el error a tecnologías de asistencia.

Página de login

import { LoginForm } from "@/components/auth/login-form"
export default function LoginPage() {
  return <main className="flex min-h-screen items-center justify-center p-6"><LoginForm /></main>
}

Proteger el dashboard

En Next.js 14 utiliza middleware.ts. La documentación reciente también usa la convención proxy; no las trates como nombres intercambiables en un mismo proyecto.

export { auth as middleware } from "@/lib/auth"
export const config = { matcher: ["/dashboard/:path*"] }

Para redirección explícita:

import { auth } from "@/lib/auth"
import { NextResponse } from "next/server"

export default auth((request) => {
  if (!request.auth) {
    const url = new URL("/login", request.nextUrl.origin)
    url.searchParams.set("callbackUrl", request.nextUrl.pathname)
    return NextResponse.redirect(url)
  }
  return NextResponse.next()
})
export const config = { matcher: ["/dashboard/:path*"] }

El matcher limita la protección a esa ruta y evita bloquear /login, /api/auth y recursos públicos.

Leer la sesión en un Server Component

import { redirect } from "next/navigation"
import { auth } from "@/lib/auth"

export default async function DashboardPage() {
  const session = await auth()
  if (!session?.user) redirect("/login")
  return <main className="p-6"><h1 className="text-2xl font-bold">Bienvenido, {session.user.name ?? session.user.email}</h1></main>
}

Comprobar en el servidor evita convertir toda la página en un Client Component. Además, cualquier consulta de datos privados debe repetir la autorización.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Leer la sesión en un Client Component

"use client"
import { SessionProvider } from "next-auth/react"
export function AuthSessionProvider({ children }: { children: React.ReactNode }) {
  return <SessionProvider>{children}</SessionProvider>
}

Envuelve el contenido del layout con AuthSessionProvider. Después:

"use client"
import { useSession } from "next-auth/react"
export function UserMenu() {
  const { data: session, status } = useSession()
  if (status === "loading") return <span>Cargando...</span>
  if (!session?.user) return null
  return <span>{session.user.email}</span>
}

useSession necesita un Client Component y SessionProvider. Resérvalo para elementos interactivos; para contenido protegido, prefiere auth(). El ejemplo oficial de cliente muestra este patrón.

Registro de usuarios

"use server"
import bcrypt from "bcryptjs"
import { db } from "@/lib/db"
import { registerSchema } from "@/lib/validations/auth"

export async function registerUser(input: unknown) {
  const parsed = registerSchema.safeParse(input)
  if (!parsed.success) return { ok: false, message: "Los datos enviados no son válidos", errors: parsed.error.flatten().fieldErrors }
  const { name, email, password } = parsed.data
  const existing = await db.user.findUnique({ where: { email } })
  if (existing) return { ok: false, message: "No se pudo crear la cuenta" }
  const passwordHash = await bcrypt.hash(password, 12)
  await db.user.create({ data: { name, email, passwordHash } })
  return { ok: true, message: "Cuenta creada correctamente" }
}

No confirmes públicamente si un correo ya está registrado. En una aplicación real añade verificación de email, recuperación de contraseña, limitación de intentos, protección contra bots y controles para cuentas deshabilitadas.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Cerrar sesión

Desde el servidor

import { signOut } from "@/lib/auth"
export function LogoutButton() {
  return <form action={async () => { "use server"; await signOut({ redirectTo: "/login" }) }}><button type="submit">Cerrar sesión</button></form>
}

Desde el cliente

"use client"
import { signOut } from "next-auth/react"
export function ClientLogoutButton() {
  return <button onClick={() => signOut({ callbackUrl: "/login" })}>Cerrar sesión</button>
}

No mezcles sin motivo el signOut de next-auth/react con el exportado por tu configuración central.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

JWT frente a sesiones de base de datos

Estrategia Ventajas Costes
JWT (session.strategy: "jwt") Configuración sencilla, pocas consultas y buen encaje serverless. Revocar una sesión individual es más difícil; los tokens deben ser pequeños y los cambios de usuario pueden tardar en reflejarse.
Base de datos Revocación centralizada, control de sesiones activas y cierre global. Más consultas, adaptador y configuración; revisa compatibilidad con Credentials.

Ninguna estrategia es automáticamente más segura. Elige según revocación, latencia, escala y requisitos administrativos. Next.js explica estos compromisos en su guía de autenticación.

Best Value
The New Real Book
  • Used Book in Good Condition

Roles y autorización real

callbacks: {
  async jwt({ token, user }) {
    if (user) { token.id = user.id; token.role = user.role }
    return token
  },
  async session({ session, token }) {
    if (session.user) { session.user.id = token.id as string; session.user.role = token.role as string }
    return session
  },
}

Amplía los tipos en types/next-auth.d.ts para Session.user.id, Session.user.role, User.role y los campos del JWT. No permitas que el formulario elija su propio rol. Cada Server Action, Route Handler, consulta y mutación debe comprobar el rol en el servidor; ocultar un botón no es autorización.

Sign-in en cliente o servidor

Enfoque Cuándo usarlo
signIn de next-auth/react Formulario Client Component, errores inline, React Hook Form y navegación manual.
signIn de @/lib/auth Server Action, menos JavaScript en el cliente y flujo basado en useActionState.

React Hook Form y Server Actions son alternativas para el mismo formulario; combínalos solo cuando tengas una razón concreta.

Comprobaciones antes de publicar

  • Credenciales válidas redirigen a /dashboard y la sesión aparece en servidor y cliente.
  • Email vacío, formato inválido, contraseña vacía, menos de ocho caracteres, espacios y mayúsculas se comportan como esperas.
  • Usuario inexistente, contraseña incorrecta, usuario sin passwordHash y fallos de base de datos devuelven un error genérico.
  • El acceso directo sin sesión, la sesión caducada y el rol insuficiente quedan bloqueados.
  • Las Server Actions y Route Handlers rechazan peticiones directas sin autorización.
  • En producción hay HTTPS, AUTH_SECRET, cookies y variables de entorno correctas, rate limiting, recuperación de contraseña, verificación de email y logs sin contraseñas ni tokens.

Errores frecuentes y solución

Síntoma Causa probable Corrección
La sesión siempre es null Falta secreto, cookie, proveedor o se mezclan versiones. Comprueba AUTH_SECRET, origen/protocolo, SessionProvider, imports desde lib/auth y que el usuario tenga id.
authorize recibe campos vacíos Los nombres enviados no coinciden. Envía email y password exactamente como están declarados en credentials.
Zod valida pero el login falla La forma de los datos no prueba existencia, contraseña, base de datos ni sesión. Revisa authorize, Prisma, hash y cookies por separado.
Se revela información de usuarios Mensajes distintos para email inexistente y contraseña errónea. Usa “El email o la contraseña no son correctos”.
Middleware bloquea páginas públicas Matcher demasiado amplio. Limítalo a /dashboard/:path* y excluye login, registro y API pública.
Se protege solo la interfaz La autorización se hizo ocultando enlaces. Repite la comprobación en servidor, acciones, endpoints y mutaciones.

Cuándo elegir otra solución

Auth.js ofrece control y es una buena base para Credentials con Prisma. Supabase Auth o Clerk pueden reducir el mantenimiento de usuarios, recuperación y proveedores sociales. Auth0 encaja mejor con SSO empresarial. Vercel es un despliegue natural para Next.js y Prisma Postgres o Neon son opciones de PostgreSQL gestionado; revisa siempre límites, regiones, pooling, latencia y presupuesto de tu proyecto. Cambiar de proveedor también cambia la integración y puede hacer innecesario authorize.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Para una aplicación pequeña, Auth.js con JWT y PostgreSQL es un punto de partida razonable. Para requisitos de revocación administrativa, considera sesiones de base de datos; para equipos que no quieren mantener identidad, evalúa un proveedor gestionado.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Outdated Drivers Are Slowing You DownFree scan - exact matches
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.