Introducción/Guías/Autenticación

Cómo implementar autenticación en Next.js

Comprender la autenticación es crucial para proteger los datos de tu aplicación. Esta página te guiará a través de las características de React y Next.js que puedes utilizar para implementar autenticación.

Antes de comenzar, es útil dividir el proceso en tres conceptos:

  1. Autenticación: Verifica si el usuario es quien dice ser. Requiere que el usuario demuestre su identidad con algo que posee, como un nombre de usuario y contraseña.
  2. Gestión de sesiones: Rastrea el estado de autenticación del usuario entre solicitudes.
  3. Autorización: Decide qué rutas y datos puede acceder el usuario.

Este diagrama muestra el flujo de autenticación utilizando características de React y Next.js:

Diagrama mostrando el flujo de autenticación con características de React y Next.js

Los ejemplos en esta página explican una autenticación básica con nombre de usuario y contraseña con fines educativos. Aunque puedes implementar una solución de autenticación personalizada, para mayor seguridad y simplicidad, recomendamos usar una biblioteca de autenticación. Estas ofrecen soluciones incorporadas para autenticación, gestión de sesiones y autorización, así como características adicionales como inicios de sesión sociales, autenticación multifactor y control de acceso basado en roles. Puedes encontrar una lista en la sección Bibliotecas de Autenticación.

Autenticación

Estos son los pasos para implementar un formulario de registro y/o inicio de sesión:

  1. El usuario envía sus credenciales a través de un formulario.
  2. El formulario envía una solicitud manejada por una ruta de API.
  3. Tras una verificación exitosa, el proceso se completa, indicando la autenticación exitosa del usuario.
  4. Si la verificación falla, se muestra un mensaje de error.

Considera un formulario de inicio de sesión donde los usuarios pueden ingresar sus credenciales:

import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      router.push('/profile')
    } else {
      // Manejar errores
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Contraseña" required />
      <button type="submit">Iniciar sesión</button>
    </form>
  )
}

El formulario anterior tiene dos campos de entrada para capturar el correo electrónico y la contraseña del usuario. Al enviarlo, se activa una función que envía una solicitud POST a una ruta de API (/api/auth/login).

Luego puedes llamar a la API de tu Proveedor de Autenticación en la ruta de API para manejar la autenticación:

import type { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: 'Credenciales inválidas.' })
    } else {
      res.status(500).json({ error: 'Algo salió mal.' })
    }
  }
}

Gestión de Sesiones

La gestión de sesiones asegura que el estado autenticado del usuario se mantenga entre solicitudes. Incluye crear, almacenar, actualizar y eliminar sesiones o tokens.

Existen dos tipos de sesiones:

  1. Sin estado (Stateless): Los datos de la sesión (o un token) se almacenan en las cookies del navegador. La cookie se envía con cada solicitud, permitiendo verificar la sesión en el servidor. Este método es más simple, pero puede ser menos seguro si no se implementa correctamente.
  2. Base de datos: Los datos de la sesión se almacenan en una base de datos, y el navegador del usuario solo recibe el ID de sesión encriptado. Este método es más seguro, pero puede ser complejo y consumir más recursos del servidor.

Es bueno saber: Aunque puedes usar cualquiera de los métodos, o ambos, recomendamos usar una biblioteca de gestión de sesiones como iron-session o Jose.

Sesiones sin estado (Stateless)

Configuración y eliminación de cookies

Puede usar Rutas API para establecer la sesión como una cookie en el servidor:

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'
import { encrypt } from '@/app/lib/session'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // Una semana
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: '¡Cookie configurada exitosamente!' })
}

Sesiones en base de datos

Para crear y administrar sesiones en base de datos, deberá seguir estos pasos:

  1. Crear una tabla en su base de datos para almacenar sesiones y datos (o verificar si su biblioteca de autenticación maneja esto).
  2. Implementar funcionalidad para insertar, actualizar y eliminar sesiones.
  3. Cifrar el ID de sesión antes de almacenarlo en el navegador del usuario, y asegurarse de que la base de datos y la cookie estén sincronizadas (esto es opcional, pero recomendado para verificaciones optimistas de autenticación en Middleware).

Creación de una sesión en el servidor:

import db from '../../lib/db'
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: 'Error interno del servidor' })
  }
}

Autorización

Una vez que un usuario está autenticado y se crea una sesión, puede implementar autorización para controlar lo que el usuario puede acceder y hacer dentro de su aplicación.

Hay dos tipos principales de verificaciones de autorización:

  1. Optimistas: Verifican si el usuario está autorizado para acceder a una ruta o realizar una acción usando los datos de sesión almacenados en la cookie. Estas verificaciones son útiles para operaciones rápidas, como mostrar/ocultar elementos de la interfaz de usuario o redirigir usuarios según permisos o roles.
  2. Seguras: Verifican si el usuario está autorizado para acceder a una ruta o realizar una acción usando los datos de sesión almacenados en la base de datos. Estas verificaciones son más seguras y se usan para operaciones que requieren acceso a datos sensibles o acciones.

Para ambos casos, recomendamos:

Verificaciones optimistas con Middleware (Opcional)

Hay algunos casos donde puede querer usar Middleware y redirigir usuarios según permisos:

  • Para realizar verificaciones optimistas. Dado que Middleware se ejecuta en cada ruta, es una buena manera de centralizar la lógica de redirección y pre-filtrar usuarios no autorizados.
  • Para proteger rutas estáticas que comparten datos entre usuarios (ej. contenido detrás de un muro de pago).

Sin embargo, dado que Middleware se ejecuta en cada ruta, incluyendo rutas precargadas, es importante solo leer la sesión de la cookie (verificaciones optimistas), y evitar verificaciones en la base de datos para prevenir problemas de rendimiento.

Por ejemplo:

import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'

// 1. Especificar rutas protegidas y públicas
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']

export default async function middleware(req: NextRequest) {
  // 2. Verificar si la ruta actual es protegida o pública
  const path = req.nextUrl.pathname
  const isProtectedRoute = protectedRoutes.includes(path)
  const isPublicRoute = publicRoutes.includes(path)

  // 3. Descifrar la sesión desde la cookie
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  // 4. Redirigir a /login si el usuario no está autenticado
  if (isProtectedRoute && !session?.userId) {
    return NextResponse.redirect(new URL('/login', req.nextUrl))
  }

  // 5. Redirigir a /dashboard si el usuario está autenticado
  if (
    isPublicRoute &&
    session?.userId &&
    !req.nextUrl.pathname.startsWith('/dashboard')
  ) {
    return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
  }

  return NextResponse.next()
}

// Rutas donde Middleware no debería ejecutarse
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

Si bien Middleware puede ser útil para verificaciones iniciales, no debería ser su única línea de defensa para proteger sus datos. La mayoría de las verificaciones de seguridad deberían realizarse lo más cerca posible de su fuente de datos, consulte Capa de Acceso a Datos para obtener más información.

Consejos:

  • En Middleware, también puede leer cookies usando req.cookies.get('session').value.
  • Middleware usa el Edge Runtime, verifique si su biblioteca de autenticación y gestión de sesiones son compatibles.
  • Puede usar la propiedad matcher en Middleware para especificar en qué rutas debería ejecutarse Middleware. Aunque, para autenticación, se recomienda que Middleware se ejecute en todas las rutas.

Creación de una Capa de Acceso a Datos (DAL)

Protección de Rutas de API

Las Rutas de API en Next.js son esenciales para manejar la lógica del lado del servidor y la gestión de datos. Es crucial asegurar estas rutas para garantizar que solo los usuarios autorizados puedan acceder a funcionalidades específicas. Esto generalmente implica verificar el estado de autenticación del usuario y sus permisos basados en roles.

Aquí hay un ejemplo de cómo asegurar una Ruta de API:

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)

  // Verifica si el usuario está autenticado
  if (!session) {
    res.status(401).json({
      error: 'El usuario no está autenticado',
    })
    return
  }

  // Verifica si el usuario tiene el rol 'admin'
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: 'Acceso no autorizado: El usuario no tiene privilegios de administrador.',
    })
    return
  }

  // Procede con la ruta para usuarios autorizados
  // ... implementación de la Ruta de API
}

Este ejemplo demuestra una Ruta de API con una verificación de seguridad de dos niveles para autenticación y autorización. Primero verifica si hay una sesión activa, y luego verifica si el usuario que inició sesión es un 'admin'. Este enfoque garantiza un acceso seguro, limitado a usuarios autenticados y autorizados, manteniendo una seguridad robusta para el procesamiento de solicitudes.

Recursos

Ahora que ha aprendido sobre autenticación en Next.js, aquí hay bibliotecas y recursos compatibles con Next.js para ayudarle a implementar autenticación segura y gestión de sesiones:

Bibliotecas de Autenticación

Bibliotecas de Gestión de Sesiones

Lectura Adicional

Para continuar aprendiendo sobre autenticación y seguridad, consulte los siguientes recursos: