Inicio / Artículos

Inicio de sesión con Google mediante Auth.js y Prisma: Integrar las guías de Next.js

Combina las guías de Prisma y Auth.js para Next.js en una sola aplicación funcional con inicio de sesión mediante Google, usuarios persistentes y publicaciones vinculadas a la sesión, incluyendo las correcciones que los documentos omiten.

Editor: REACTAPP.TOP10 min de lectura2404 palabras

Fuentes: Prisma + NextAuth setup for NextJS

Agregar cuentas y datos guardados a una aplicación Next.js no requiere un backend separado. Con Auth.js (el sucesor de NextAuth) para la autenticación, Prisma como ORM y una base de datos Postgres alojada como Neon, el mismo proyecto Next.js puede gestionar el inicio de sesión, las sesiones y la persistencia. Cada herramienta cuenta con una guía decente para Next.js por separado, pero estas guías difieren en cuanto a la estructura de carpetas, se basan en convenciones obsoletas y nunca muestran cómo se conectan las diferentes partes. Esta guía sigue ese orden de guías e indica cada ajuste necesario, para que al final tengas un sistema de inicio de sesión con Google, usuarios almacenados en Postgres y publicaciones atribuidas al usuario que inició sesión.

Por qué mantener la autenticación y los datos dentro de la aplicación Next.js

Una configuración común para un producto que comienza como una herramienta principalmente del lado del cliente es contar con un backend separado, por ejemplo Django con Postgres y un proveedor de identidades alojado como Auth0. Eso funciona, pero implica tener que desplegar, monitorear y pagar por un segundo servicio. Cuando un producto pequeño necesita añadir funcionalidades como flujos de trabajo guardados, reutilizar el servidor de Next.js para la autenticación y el acceso a la base de datos reduce los costos de alojamiento y las partes móviles del sistema. El trade-off es que ahora debes encargarte de la integración entre tres bibliotecas, y de eso trata el resto de este artículo.

Estos paquetes cambian con rapidez. Auth.js para Next.js sigue publicándose bajo una etiqueta beta, y tanto Prisma como Next.js han modificado sus valores predeterminados en las últimas versiones importantes. Considera los pasos a continuación como una guía y verifica los detalles en la documentación actual de cada proyecto.

Instalando Prisma sin los complementos alojados

La guía de Prisma para Next.js está escrita en torno a Prisma Postgres, la base de datos alojada por la propia empresa. Si planeas usar otro tipo de Postgres, puedes omitir @prisma/extension-accelerate, ya que solo es relevante para esa opción alojada.

Para una configuración básica con Postgres necesitas dos instalaciones: la CLI y tsx como dependencias de desarrollo, además del cliente y dotenv como dependencias en tiempo de ejecución. El fragmento muestra ambos comandos en una sola línea; úsalos uno tras otro:

npm install prisma tsx --save-dev npm install @prisma/client dotenv

A continuación, inicializa Prisma en el proyecto:

npx prisma init

La guía también menciona la bandera --db para init. Esa bandera proporciona una instancia de Prisma Postgres, así que óyela si utilizas tu propia base de datos.

Para hacer que una base de datos funcione localmente sin instalar Postgres, Prisma puede iniciar un servidor de desarrollo para usted:

npx prisma dev

El paso init ya ha escrito un valor de DATABASE_URL en su archivo .env que apunta a este servidor local. Si ya tiene Postgres ejecutándose localmente, o desea apuntar a una rama de Neon, reemplace ese valor por su propia cadena de conexión. Para conocer mejor cómo se comporta Prisma 7 en un proyecto Next.js 16, incluyendo el nuevo archivo de configuración, consulte nuestra guía de configuración de Prisma 7.

Creación de la aplicación de posts de ejemplo

El resto de la guía de Prisma consiste en crear un pequeño blog con usuarios y publicaciones. Si está integrando Prisma a una aplicación existente, puede saltarse directamente a la sección de autenticación. No obstante, sigue el proceso completo, ya que los pasos posteriores suponen que esos modelos ya existen.

Comience agregando los modelos a schema.prisma tal como se describe en el paso correspondiente al esquema, y luego cargue las variables de entorno en la configuración de Prisma como se muestra en el paso de dotenv.

Corrección de la ruta de importación del cliente generado

Cuando se inicializa la base de datos, el script de inicialización, tal como está escrito, importa directamente desde la carpeta prisma generada, lo cual falla. El código generado expone el cliente bajo una entrada llamada client, por lo que la primera línea de importación del script de inicialización debe apuntar allí:

import { PrismaClient, Prisma } from "../app/generated/prisma/client";

El alias @/app funciona tan bien como la ruta relativa ../app.

La misma corrección se aplica al configurar el cliente Prisma compartido en lib/prisma.ts:

import { PrismaClient } from '../app/generated/prisma/client'

La guía también alterna entre las convenciones src/app y la simple app/ de un paso a otro. Elige la que utilice realmente tu proyecto: si el directorio raíz es src/, la ruta es ../src/app/generated; si no, ../app/generated. La mayoría de los errores “módulo no encontrado” en esta etapa se deben a mezclar ambas convenciones.

Una vez instalado el cliente, agrega las páginas que describe la guía: la consulta de la página principal, la lista de publicaciones, la página de detalles de una publicación y el formulario para crear publicaciones.

Agregar Auth.js

Existen dos sitios de documentación para esta biblioteca: el más antiguo sitio de NextAuth y el más reciente referencia de Auth.js para Next.js. La documentación de Auth.js describe la versión actual y es la que debe seguirse.

Instale el paquete desde su canal beta:

npm install next-auth@beta

Luego ejecute npx auth secret. Esto genera un AUTH_SECRET aleatorio, que Auth.js utiliza para firmar y cifrar tokens y cookies, y lo escribe en .env.local. Dado que este proyecto ya guarda sus variables en .env, mueva el valor allí y elimine .env.local para tener una única fuente de configuración. Mantenga ese archivo fuera del control de versiones.

Ahora siga los pasos de instalación. La pieza central es un archivo auth.ts en la raíz del proyecto. Este archivo llama a NextAuth una vez y exporta cuatro funciones auxiliares que utiliza el resto de la aplicación: los handlers HTTP, signIn, signOut y auth, que lee la sesión actual:

import NextAuth from "next-auth"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})

El manejador de rutas ubicado en ./app/api/auth/[...nextauth]/route.ts simplemente vuelve a exportar los manejadores GET y POST para que Auth.js pueda ofrecer sus puntos finales de inicio de sesión, devolución de llamada y gestión de sesiones. En el fragmento mostrado, el comentario y la exportación están en una misma línea; en su archivo, coloque la exportación en una línea separada después de la importación:

import { handlers } from "@/auth" // Referring to the auth.ts we just created export const { GET, POST } = handlers

La guía de instalación también incluye un middleware.ts. Desde entonces, Next.js ha cambiado el nombre de middleware a proxy (consulte la nota de Next.js sobre este cambio), por lo que el archivo descrito no coincide con la versión actual de Next.js. No es necesario para esta configuración, ya que cada página y acción protegida verifica la sesión por sí misma. Si más adelante desea protección a nivel de solicitud, debe adaptarla a la convención de proxy.

Conectar Google como proveedor de inicio de sesión

En la Google Cloud Console, cree un cliente OAuth de tipo aplicación web. Establezca el origen JavaScript autorizado en localhost:3000 y la URI de redirección en localhost:3000/api/auth/callback/google, que es la ruta de callback que Auth.js expone para el proveedor Google. Al desplegar, agregue también el origen y la ruta de callback para producción. Guarde el ID del cliente y la clave secreta que le proporciona Google en su archivo de entorno bajo los nombres de variables especificados en la documentación de Auth.js, nunca en el código fuente.

Luego, la guía de OAuth agrega un componente de inicio de sesión. Se trata de un formulario cuya acción es una Acción del Servidor integrada que llama a signIn:

import { signIn } from "@/auth"

export function SignIn() {
  return (
    <form
      action={async () => {
        "use server"
        await signIn()
      }}
    >
      <button type="submit">Sign in</button>
    </form>
  )
}

Al ser llamado sin argumentos, signIn() envía al usuario a la página integrada que muestra todos los proveedores configurados. Dado que solo está configurado Google, pasar "google" omite esa pantalla intermedia.

Un componente de cierre de sesión correspondiente, extraído de los documentos de gestión de sesiones, sigue la misma estructura:

import { signOut } from "@/auth"

export function SignOut() {
  return (
    <form
      action={async () => {
        "use server"
        await signOut()
      }}
    >
      <button type="submit">Sign Out</button>
    </form>
  )
}

Colocar los botones en una barra de navegación

Los documentos no indican con claridad dónde deben colocarse estos componentes. Un lugar adecuado es una barra de navegación que sea, a su vez, un componente servidor asíncrono: este llama a auth() y muestra SignOut cuando hay un usuario conectado y SignIn en caso contrario.

import Link from "next/link";
import { auth } from "../auth"
import SignIn from "./sign-in";
import SignOut from "./sign-out";

export default async function Navigation() {
    const session = await auth();

    return (
        <>
            <nav className="sticky top-0 z-50 bg-white/80 backdrop-blur-md border-b border-gray-200">
                <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div className="flex items-center h-16">
                        {/* Logo */}
                        <Link href="/" className="flex items-center space-x-2">
                        <span className="text-xl font-semibold text-[#369b6f]">
                            Prisma + NextAuth + Neon
                        </span>
                        </Link>

                        {/* Spacer */}
                        <div className="flex-1"></div>

                        {/* Navigation Links */}
                        <div className="hidden md:flex items-center mr-8">
                        <Link
                            href="/posts"
                            className="text-gray-700 font-medium hover:text-[#369b6f] transition-all duration-200"
                        >
                            Posts
                        </Link>
                        </div>

                        {/* Auth Buttons */}
                        <div className="flex items-center space-x-2 sm:space-x-4">
                        {session?.user ? (
                            <SignOut/>
                        ) : (
                            <SignIn/>
                        )}
                        </div>
                    </div>
                </div>
            </nav>
        </>
    );
}

Observe las importaciones aquí: este archivo utiliza importaciones predeterminadas para SignIn y SignOut, mientras que los componentes anteriores utilizan exportaciones nombradas. Cambie las importaciones a import { SignIn } from "./sign-in" o exporte los componentes como predeterminados; de lo contrario, la compilación fallará.

Luego, muestre la navegación en el layout raíz para que aparezca en todas las páginas:

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
          <Navigation/>
          <main>
            {children}
          </main>
      </body>
    </html>
  );
}

Recuerde importar Navigation al principio de app/layout.tsx. Una vez que se recargue el servidor de desarrollo, aparecerá una barra con un botón de inicio de sesión o cierre de sesión encima de cada página. Tenga en cuenta que ambos botones dependen de acciones del servidor definidas directamente, por lo que solo funcionan dentro de los componentes del servidor. Un componente del cliente no puede definir funciones del servidor de forma directa; en ese caso, deberá mover esas acciones a un archivo separado marcado con "use server" e importarlas.

Persistencia de usuarios con el adaptador Prisma

Por ahora el inicio de sesión funciona, pero no se almacena ninguna información del usuario en su base de datos. El adaptador Prisma conecta Auth.js con su cliente Prisma para que los usuarios, las cuentas OAuth vinculadas y las sesiones se guarden en Postgres. Instálelo:

npm install @auth/prisma-adapter

Luego, actualice auth.ts para pasar el adaptador de su cliente Prisma compartido y para registrar al proveedor de Google:

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import { PrismaAdapter } from "@auth/prisma-adapter";
import prisma from "@/lib/prisma";

export const { handlers, signIn, signOut, auth } = NextAuth({
    adapter: PrismaAdapter(prisma),
    providers: [Google],
})

Con un adaptador configurado, Auth.js almacena las sesiones en la base de datos por defecto en lugar de en una cookie firmada, lo que le permite revocar sesiones del lado del servidor a costa de una búsqueda en la base de datos por cada verificación de sesión. El proveedor de Google obtiene automáticamente sus credenciales desde las variables de entorno cuando siguen la convención de nombres de Auth.js.

