Autenticacion y Autorizacion
Por que la autenticacion es dificil
La autenticacion es una de las partes mas complejas de cualquier aplicacion web. Parece simple en la superficie — los usuarios inician sesion, los usuarios ven sus cosas — pero debajo hay un laberinto de preocupaciones de seguridad:
- Gestion de sesiones — Como mantienes a los usuarios logueados entre cargas de pagina sin hacer facil que los atacantes secuestren sesiones?
- Seguridad de tokens — Los JWTs pueden ser robados de localStorage. Las cookies necesitan flags apropiados (HttpOnly, Secure, SameSite).
- Hashing de contrasenas — Nunca almacenas contrasenas en texto plano. Las hasheas con bcrypt o Argon2, usando salt para prevenir ataques de rainbow table.
- Flujos OAuth — Redirigir a Google, recibir callbacks, intercambiar codigos por tokens, manejar casos extremos cuando los proveedores estan caidos.
- Proteccion CSRF — Prevenir que otros sitios web hagan peticiones en nombre de tus usuarios logueados.
- Rate limiting — Detener intentos de login por fuerza bruta antes de que alguien adivine una contrasena.
La buena noticia: no necesitas implementar nada de esto desde cero. Librerias como NextAuth.js y servicios como Clerk manejan las partes dificiles. La IA te ayuda a configurarlos correctamente. Tu trabajo es entender que hace cada pieza para que puedas tomar buenas decisiones y darle prompts efectivos a la IA.
Autenticacion vs. Autorizacion
Estos dos terminos suenan similar pero significan cosas muy diferentes.
Autenticacion responde: "Quien eres?" Es el proceso de login — verificar identidad a traves de contrasenas, OAuth, magic links o biometria.
Autorizacion responde: "Que puedes hacer?" Es el sistema de permisos — determinar a que recursos y acciones puede acceder un usuario basandose en su rol, plan u otros atributos.
Un ejemplo practico: cuando inicias sesion en una app de gestion de proyectos (autenticacion), puedes ver tus propios proyectos pero no los proyectos privados de otras personas (autorizacion). Un usuario admin puede ver todos los proyectos y eliminar cualquiera de ellos (diferente nivel de autorizacion, mismo sistema de autenticacion).
Ambos son esenciales. Un sistema con autenticacion pero sin autorizacion deja que cada usuario logueado haga todo. Un sistema con autorizacion pero sin autenticacion no puede verificar quien esta haciendo la peticion.
Configuracion de NextAuth.js
NextAuth.js (ahora llamado Auth.js) es la libreria de autenticacion mas popular para Next.js. Maneja proveedores OAuth, sesiones, callbacks y seguridad — tu solo lo configuras.
Instalacion
npm install next-auth@beta
El archivo de configuracion de auth
Crea tu configuracion de autenticacion:
// auth.ts (raiz del proyecto)
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
GitHub({
clientId: process.env.AUTH_GITHUB_ID!,
clientSecret: process.env.AUTH_GITHUB_SECRET!,
}),
],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user
const isProtected = nextUrl.pathname.startsWith("/dashboard")
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/login", nextUrl))
}
return true
},
},
})
Handler de ruta API
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
Variables de entorno
# .env.local
AUTH_SECRET="genera-un-string-aleatorio-de-32-caracteres" # npx auth secret
AUTH_GOOGLE_ID="tu-google-client-id"
AUTH_GOOGLE_SECRET="tu-google-client-secret"
AUTH_GITHUB_ID="tu-github-client-id"
AUTH_GITHUB_SECRET="tu-github-client-secret"
El AUTH_SECRET se usa para encriptar tokens de sesion. Generalo con npx auth secret o cualquier generador de strings aleatorios. Nunca lo reutilices entre entornos.
Implementando login OAuth
OAuth permite a los usuarios iniciar sesion con cuentas existentes (Google, GitHub, etc.) en lugar de crear nuevas credenciales. Es mas seguro (no hay contrasenas que almacenar) y mas conveniente (un clic para iniciar sesion).
Proveedor de Google paso a paso
-
Crea credenciales en Google Cloud Console:
- Ve a APIs & Services > Credentials
- Crea un OAuth 2.0 Client ID
- Establece el tipo de aplicacion como "Web application"
- Agrega URI de redireccion autorizado:
http://localhost:3000/api/auth/callback/google
-
Agrega credenciales a
.env.local:AUTH_GOOGLE_ID="123456789.apps.googleusercontent.com" AUTH_GOOGLE_SECRET="GOCSPX-tu-secreto-aqui" -
Crea un boton de inicio de sesion:
// components/sign-in-button.tsx
import { signIn } from "@/auth"
export function SignInButton() {
return (
<form
action={async () => {
"use server"
await signIn("google", { redirectTo: "/dashboard" })
}}
>
<button
type="submit"
className="flex items-center gap-2 px-4 py-2 bg-white border rounded-lg hover:bg-gray-50"
>
<GoogleIcon className="w-5 h-5" />
Iniciar sesion con Google
</button>
</form>
)
}
El flujo: El usuario hace clic en el boton → es redirigido a Google → el usuario aprueba → Google redirige de vuelta con un codigo → NextAuth intercambia el codigo por un token → se crea la sesion → el usuario es redirigido a /dashboard.
Prompt: "Configura login OAuth con Google usando NextAuth.js. Crea la configuracion de auth, la ruta de API, el boton de inicio de sesion y el boton de cerrar sesion. Redirige a /dashboard despues del login."
Autenticacion con email y contrasena
A veces necesitas el login tradicional con email y contrasena. NextAuth lo soporta a traves del proveedor Credentials, pero viene con advertencias — tu eres responsable de la seguridad de las contrasenas.
import Credentials from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Contrasena", type: "password" },
},
async authorize(credentials) {
const user = await db.user.findUnique({
where: { email: credentials.email as string },
})
if (!user) return null
const passwordMatch = await bcrypt.compare(
credentials.password as string,
user.password
)
if (!passwordMatch) return null
return { id: user.id, email: user.email, name: user.name }
},
}),
],
session: { strategy: "jwt" },
})
Flujo de registro
El endpoint de registro hashea la contrasena antes de almacenarla:
// app/api/auth/register/route.ts
import bcrypt from "bcryptjs"
import { z } from "zod"
const RegisterSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8)
.regex(/[A-Z]/, "Debe contener una letra mayuscula")
.regex(/[0-9]/, "Debe contener un numero"),
})
export async function POST(request: Request) {
const body = await request.json()
const result = RegisterSchema.safeParse(body)
if (!result.success) {
return NextResponse.json(
{ error: result.error.flatten() },
{ status: 400 }
)
}
const existingUser = await db.user.findUnique({
where: { email: result.data.email },
})
if (existingUser) {
return NextResponse.json(
{ error: "Email ya registrado" },
{ status: 409 }
)
}
const hashedPassword = await bcrypt.hash(result.data.password, 12)
const user = await db.user.create({
data: {
name: result.data.name,
email: result.data.email,
password: hashedPassword,
},
})
return NextResponse.json(
{ data: { id: user.id, email: user.email } },
{ status: 201 }
)
}
El numero 12 en bcrypt.hash(password, 12) es el factor de costo — cuantas rondas de hashing realizar. Numeros mas altos son mas lentos pero mas seguros. Doce es un buen balance.
Gestion de sesiones
Una vez que un usuario esta autenticado, necesitas mantener su sesion — recordar quien es a traves de las peticiones.
JWT vs. sesiones en base de datos
JWT (JSON Web Token): Los datos de la sesion se codifican en un token almacenado en una cookie. El servidor no necesita buscar la sesion — el token contiene la informacion. Rapido, pero mas dificil de revocar.
Sesiones en base de datos: La sesion se almacena en tu base de datos. La cookie solo contiene un ID de sesion. El servidor busca la sesion en cada peticion. Mas lento, pero puedes revocar sesiones instantaneamente.
Para la mayoria de apps, JWT esta bien. Usa sesiones en base de datos si necesitas la capacidad de forzar el cierre de sesion de usuarios o ver sesiones activas.
Accediendo a datos de sesion
En Server Components:
import { auth } from "@/auth"
export default async function DashboardPage() {
const session = await auth()
if (!session?.user) {
redirect("/login")
}
return <h1>Bienvenido, {session.user.name}</h1>
}
En Client Components:
"use client"
import { useSession } from "next-auth/react"
export function UserMenu() {
const { data: session, status } = useSession()
if (status === "loading") return <Skeleton />
if (!session) return <SignInButton />
return (
<div className="flex items-center gap-2">
<img src={session.user.image} className="w-8 h-8 rounded-full" />
<span>{session.user.name}</span>
</div>
)
}
En rutas de API:
import { auth } from "@/auth"
export async function GET() {
const session = await auth()
if (!session) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 })
}
// Continuar con la logica autenticada...
}
Session Provider
Envuelve el root layout de tu app para hacer las sesiones disponibles a los Client Components:
// app/layout.tsx
import { SessionProvider } from "next-auth/react"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
)
}
Protegiendo rutas y endpoints de API
La autenticacion es inutil si los usuarios pueden saltarla navegando directamente a URLs protegidas.
Proteccion basada en middleware
El enfoque mas eficiente — se ejecuta antes de que la pagina se renderice:
// middleware.ts
import { auth } from "@/auth"
export default auth((req) => {
const isLoggedIn = !!req.auth
const isProtectedRoute = req.nextUrl.pathname.startsWith("/dashboard")
const isAuthRoute = req.nextUrl.pathname.startsWith("/login")
if (isProtectedRoute && !isLoggedIn) {
return Response.redirect(new URL("/login", req.nextUrl))
}
if (isAuthRoute && isLoggedIn) {
return Response.redirect(new URL("/dashboard", req.nextUrl))
}
})
export const config = {
matcher: ["/dashboard/:path*", "/login", "/register"],
}
Esto maneja dos casos: redirigir usuarios no autenticados al login, y redirigir usuarios ya autenticados lejos de la pagina de login.
Proteccion del lado del servidor
Para paginas individuales que necesitan verificaciones adicionales:
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function SettingsPage() {
const session = await auth()
if (!session) redirect("/login")
// Solo alcanzado por usuarios autenticados
return <SettingsForm user={session.user} />
}
Proteccion de rutas de API
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
const session = await auth()
if (!session) {
return NextResponse.json({ error: "No autorizado" }, { status: 401 })
}
// Verificar que el usuario es dueno de este recurso
const post = await db.post.findUnique({ where: { id: params.id } })
if (post?.authorId !== session.user.id) {
return NextResponse.json({ error: "Prohibido" }, { status: 403 })
}
await db.post.delete({ where: { id: params.id } })
return NextResponse.json({ success: true })
}
Nota la diferencia entre 401 (no ha iniciado sesion) y 403 (ha iniciado sesion pero no tiene permiso). Esto importa para que el frontend muestre el mensaje correcto.
Control de acceso basado en roles
La mayoria de apps necesitan mas que solo "logueado" o "no logueado". Necesitas roles — admin, editor, viewer — que determinen lo que cada usuario puede hacer.
Agregando roles a tu modelo de datos
enum Role {
USER
EDITOR
ADMIN
}
model User {
id String @id @default(cuid())
email String @unique
name String
role Role @default(USER)
// ... otros campos
}
Extendiendo la sesion con el rol
// auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
callbacks: {
async session({ session, token }) {
if (token.role) {
session.user.role = token.role as Role
}
return session
},
async jwt({ token, user }) {
if (user) {
const dbUser = await db.user.findUnique({
where: { email: user.email! },
})
token.role = dbUser?.role
}
return token
},
},
// ... proveedores
})
Verificando roles en middleware
export default auth((req) => {
const isAdmin = req.auth?.user?.role === "ADMIN"
const isAdminRoute = req.nextUrl.pathname.startsWith("/admin")
if (isAdminRoute && !isAdmin) {
return Response.redirect(new URL("/dashboard", req.nextUrl))
}
})
UI condicional basada en rol
import { auth } from "@/auth"
export default async function Sidebar() {
const session = await auth()
return (
<nav>
<Link href="/dashboard">Dashboard</Link>
<Link href="/projects">Proyectos</Link>
{session?.user?.role === "ADMIN" && (
<Link href="/admin">Panel de Admin</Link>
)}
</nav>
)
}
Prompt: "Agrega control de acceso basado en roles con roles USER, EDITOR y ADMIN. Los editores pueden crear y editar publicaciones. Los admins pueden hacer todo incluyendo gestionar usuarios. Protege las rutas /admin solo para admins."
Clerk como alternativa gestionada
Si quieres autenticacion sin gestionar nada de la infraestructura, Clerk es un servicio gestionado que maneja todo — registro, inicio de sesion, gestion de sesiones, perfiles de usuario, organizaciones y mas.
Por que elegir Clerk
- Componentes pre-construidos — Formularios de inicio de sesion, botones de usuario y paginas de perfil que se ven profesionales de entrada
- Infraestructura gestionada — Sin tokens de sesion que configurar, sin credenciales OAuth que gestionar (Clerk actua como el cliente OAuth)
- Integracion con webhooks — Sincroniza datos de usuario a tu base de datos cuando ocurren eventos (usuario creado, actualizado, eliminado)
- Cero codigo de auth que escribir — Tu configuras, no codificas
Guia de configuracion
npm install @clerk/nextjs
// app/layout.tsx
import { ClerkProvider } from "@clerk/nextjs"
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider>
<html>
<body>{children}</body>
</html>
</ClerkProvider>
)
}
// middleware.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
const isProtectedRoute = createRouteMatcher(["/dashboard(.*)", "/api/protected(.*)"])
export default clerkMiddleware(async (auth, req) => {
if (isProtectedRoute(req)) {
await auth.protect()
}
})
Componentes integrados
import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs"
export function Header() {
return (
<header className="flex justify-between items-center p-4">
<Logo />
<SignedOut>
<SignInButton />
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</header>
)
}
Eso es todo. Sin archivo de configuracion de auth. Sin setup de proveedor. Sin handlers de callback. Clerk gestiona todo.
Cuando usar Clerk vs. NextAuth
Elige Clerk cuando: quieres moverte rapido, no quieres gestionar infraestructura de auth, estas bien con una dependencia de terceros, y tu app no necesita flujos de auth inusuales.
Elige NextAuth cuando: necesitas control total sobre el flujo de auth, quieres alojar todo tu mismo, necesitas proveedores personalizados, o estas integrando auth en un sistema existente.
Prompt: "Configura autenticacion con Clerk con login de Google y GitHub. Protege todas las rutas /dashboard. Agrega un endpoint de webhook que cree un usuario en mi base de datos cuando alguien se registra a traves de Clerk."
Errores comunes de autenticacion
Estos errores aparecen frecuentemente en codigo de auth generado por IA. Vigila:
-
Almacenar contrasenas en texto plano — Siempre hashea con bcrypt (factor de costo 10-12) o Argon2. Si ves
password: input.passwordyendo directamente a la base de datos, eso es un bug. -
No validar tokens del lado del servidor — Nunca confies en un JWT sin verificar su firma. NextAuth maneja esto, pero si estas construyendo auth personalizado, es critico.
-
Exponer IDs de usuario en codigo del cliente — Los IDs internos de la base de datos deben quedarse en el servidor. Usa tokens de sesion para la comunicacion cliente-servidor.
-
No aplicar rate limiting a intentos de login — Sin rate limiting, los atacantes pueden probar millones de contrasenas. Agrega un rate limiter a tu endpoint de login.
-
Confiar en verificaciones de auth del lado del cliente — El hook
useSessiones para renderizado de UI (mostrar/ocultar elementos). No es para seguridad. Siempre verifica sesiones en el servidor antes de realizar acciones. -
Olvidar proteger rutas de API — Cada ruta de API que accede a datos de usuario debe verificar la sesion. Es facil agregar una nueva ruta y olvidar la verificacion de auth.
Prompt: "Revisa la implementacion de auth por problemas de seguridad. Verifica contrasenas en texto plano, validacion del lado del servidor faltante, rutas de API sin proteger y rate limiting faltante."
Patrones de prompts para autenticacion
Estos prompts generan implementaciones solidas de auth de manera confiable:
Configuracion OAuth: "Agrega login OAuth con Google usando NextAuth.js. Crea la configuracion de auth con estrategia de sesion JWT, el handler de ruta API, una pagina de inicio de sesion con boton de Google, y un boton de cerrar sesion en el header."
Proteccion de rutas: "Protege la ruta /dashboard — redirige a /login si no esta autenticado. Tambien redirige usuarios autenticados lejos de /login hacia /dashboard."
Acceso basado en roles: "Agrega rol de admin y restringe las paginas /admin solo a usuarios admin. Muestra una pagina 403 si un no-admin intenta acceder a rutas de admin. Agrega una funcion helper de verificacion de admin."
Sistema de auth completo: "Implementa autenticacion completa con NextAuth.js: proveedores OAuth de Google y GitHub, email/contrasena con registro, gestion de sesiones, rutas protegidas, y control de acceso basado en roles con roles USER y ADMIN."
Que sigue
Tu app ahora tiene autenticacion segura, rutas protegidas y autorizacion basada en roles. Los usuarios pueden iniciar sesion, y el sistema sabe quienes son y que tienen permitido hacer.
Pero como sabes que todo funciona correctamente? Como previenes que los bugs se cuelen a medida que agregas funcionalidades? En la siguiente leccion, nos sumergiremos en el testing — dejando que la IA escriba tests unitarios, tests de integracion y tests end-to-end que te den la confianza de desplegar sin miedo.