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

Funcionalidad de registro e inicio de sesión

Puedes usar el elemento <form> con Server Actions de React y useActionState para capturar credenciales de usuario, validar campos del formulario y llamar a la API o base de datos de tu Proveedor de Autenticación.

Dado que las Server Actions siempre se ejecutan en el servidor, proporcionan un entorno seguro para manejar la lógica de autenticación.

Aquí están los pasos para implementar la funcionalidad de registro/inicio de sesión:

1. Capturar credenciales del usuario

Para capturar credenciales del usuario, crea un formulario que invoque una Server Action al enviarse. Por ejemplo, un formulario de registro que acepte el nombre, correo electrónico y contraseña del usuario:

import { signup } from '@/app/actions/auth'

export function SignupForm() {
  return (
    <form action={signup}>
      <div>
        <label htmlFor="name">Nombre</label>
        <input id="name" name="name" placeholder="Nombre" />
      </div>
      <div>
        <label htmlFor="email">Correo electrónico</label>
        <input id="email" name="email" type="email" placeholder="Correo electrónico" />
      </div>
      <div>
        <label htmlFor="password">Contraseña</label>
        <input id="password" name="password" type="password" />
      </div>
      <button type="submit">Registrarse</button>
    </form>
  )
}

2. Validar campos del formulario en el servidor

Usa la Server Action para validar los campos del formulario en el servidor. Si tu proveedor de autenticación no proporciona validación de formularios, puedes usar una biblioteca de validación de esquemas como Zod o Yup.

Usando Zod como ejemplo, puedes definir un esquema de formulario con mensajes de error apropiados:

import { z } from 'zod'

export const SignupFormSchema = z.object({
  name: z
    .string()
    .min(2, { message: 'El nombre debe tener al menos 2 caracteres.' })
    .trim(),
  email: z.string().email({ message: 'Por favor ingresa un correo electrónico válido.' }).trim(),
  password: z
    .string()
    .min(8, { message: 'Debe tener al menos 8 caracteres' })
    .regex(/[a-zA-Z]/, { message: 'Debe contener al menos una letra.' })
    .regex(/[0-9]/, { message: 'Debe contener al menos un número.' })
    .regex(/[^a-zA-Z0-9]/, {
      message: 'Debe contener al menos un carácter especial.',
    })
    .trim(),
})

export type FormState =
  | {
      errors?: {
        name?: string[]
        email?: string[]
        password?: string[]
      }
      message?: string
    }
  | undefined

Para evitar llamadas innecesarias a la API o base de datos de tu proveedor de autenticación, puedes hacer un return anticipado en la Server Action si algún campo del formulario no coincide con el esquema definido.

import { SignupFormSchema, FormState } from '@/app/lib/definitions'

export async function signup(state: FormState, formData: FormData) {
  // Validar campos del formulario
  const validatedFields = SignupFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    password: formData.get('password'),
  })

  // Si algún campo del formulario es inválido, retornar anticipadamente
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Llamar al proveedor o base de datos para crear un usuario...
}

De vuelta en tu <SignupForm />, puedes usar el hook useActionState de React para mostrar errores de validación mientras se envía el formulario:

'use client'

import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'

export default function SignupForm() {
  const [state, action, pending] = useActionState(signup, undefined)

  return (
    <form action={action}>
      <div>
        <label htmlFor="name">Nombre</label>
        <input id="name" name="name" placeholder="Nombre" />
      </div>
      {state?.errors?.name && <p>{state.errors.name}</p>}

      <div>
        <label htmlFor="email">Correo electrónico</label>
        <input id="email" name="email" placeholder="Correo electrónico" />
      </div>
      {state?.errors?.email && <p>{state.errors.email}</p>}

      <div>
        <label htmlFor="password">Contraseña</label>
        <input id="password" name="password" type="password" />
      </div>
      {state?.errors?.password && (
        <div>
          <p>La contraseña debe:</p>
          <ul>
            {state.errors.password.map((error) => (
              <li key={error}>- {error}</li>
            ))}
          </ul>
        </div>
      )}
      <button disabled={pending} type="submit">
        Registrarse
      </button>
    </form>
  )
}

Nota importante:

  • En React 19, useFormStatus incluye claves adicionales en el objeto devuelto, como data, method y action. Si no estás usando React 19, solo está disponible la clave pending.
  • Antes de mutar datos, siempre debes asegurarte de que un usuario también esté autorizado para realizar la acción. Consulta Autenticación y Autorización.