Armonización del modelo de usuario

El adaptador necesita tablas y columnas específicas, que se enumeran en el esquema del adaptador Prisma. Copíelas a schema.prisma. Esto genera conflictos con la aplicación de ejemplo: la guía de Prisma creó un modelo User con su propio tipo de id, mientras que Auth.js espera un id de tipo cadena además de varios campos adicionales. Para un proyecto de ejemplo nuevo, el camino más sencillo es reemplazar el modelo y reconstruir la base de datos desde cero. Si está integrándolo en una aplicación que ya tiene usuarios reales, no restablezca nada; planifique una migración adecuada que asocie a los usuarios existentes con la nueva estructura. Eso es un problema separado que está fuera del alcance de esta guía.

Para reconstruirlo correctamente, primero detenga la ejecución del proceso de inicialización, ya que los datos de inicialización se refieren a la antigua estructura de usuarios. Elimine la entrada correspondiente en prisma.config.ts:

seed: `tsx prisma/seed.ts`

Luego se reinicia la base de datos, lo que elimina todas las tablas y los datos:

npx prisma migrate reset

Solo ejecute esto en una base de datos de desarrollo. Finalmente, cree una migración para el nuevo esquema y aplíquela:

npx prisma migrate dev

Inicie la aplicación nuevamente (npm run dev) e inicie sesión mediante Google. Debería aparecer una nueva fila en la tabla User, con una fila relacionada en Account para el enlace de Google. Para inspeccionar las filas, ejecute npx prisma studio, el navegador visual de bases de datos que viene incluido con Prisma.

Atribuir publicaciones al usuario conectado

El formulario de ejemplo del guía de Prisma crea publicaciones con un authorId predefinido. Ahora que existen sesiones, la página puede leer la sesión y utilizar el verdadero ID del usuario en su lugar. La página a continuación llama a auth(), muestra un mensaje a los visitantes anónimos y pasa session.user.id a Prisma al crear la publicación. Luego vuelve a validar la lista de publicaciones y redirige al usuario hacia ella:

export default async function NewPost() {
  const session = await auth();

  async function createPost(formData: FormData) {
    "use server";

    if(!session?.user?.id){
      return;
    }

    const title = formData.get("title") as string;
    const content = formData.get("content") as string;

    await prisma.post.create({
      data: {
        title,
        content,
        authorId: session.user.id,
      },
    });

    revalidatePath("/posts");
    redirect("/posts");
  }

  if(!session?.user){
    return <div>You need to sign in to create a post</div>
  }

  ...

}

Hay un detalle que merece ser corregido antes de que esto se utilice en entornos de producción. La acción lee session desde la renderización de la página a través de un cierre. Una acción del servidor es un endpoint público que puede ser invocado directamente, por lo que el patrón más seguro es llamar a auth() dentro del cuerpo de la acción y verificar el resultado allí, de modo que la verificación se ejecute en cada envío. Explicamos por qué en por qué las acciones del servidor necesitan autorización dentro de cada cuerpo de función. También es una buena idea validar title y content en lugar de convertirlos con as string.

Conclusión

  • Sigue las guías oficiales de Prisma y Auth.js, pero corrige la importación del cliente generado para que apunte a generated/prisma/client y mantén la consistencia entre src/app y app/.
  • Mantén todos los secretos y cadenas de conexión en un archivo de entorno que nunca se suba a control de versiones.
  • Omite el middleware.ts documentado; Next.js actual utiliza la convención de proxy, y las comprobaciones auth() a nivel de página son suficientes aquí.
  • Agregar el adaptador de Prisma cambia la estructura requerida para User. Restablecerla está bien para una demostración, pero los datos existentes necesitan una migración real.
  • Verifica la sesión dentro de cada acción del servidor, no solo en la página que muestra el formulario.

A partir de aquí, los siguientes pasos lógicos son el inicio de sesión desde componentes del cliente y dirigir DATABASE_URL hacia una instancia de Postgres alojada para el despliegue.

Lecturas relacionadas