Saltar al contenido
Lección 13 de 22

Autenticación y Autorización

14 min read

Por qué la autenticación es difícil

La autenticación es una de las partes más complejas de cualquier aplicación web. Parece simple en la superficie — los usuarios inician sesión, los usuarios ven sus cosas — pero debajo hay un laberinto de preocupaciones de seguridad:

  • Gestión de sesiones — ¿Cómo mantienes a los usuarios logueados entre cargas de página sin hacer fácil 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 contraseñas — Nunca almacenas contraseñas 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 códigos por tokens, manejar casos extremos cuando los proveedores están caídos.
  • Protección 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 contraseña.

La buena noticia: no necesitas implementar nada de esto desde cero. Librerías como NextAuth.js y servicios como Clerk manejan las partes difíciles. La IA te ayuda a configurarlos correctamente. Tu trabajo es entender qué hace cada pieza para que puedas tomar buenas decisiones y darle prompts efectivos a la IA.

Autenticación vs. Autorización

Estos dos términos suenan similar pero significan cosas muy diferentes.

Autenticación responde: "¿Quién eres?" Es el proceso de login — verificar identidad a través de contraseñas, OAuth, magic links o biometría.

Autorización responde: "¿Qué puedes hacer?" Es el sistema de permisos — determinar a qué recursos y acciones puede acceder un usuario basándose en su rol, plan u otros atributos.

Un ejemplo práctico: cuando inicias sesión en una app de gestión de proyectos (autenticación), puedes ver tus propios proyectos pero no los proyectos privados de otras personas (autorización). Un usuario admin puede ver todos los proyectos y eliminar cualquiera de ellos (diferente nivel de autorización, mismo sistema de autenticación).

Ambos son esenciales. Un sistema con autenticación pero sin autorización deja que cada usuario logueado haga todo. Un sistema con autorización pero sin autenticación no puede verificar quién está haciendo la petición.

Configuración de NextAuth.js

NextAuth.js (ahora llamado Auth.js) es la librería de autenticación más popular para Next.js. Maneja proveedores OAuth, sesiones, callbacks y seguridad — tú solo lo configuras.

Instalación

npm install next-auth@beta

El archivo de configuración de auth

Crea tu configuración de autenticación:

// 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 sesión. Genéralo con npx auth secret o cualquier generador de strings aleatorios. Nunca lo reutilices entre entornos.

Implementando login OAuth

OAuth permite a los usuarios iniciar sesión con cuentas existentes (Google, GitHub, etc.) en lugar de crear nuevas credenciales. Es más seguro (no hay contraseñas que almacenar) y más conveniente (un clic para iniciar sesión).

Proveedor de Google paso a paso

  1. Crea credenciales en Google Cloud Console:

    • Ve a APIs & Services > Credentials
    • Crea un OAuth 2.0 Client ID
    • Establece el tipo de aplicación como "Web application"
    • Agrega URI de redirección autorizado: http://localhost:3000/api/auth/callback/google
  2. Agrega credenciales a .env.local:

    AUTH_GOOGLE_ID="123456789.apps.googleusercontent.com"
    AUTH_GOOGLE_SECRET="GOCSPX-tu-secreto-aqui"
    
  3. Crea un botón de inicio de sesión:

// 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 botón → es redirigido a Google → el usuario aprueba → Google redirige de vuelta con un código → NextAuth intercambia el código por un token → se crea la sesión → el usuario es redirigido a /dashboard.

Prompt: "Configura login OAuth con Google usando NextAuth.js. Crea la configuración de auth, la ruta de API, el botón de inicio de sesión y el botón de cerrar sesión. Redirige a /dashboard después del login."

Autenticación con email y contraseña

A veces necesitas el login tradicional con email y contraseña. NextAuth lo soporta a través del proveedor Credentials, pero viene con advertencias — tú eres responsable de la seguridad de las contraseñas.

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 contraseña 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 número 12 en bcrypt.hash(password, 12) es el factor de costo — cuántas rondas de hashing realizar. Números más altos son más lentos pero más seguros. Doce es un buen balance.

Gestión de sesiones

Una vez que un usuario está autenticado, necesitas mantener su sesión — recordar quién es a través de las peticiones.

JWT vs. sesiones en base de datos

JWT (JSON Web Token): Los datos de la sesión se codifican en un token almacenado en una cookie. El servidor no necesita buscar la sesión — el token contiene la información. Rápido, pero más difícil de revocar.

Sesiones en base de datos: La sesión se almacena en tu base de datos. La cookie solo contiene un ID de sesión. El servidor busca la sesión en cada petición. Más lento, pero puedes revocar sesiones instantáneamente.

Para la mayoría de apps, JWT está bien. Usa sesiones en base de datos si necesitas la capacidad de forzar el cierre de sesión de usuarios o ver sesiones activas.

Accediendo a datos de sesión

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 autenticación es inútil si los usuarios pueden saltarla navegando directamente a URLs protegidas.

Protección basada en middleware

El enfoque más eficiente — se ejecuta antes de que la página 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 página de login.

Protección del lado del servidor

Para páginas 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} />
}

Protección 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 sesión) y 403 (ha iniciado sesión pero no tiene permiso). Esto importa para que el frontend muestre el mensaje correcto.

Control de acceso basado en roles

La mayoría de apps necesitan más 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 sesión 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 autenticación sin gestionar nada de la infraestructura, Clerk es un servicio gestionado que maneja todo — registro, inicio de sesión, gestión de sesiones, perfiles de usuario, organizaciones y más.

Por qué elegir Clerk

  • Componentes pre-construidos — Formularios de inicio de sesión, botones de usuario y páginas de perfil que se ven profesionales de entrada
  • Infraestructura gestionada — Sin tokens de sesión que configurar, sin credenciales OAuth que gestionar (Clerk actúa como el cliente OAuth)
  • Integración con webhooks — Sincroniza datos de usuario a tu base de datos cuando ocurren eventos (usuario creado, actualizado, eliminado)
  • Cero código de auth que escribir — Tú configuras, no codificas

Guía de configuración

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 configuración de auth. Sin setup de proveedor. Sin handlers de callback. Clerk gestiona todo.

Cuándo usar Clerk vs. NextAuth

Elige Clerk cuando: quieres moverte rápido, no quieres gestionar infraestructura de auth, estás 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 tú mismo, necesitas proveedores personalizados, o estás integrando auth en un sistema existente.

Prompt: "Configura autenticación 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 través de Clerk."

Errores comunes de autenticación

Estos errores aparecen frecuentemente en código de auth generado por IA. Vigila:

  1. Almacenar contraseñas en texto plano — Siempre hashea con bcrypt (factor de costo 10-12) o Argon2. Si ves password: input.password yendo directamente a la base de datos, eso es un bug.

  2. No validar tokens del lado del servidor — Nunca confíes en un JWT sin verificar su firma. NextAuth maneja esto, pero si estás construyendo auth personalizado, es crítico.

  3. Exponer IDs de usuario en código del cliente — Los IDs internos de la base de datos deben quedarse en el servidor. Usa tokens de sesión para la comunicación cliente-servidor.

  4. No aplicar rate limiting a intentos de login — Sin rate limiting, los atacantes pueden probar millones de contraseñas. Agrega un rate limiter a tu endpoint de login.

  5. Confiar en verificaciones de auth del lado del cliente — El hook useSession es para renderizado de UI (mostrar/ocultar elementos). No es para seguridad. Siempre verifica sesiones en el servidor antes de realizar acciones.

  6. Olvidar proteger rutas de API — Cada ruta de API que accede a datos de usuario debe verificar la sesión. Es fácil agregar una nueva ruta y olvidar la verificación de auth.

Prompt: "Revisa la implementación de auth por problemas de seguridad. Verifica contraseñas en texto plano, validación del lado del servidor faltante, rutas de API sin proteger y rate limiting faltante."

Patrones de prompts para autenticación

Estos prompts generan implementaciones sólidas de auth de manera confiable:

Configuración OAuth: "Agrega login OAuth con Google usando NextAuth.js. Crea la configuración de auth con estrategia de sesión JWT, el handler de ruta API, una página de inicio de sesión con botón de Google, y un botón de cerrar sesión en el header."

Protección de rutas: "Protege la ruta /dashboard — redirige a /login si no está autenticado. También redirige usuarios autenticados lejos de /login hacia /dashboard."

Acceso basado en roles: "Agrega rol de admin y restringe las páginas /admin solo a usuarios admin. Muestra una página 403 si un no-admin intenta acceder a rutas de admin. Agrega una función helper de verificación de admin."

Sistema de auth completo: "Implementa autenticación completa con NextAuth.js: proveedores OAuth de Google y GitHub, email/contraseña con registro, gestión de sesiones, rutas protegidas, y control de acceso basado en roles con roles USER y ADMIN."

Qué sigue

Tu app ahora tiene autenticación segura, rutas protegidas y autorización basada en roles. Los usuarios pueden iniciar sesión, y el sistema sabe quiénes son y qué tienen permitido hacer.

Pero ¿cómo sabes que todo funciona correctamente? ¿Cómo previenes que los bugs se cuelen a medida que agregas funcionalidades? En la siguiente lección, nos sumergiremos en el testing — dejando que la IA escriba tests unitarios, tests de integración y tests end-to-end que te den la confianza de desplegar sin miedo.