The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →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.
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.
#1 Best Overall
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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePersistir 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.
Rank #2
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.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Clear out junk files and repair common Windows errors3Scan for outdated or missing drivers - takes under a minuteConstruir 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.
Rank #3
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.
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:
Rank #4
"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.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.
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
- 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
/dashboardy 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
passwordHashy 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.
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.
Quick Recap
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.