3. Crear un usuario o verificar credenciales

Después de validar los campos del formulario, puedes crear una nueva cuenta de usuario o verificar si el usuario existe llamando a la API o base de datos de tu proveedor de autenticación.

Continuando con el ejemplo anterior:

export async function signup(state: FormState, formData: FormData) {
  // 1. Validar campos del formulario
  // ...

  // 2. Preparar datos para inserción en la base de datos
  const { name, email, password } = validatedFields.data
  // Ejemplo: Hashear la contraseña antes de almacenarla
  const hashedPassword = await bcrypt.hash(password, 10)

  // 3. Insertar el usuario en la base de datos o llamar a la API de una biblioteca de autenticación
  const data = await db
    .insert(users)
    .values({
      name,
      email,
      password: hashedPassword,
    })
    .returning({ id: users.id })

  const user = data[0]

  if (!user) {
    return {
      message: 'Ocurrió un error al crear tu cuenta.',
    }
  }

  // TODO:
  // 4. Crear sesión de usuario
  // 5. Redirigir al usuario
}

Después de crear exitosamente la cuenta de usuario o verificar las credenciales, puedes crear una sesión para gestionar el estado de autenticación del usuario. Dependiendo de tu estrategia de gestión de sesiones, esta puede almacenarse en una cookie, base de datos o ambas. Continúa en la sección de Gestión de Sesiones para aprender más.

Consejos:

  • El ejemplo anterior es detallado ya que desglosa los pasos de autenticación con fines educativos. Esto resalta que implementar tu propia solución segura puede volverse complejo rápidamente. Considera usar una Biblioteca de Autenticación para simplificar el proceso.
  • Para mejorar la experiencia del usuario, podrías verificar correos electrónicos o nombres de usuario duplicados antes en el flujo de registro. Por ejemplo, mientras el usuario escribe un nombre de usuario o cuando el campo de entrada pierde el foco. Esto puede evitar envíos innecesarios del formulario y proporcionar retroalimentación inmediata al usuario. Puedes controlar la frecuencia de estas verificaciones con bibliotecas como use-debounce.

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)

Para crear y gestionar sesiones sin estado, hay algunos pasos que debes seguir:

  1. Generar una clave secreta, que se usará para firmar tu sesión, y almacenarla como una variable de entorno.
  2. Escribir lógica para encriptar/desencriptar datos de sesión usando una biblioteca de gestión de sesiones.
  3. Gestionar cookies usando la API cookies de Next.js.

Además de lo anterior, considera añadir funcionalidad para actualizar (o refrescar) la sesión cuando el usuario regrese a la aplicación, y eliminar la sesión cuando el usuario cierre sesión.

Es bueno saber: Verifica si tu biblioteca de autenticación incluye gestión de sesiones.

1. Generar una clave secreta

Hay varias formas de generar una clave secreta para firmar tu sesión. Por ejemplo, puedes usar el comando openssl en tu terminal:

terminal
openssl rand -base64 32

Este comando genera una cadena aleatoria de 32 caracteres que puedes usar como tu clave secreta y almacenar en tu archivo de variables de entorno:

.env
SESSION_SECRET=tu_clave_secreta

Luego puedes referenciar esta clave en tu lógica de gestión de sesiones:

app/lib/session.js
const secretKey = process.env.SESSION_SECRET

2. Encriptar y desencriptar sesiones

A continuación, puedes usar tu biblioteca de gestión de sesiones preferida para encriptar y desencriptar sesiones. Continuando con el ejemplo anterior, usaremos Jose (compatible con el Edge Runtime) y el paquete server-only de React para asegurar que tu lógica de gestión de sesiones solo se ejecute en el servidor.

import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'

const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)

export async function encrypt(payload: SessionPayload) {
  return new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d')
    .sign(encodedKey)
}

export async function decrypt(session: string | undefined = '') {
  try {
    const { payload } = await jwtVerify(session, encodedKey, {
      algorithms: ['HS256'],
    })
    return payload
  } catch (error) {
    console.log('Error al verificar la sesión')
  }
}

Consejos:

  • El payload debe contener los datos de usuario mínimos y únicos que se usarán en solicitudes posteriores, como el ID del usuario, rol, etc. No debe contener información personal identificable como número de teléfono, dirección de correo electrónico, información de tarjetas de crédito, etc., o datos sensibles como contraseñas.

3. Configurar cookies (opciones recomendadas)

Para almacenar la sesión en una cookie, usa la API cookies de Next.js. La cookie debe configurarse en el servidor e incluir las opciones recomendadas:

  • HttpOnly: Evita que JavaScript del lado del cliente acceda a la cookie.
  • Secure: Usa https para enviar la cookie.
  • SameSite: Especifica si la cookie puede enviarse con solicitudes entre sitios.
  • Max-Age o Expires: Elimina la cookie después de un período determinado.
  • Path: Define la ruta URL para la cookie.

Consulta MDN para más información sobre cada una de estas opciones.

import 'server-only'
import { cookies } from 'next/headers'

export async function createSession(userId: string) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  const session = await encrypt({ userId, expiresAt })
  const cookieStore = await cookies()

  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

De vuelta en tu Acción de Servidor, puedes invocar la función createSession() y usar la API redirect() para redirigir al usuario a la página apropiada:

import { createSession } from '@/app/lib/session'

export async function signup(state: FormState, formData: FormData) {
  // Pasos previos:
  // 1. Validar campos del formulario
  // 2. Preparar datos para inserción en la base de datos
  // 3. Insertar el usuario en la base de datos o llamar a una API de biblioteca

  // Pasos actuales:
  // 4. Crear sesión de usuario
  await createSession(user.id)
  // 5. Redirigir al usuario
  redirect('/profile')
}

Consejos:

  • Las cookies deben configurarse en el servidor para evitar manipulaciones del lado del cliente.
  • 🎥 Mira: Aprende más sobre sesiones sin estado y autenticación con Next.js → YouTube (11 minutos).

Actualizar (o refrescar) sesiones

También puedes extender el tiempo de expiración de la sesión. Esto es útil para mantener al usuario conectado después de que acceda nuevamente a la aplicación. Por ejemplo:

import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export async function updateSession() {
  const session = (await cookies()).get('session')?.value
  const payload = await decrypt(session)

  if (!session || !payload) {
    return null
  }

  const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expires,
    sameSite: 'lax',
    path: '/',
  })
}

Consejo: Verifica si tu biblioteca de autenticación admite tokens de actualización, que pueden usarse para extender la sesión del usuario.

Eliminación de la sesión

Para eliminar la sesión, puede borrar la cookie:

import 'server-only'
import { cookies } from 'next/headers'

export async function deleteSession() {
  const cookieStore = await cookies()
  cookieStore.delete('session')
}

Luego puede reutilizar la función deleteSession() en su aplicación, por ejemplo, al cerrar sesión:

import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'

export async function logout() {
  await deleteSession()
  redirect('/login')
}

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

Por ejemplo:

import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'

export async function createSession(id: number) {
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)

  // 1. Crear una sesión en la base de datos
  const data = await db
    .insert(sessions)
    .values({
      userId: id,
      expiresAt,
    })
    // Devolver el ID de sesión
    .returning({ id: sessions.id })

  const sessionId = data[0].id

  // 2. Cifrar el ID de sesión
  const session = await encrypt({ sessionId, expiresAt })

  // 3. Almacenar la sesión en cookies para verificaciones optimistas de autenticación
  const cookieStore = await cookies()
  cookieStore.set('session', session, {
    httpOnly: true,
    secure: true,
    expires: expiresAt,
    sameSite: 'lax',
    path: '/',
  })
}

Consejos:

  • Para un acceso más rápido, puede considerar agregar caché del servidor durante el tiempo de vida de la sesión. También puede mantener los datos de sesión en su base de datos principal y combinar solicitudes de datos para reducir el número de consultas.
  • Puede optar por usar sesiones en base de datos para casos de uso más avanzados, como realizar un seguimiento de la última vez que un usuario inició sesión, el número de dispositivos activos o dar a los usuarios la capacidad de cerrar sesión en todos los dispositivos.

Después de implementar la gestión de sesiones, deberá agregar lógica de autorización para controlar lo que los usuarios pueden acceder y hacer dentro de su aplicación. Continúe a la sección Autorización para obtener más información.

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)

Recomendamos crear una DAL para centralizar sus solicitudes de datos y lógica de autorización.

La DAL debe incluir una función que verifique la sesión del usuario mientras interactúa con su aplicación. Como mínimo, la función debería verificar si la sesión es válida, luego redirigir o devolver la información del usuario necesaria para realizar más solicitudes.

Por ejemplo, cree un archivo separado para su DAL que incluya una función verifySession(). Luego use la API de cache de React para memorizar el valor de retorno de la función durante un pase de renderizado de React:

import 'server-only'

import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'

export const verifySession = cache(async () => {
  const cookie = (await cookies()).get('session')?.value
  const session = await decrypt(cookie)

  if (!session?.userId) {
    redirect('/login')
  }

  return { isAuth: true, userId: session.userId }
})

Luego puede invocar la función verifySession() en sus solicitudes de datos, Acciones de Servidor, Manejadores de Ruta:

export const getUser = cache(async () => {
  const session = await verifySession()
  if (!session) return null

  try {
    const data = await db.query.users.findMany({
      where: eq(users.id, session.userId),
      // Devuelva explícitamente las columnas que necesita en lugar de todo el objeto de usuario
      columns: {
        id: true,
        name: true,
        email: true,
      },
    })

    const user = data[0]

    return user
  } catch (error) {
    console.log('Error al obtener usuario')
    return null
  }
})

Consejo:

  • Una DAL puede usarse para proteger datos obtenidos en tiempo de solicitud. Sin embargo, para rutas estáticas que comparten datos entre usuarios, los datos se obtendrán en tiempo de compilación y no en tiempo de solicitud. Use Middleware para proteger rutas estáticas.
  • Para verificaciones seguras, puede verificar si la sesión es válida comparando el ID de sesión con su base de datos. Use la función cache de React para evitar solicitudes duplicadas innecesarias a la base de datos durante un pase de renderizado.
  • Puede consolidar solicitudes de datos relacionadas en una clase JavaScript que ejecute verifySession() antes de cualquier método.

Uso de Objetos de Transferencia de Datos (DTO)

Al recuperar datos, se recomienda devolver solo la información necesaria que se utilizará en su aplicación, y no objetos completos. Por ejemplo, si está obteniendo datos de usuario, podría devolver solo el ID y el nombre del usuario, en lugar del objeto de usuario completo que podría contener contraseñas, números de teléfono, etc.

Sin embargo, si no tiene control sobre la estructura de datos devuelta, o está trabajando en equipo y desea evitar que se pasen objetos completos al cliente, puede utilizar estrategias como especificar qué campos son seguros para exponer al cliente.

import 'server-only'
import { getUser } from '@/app/lib/dal'

function canSeeUsername(viewer: User) {
  return true
}

function canSeePhoneNumber(viewer: User, team: string) {
  return viewer.isAdmin || team === viewer.team
}

export async function getProfileDTO(slug: string) {
  const data = await db.query.users.findMany({
    where: eq(users.slug, slug),
    // Devuelve columnas específicas aquí
  })
  const user = data[0]

  const currentUser = await getUser(user.id)

  // O devuelve solo lo específico para la consulta aquí
  return {
    username: canSeeUsername(currentUser) ? user.username : null,
    phonenumber: canSeePhoneNumber(currentUser, user.team)
      ? user.phonenumber
      : null,
  }
}

Al centralizar sus solicitudes de datos y la lógica de autorización en una DAL (Capa de Acceso a Datos) y utilizar DTOs, puede asegurarse de que todas las solicitudes de datos sean seguras y consistentes, facilitando el mantenimiento, auditoría y depuración a medida que su aplicación escala.

Es bueno saberlo:

  • Hay varias formas de definir un DTO, desde usar toJSON(), hasta funciones individuales como en el ejemplo anterior, o clases de JS. Dado que estos son patrones de JavaScript y no una característica de React o Next.js, recomendamos investigar para encontrar el mejor patrón para su aplicación.
  • Aprenda más sobre las mejores prácticas de seguridad en nuestro artículo sobre Seguridad en Next.js.

Componentes del Servidor

Las verificaciones de autenticación en Componentes del Servidor son útiles para el acceso basado en roles. Por ejemplo, para renderizar componentes condicionalmente según el rol del usuario:

import { verifySession } from '@/app/lib/dal'

export default function Dashboard() {
  const session = await verifySession()
  const userRole = session?.user?.role // Asumiendo que 'role' es parte del objeto de sesión

  if (userRole === 'admin') {
    return <AdminDashboard />
  } else if (userRole === 'user') {
    return <UserDashboard />
  } else {
    redirect('/login')
  }
}

En el ejemplo, usamos la función verifySession() de nuestra DAL para verificar los roles 'admin', 'user' y no autorizados. Este patrón asegura que cada usuario interactúe solo con los componentes apropiados para su rol.

Diseños y verificaciones de autenticación

Debido al Renderizado Parcial, tenga cuidado al realizar verificaciones en Diseños, ya que estos no se vuelven a renderizar en la navegación, lo que significa que la sesión del usuario no se verificará en cada cambio de ruta.

En su lugar, debe realizar las verificaciones cerca de su fuente de datos o del componente que se renderizará condicionalmente.

Por ejemplo, considere un diseño compartido que obtiene los datos del usuario y muestra la imagen del usuario en una navegación. En lugar de hacer la verificación de autenticación en el diseño, debe obtener los datos del usuario (getUser()) en el diseño y hacer la verificación de autenticación en su DAL.

Esto garantiza que donde sea que se llame a getUser() dentro de su aplicación, se realice la verificación de autenticación, y evita que los desarrolladores olviden verificar si el usuario está autorizado para acceder a los datos.

export default async function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getUser();

  return (
    // ...
  )
}

Es bueno saberlo:

  • Un patrón común en SPAs es devolver null en un diseño o un componente de nivel superior si un usuario no está autorizado. Este patrón no se recomienda ya que las aplicaciones de Next.js tienen múltiples puntos de entrada, lo que no evitará que se acceda a segmentos de ruta anidados y Acciones del Servidor.

Acciones del Servidor

Trate las Acciones del Servidor con las mismas consideraciones de seguridad que los puntos finales de API públicos, y verifique si el usuario está autorizado para realizar una mutación.

En el ejemplo a continuación, verificamos el rol del usuario antes de permitir que la acción continúe:

'use server'
import { verifySession } from '@/app/lib/dal'

export async function serverAction(formData: FormData) {
  const session = await verifySession()
  const userRole = session?.user?.role

  // Devuelve temprano si el usuario no está autorizado para realizar la acción
  if (userRole !== 'admin') {
    return null
  }

  // Procede con la acción para usuarios autorizados
}

Manejadores de Ruta

Trate los Manejadores de Ruta con las mismas consideraciones de seguridad que los puntos finales de API públicos, y verifique si el usuario está autorizado para acceder al Manejador de Ruta.

Por ejemplo:

import { verifySession } from '@/app/lib/dal'

export async function GET() {
  // Autenticación del usuario y verificación de rol
  const session = await verifySession()

  // Verifica si el usuario está autenticado
  if (!session) {
    // El usuario no está autenticado
    return new Response(null, { status: 401 })
  }

  // Verifica si el usuario tiene el rol 'admin'
  if (session.user.role !== 'admin') {
    // El usuario está autenticado pero no tiene los permisos correctos
    return new Response(null, { status: 403 })
  }

  // Continúa para usuarios autorizados
}

El ejemplo anterior demuestra un Manejador de Ruta con una verificación de seguridad de dos niveles. Primero verifica si hay una sesión activa, y luego verifica si el usuario que inició sesión es un 'admin'.

Proveedores de Contexto

El uso de proveedores de contexto para autenticación funciona debido al entrelazado. Sin embargo, el context de React no es compatible con los Componentes del Servidor, lo que los hace aplicables solo a Componentes del Cliente.

Esto funciona, pero cualquier Componente del Servidor hijo se renderizará primero en el servidor y no tendrá acceso a los datos de sesión del proveedor de contexto:

import { ContextProvider } from 'auth-lib'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <ContextProvider>{children}</ContextProvider>
      </body>
    </html>
  )
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}
'use client';

import { useSession } from "auth-lib";

export default function Profile() {
  const { userId } = useSession();
  const { data } = useSWR(`/api/user/${userId}`, fetcher)

  return (
    // ...
  );
}

Si se necesitan datos de sesión en Componentes del Cliente (por ejemplo, para la obtención de datos del lado del cliente), use la API taintUniqueValue de React para evitar que los datos sensibles de sesión se expongan al cliente.

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: