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 usar 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 conoce, como un nombre de usuario y contraseña.
  2. Gestión de sesiones: Realiza un seguimiento del 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 usando características de React y Next.js:

Diagrama que muestra 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. Si bien puedes implementar una solución de autenticación personalizada, para mayor seguridad y simplicidad, recomendamos usar una biblioteca de autenticación. Estas ofrecen soluciones integradas para autenticación, gestión de sesiones y autorización, además de 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 API.
  3. Tras la verificación exitosa, se completa el proceso, indicando la autenticación exitosa del usuario.
  4. Si la verificación falla, se muestra un mensaje de error.

Considere 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>
  )
}
import { FormEvent } from 'react'
import { useRouter } from 'next/router'

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

  async function handleSubmit(event) {
    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 API (/api/auth/login).

Luego puede llamar a la API de su Proveedor de Autenticación en la ruta 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.' })
    }
  }
}
import { signIn } from '@/auth'

export default async function handler(req, res) {
  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 usar más recursos del servidor.

Nota importante: Aunque puede 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)

Configurar y eliminar 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: 'Successfully set cookie!' })
}
import { serialize } from 'cookie'
import { encrypt } from '@/app/lib/session'

export default function handler(req, res) {
  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: 'Successfully set cookie!' })
}

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).

Crear 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: 'Internal Server Error' })
  }
}
import db from '../../lib/db'

export default async function handler(req, res) {
  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: 'Internal Server Error' })
  }
}

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 utilizando 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 utilizando los datos de sesión almacenados en la base de datos. Estas verificaciones son más seguras y se utilizan para operaciones que requieren acceso a datos sensibles o acciones.

Para ambos casos, recomendamos:

Verificaciones optimistas con Middleware (Opcional)

Hay algunos casos en los que 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, incluidas las 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 de 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 en las que Middleware no debería ejecutarse
export const config = {
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
import { 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) {
  // 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 de la cookie
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

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

  // 6. 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 en las que 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 deben 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 la biblioteca de gestión de sesiones son compatibles.
  • Puede usar la propiedad matcher en Middleware para especificar en qué rutas debe 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
}
export default async function handler(req, res) {
  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 muestra 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 compatibles con Next.js y recursos para ayudarle a implementar una 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: