En los últimos años, añadir autenticación a tu aplicación ha pasado de ser algo oscuro y complicado a algo para lo que literalmente puedes utilizar una API.

No faltan repositorios de ejemplos y tutoriales sobre cómo implementar esquemas de autenticación específicos en Next.js, pero hay menos sobre el porqué de los esquemas, herramientas y los compromisos que esto implica.

Este artículo tratará sobre lo que hay que tener en cuenta al abordar la autenticación en Next.js, desde la elección de un proveedor hasta la creación de rutas para el inicio de sesión y la elección entre el lado del servidor y el lado del cliente.

Elegir un Método / Proveedor de Autenticación

Básicamente, hay 1.000 formas de incorporar la autenticación a tu aplicación. En lugar de centrarnos en proveedores concretos (tema para otra entrada del blog), veamos los tipos de soluciones de autenticación y algunos ejemplos de cada una. En términos de implementación, next-auth se está convirtiendo rápidamente en una opción popular para integrar tu aplicación Next.js con múltiples proveedores, añadir SSO, etc.

Base de Datos Tradicional

Es de lo más sencillo: almacenas nombres de usuario y contraseñas en una base de datos relacional. Cuando un usuario se registra por primera vez, insertas una nueva fila en la tabla «usuarios» con la información proporcionada. Cuando se conectan, compruebas las credenciales con lo que tienes almacenado en la tabla. Cuando un usuario quiere cambiar su contraseña, actualizas el valor en la tabla.

La autenticación tradicional de base de datos es, sin duda, el esquema de autenticación más popular si observas la totalidad de las aplicaciones existentes, y ha existido básicamente desde siempre. Es muy flexible, barato y no te ata a ningún proveedor en particular. Pero tienes que construirlo tú mismo y, en particular, tienes que preocuparte de la encriptación y de asegurarte de que esas valiosas contraseñas no caigan en manos equivocadas.

Soluciones de Autenticación de tu Proveedor de Bases de Datos

En los últimos años (y en honor a Firebase, desde hace ya bastantes años), se ha convertido en algo relativamente habitual que los proveedores de bases de datos administradas ofrezcan algún tipo de solución de autenticación administrada. Firebase, Supabase y AWS ofrecen tanto bases de datos administradas como autenticación administrada como servicio a través de un conjunto de API que abstrae fácilmente la creación de usuarios y la gestión de sesiones (más sobre esto más adelante).

Iniciar la sesión de un usuario con la autenticación Supabase es tan sencillo como:

async function signInWithEmail() {
  const { data, error } = await supabase.auth.signInWithPassword({
    email: '[email protected]',
    password: 'example-password',
  })
}

Soluciones de Autenticación que No Provienen de tu Proveedor de Base de Datos

Posiblemente aún más común que la autenticación como servicio de tu DBaaS sea la autenticación como servicio de toda una empresa o producto. Auth0 existe desde 2013 (ahora es propiedad de Okta), y las últimas incorporaciones, como Stytch, han dado prioridad a la experiencia del desarrollador y se han ganado cierto prestigio.

La interfaz de usuario que obtienes al utilizar Auth0 para la autenticación
Auth0 para la autenticación

Inicio de Sesión Único

El SSO (siglas en inglés de Inicio de Sesión Único, Single Sign On) te permite «externalizar» tu identidad a un proveedor externo, que puede ir desde un proveedor empresarial centrado en la seguridad, como Okta, hasta algo más ampliamente adoptado, como Google o GitHub. El SSO de Google es omnipresente en el mundo SaaS, y algunas herramientas centradas en el desarrollador sólo autentican a través de GitHub.

Sea cual sea el proveedor que elijas, el SSO suele ser un add-on de los otros tipos de autenticación anteriores y conlleva su propia idiosincrasia en cuanto a la integración con plataformas externas (atención: SAML utiliza XML).

Vale, ¿Cuál es Para Mí?

Aquí no hay una elección «correcta» — lo adecuado para tu proyecto depende de tus prioridades. Si quieres poner las cosas en marcha rápidamente sin una gran cantidad de configuración inicial, subcontratar la autenticación tiene sentido (incluso subcontratarla por completo, incluida la interfaz de usuario, a alguien como Auth0). Si prevés una configuración más compleja, construir tu propio backend de autenticación tiene sentido. Y si prevés dar soporte a clientes más grandes, necesitarás añadir SSO en algún momento.

Next.js es tan popular en este momento que la mayoría de estos proveedores de autenticación tienen documentación y guías de integración específicas para Next.js.

Creación de Rutas para el Registro y el Inicio de Sesión, y Consejos para Ir un Paso Más Allá en la Autenticación

Algunos proveedores de autenticación, como Auth0, proporcionan páginas web alojadas completas para el registro y el inicio de sesión. Pero si las creas desde cero, creo que es útil crearlas al principio del proceso, ya que las necesitarás como redirecciones cuando implementes tu autenticación.

Así que tiene sentido crear la estructura de estas páginas y luego añadir las peticiones al backend. La forma más sencilla de implementar la autenticación es tener estas dos rutas:

  • Una para registrarse
  • Otra para iniciar sesión una vez que el usuario ya tiene una cuenta

Más allá de lo básico, tendrás que cubrir casos extremos, como cuando un usuario olvida su contraseña. Algunos equipos prefieren tener el proceso de restablecimiento de contraseña en una ruta separada, mientras que otros añaden elementos dinámicos de interfaz de usuario a la página de inicio de sesión normal.

Puede que una página de registro atractiva no suponga la diferencia entre el éxito y el fracaso, pero los pequeños detalles pueden dejar una buena impresión y, en general, proporcionar una mejor Experiencia de Usuario. Aquí tienes una recopilación de sitios web que han puesto un poco más de cariño en sus procesos de autenticación.

1. Actualiza tu Barra de Navegación Si Hay una Sesión Activa

La llamada a la acción de Stripe en su barra de navegación cambia en función de si tienes una sesión autenticada o no. Este es el aspecto del sitio de marketing si no estás autenticado. Observa la llamada a la acción para iniciar sesión:

Página de Inicio de Stripe
La página de inicio de Stripe cambia la CTA en función de si estás autenticado

Y este es el aspecto si estás autenticado. Observa que la llamada a la acción cambia para llevar al usuario a su panel de control en lugar de iniciar sesión:

Cambios en la página de inicio de Stripe
Cambios en la página de inicio de Stripe

No cambia fundamentalmente mi experiencia con Stripe, pero es agradable.

Un apunte técnico interesante: hay una buena razón por la que la mayoría de las empresas no hacen que la barra de navegación de su sitio de marketing «dependa» de la autenticación — supondría una solicitud adicional a la API para comprobar el estado de autenticación en cada carga de página, que en la mayoría de los casos corresponde a visitantes que probablemente no lo están.

2. Añade Contenido Útil Junto al Formulario de Registro

En los últimos años, especialmente en SaaS, las empresas han empezado a añadir contenido a la página de registro para «animar» al usuario a completar el registro. Esto puede ayudar a mejorar tu conversión en la página, al menos incrementalmente.

Aquí tienes una página de registro de Retool, con una animación y algunos logotipos en el lateral:

Página de registro de Retool
Si vas a hacer esto, procura que las fuentes de cada lado coincidan.

También lo hacemos en Kinsta para nuestra página de registro:

Página de registro de Kinsta
Página de registro de Kinsta

El pequeño contenido extra puede ayudar a recordar al usuario para qué se está registrando y por qué lo necesita.

3. Si Utilizas una Contraseña: Sugiere o Exige una Contraseña Fuerte

Me siento cómodo diciendo que es de conocimiento común entre los desarrolladores que las contraseñas son intrínsecamente inseguras, pero no es de conocimiento común entre todas las personas que se registrarán en tu producto. Animar a tus usuarios a crear contraseñas seguras es bueno para ti y para ellos.

Coinbase es bastante estricta con el registro y exige que utilices una contraseña segura que sea más complicada que tu nombre de pila:

Crear cuenta Coinbase
Contraseña débil en Coinbase

Después de generar una con mi gestor de contraseñas, pude empezar:

Contraseña segura en Coinbase
Contraseña segura en Coinbase

La Interfaz de Usuario no me decía por qué la contraseña no era lo suficientemente segura, ni ningún requisito más allá de tener un número. Si los incluyes en el texto del producto, facilitarás las cosas al usuario y evitarás la frustración de tener que volver a intentar introducir la contraseña.

4. Etiqueta Tus Entradas para que Funcionen Bien con un Gestor de Contraseñas

Uno de cada tres estadounidenses utiliza un gestor de contraseñas como 1Password y, sin embargo, muchos formularios web siguen ignorando el «type=» en las entradas HTML. Haz que tus formularios sean compatibles con los gestores de contraseñas:

  • Incluye tus elementos de entrada en un elemento de formulario
  • Asigna un tipo y una etiqueta a las entradas
  • Añade funciones de autocompletar a tus entradas
  • No añadas campos dinámicamente (te estoy mirando a ti, Delta)

Puede marcar la diferencia entre un inicio de sesión de 10 segundos increíblemente fluido y uno manual molesto, especialmente en móviles.

Elegir Entre Sesiones y JWT

Una vez que tu usuario se autentique, tienes que elegir una estrategia para mantener ese estado a lo largo de las siguientes solicitudes. HTTP no tiene estado, y desde luego no queremos pedir a nuestro usuario su contraseña en cada solicitud. Hay dos métodos populares para manejar estosesiones (o cookies) y JWTs (tokens web JSON) — y difieren en términos de si el servidor o el cliente hace el trabajo.

Sesiones, También Conocidas como Cookies

En la autenticación basada en sesiones, la lógica y el trabajo para mantener la autenticación son gestionados por el servidor. Éste es el flujo básico:

  1. El usuario se autentica a través de la página de inicio de sesión.
  2. El servidor crea un registro que representa esta «sesión» de navegación concreta. Normalmente se inserta en una base de datos con un identificador aleatorio y detalles sobre la sesión, como cuándo comenzó y cuándo expira.
  3. Ese identificador aleatorio — algo así como «6982e583b1874abf9078e1d1dd5442f1» — se envía a tu navegador y se almacena como cookie.
  4. En posteriores peticiones del cliente, se incluye el identificador y se comprueba en la tabla de sesiones de la base de datos.

Es bastante sencillo y ajustable en cuanto a la duración de las sesiones, cuándo deben revocarse, etc. La desventaja es la latencia a una escala significativa, dadas todas las escrituras y lecturas en la base de datos, pero eso puede no ser una consideración importante para la mayoría de los lectores.

Tokens Web JSON (JWT)

En lugar de gestionar la autenticación de las solicitudes posteriores en el servidor, JWTs te permiten gestionarlas (en su mayor parte) en el lado del cliente. Funciona así

  1. El usuario se autentica a través de la página de inicio de sesión.
  2. El servidor genera un JWT que contiene la identidad del usuario, los permisos que tiene concedidos y una fecha de caducidad (entre otras posibles cosas).
  3. El servidor firma ese token, cifra criptográficamente su contenido y lo envía entero al cliente.
  4. Para cada solicitud, el cliente puede descifrar el token y verificar que el usuario tiene permiso para hacer la solicitud (todo ello sin necesidad de comunicarse con el servidor).

Con todo el trabajo posterior a la autenticación inicial descargado en el cliente, tu aplicación puede cargarse y funcionar mucho más rápido. Pero hay un problema principal: no hay forma de invalidar un JWT desde el servidor. Si el usuario quiere desconectarse de un dispositivo o cambia el alcance de su autorización, tienes que esperar a que caduque el JWT.

Elegir entre la Autenticación en el Lado del Servidor y en el Lado del Cliente

Parte de lo que hace grande a Next.js es el renderizado estático incorporado — si tu página es estática, es decir, no necesita hacer ninguna llamada a una API externa, Next.js la almacena automáticamente en caché y puede servirla extremadamente rápido a través de una CDN. La versión anterior a Next.js 13 sabe si una página es estática si no incluye ningún `getServerSideProps` o `getInitialProps` en el archivo, mientras que las versiones posteriores a Next.js 13 utilizan Componentes de Servidor React para hacerlo.

Para la autenticación, puedes elegir entre: renderizar una página estática de «carga» y hacer la obtención en el lado del cliente o hacerlo todo en el lado del servidor. Para las páginas que requieren autenticación[1], puedes renderizar un «esqueleto» estático y luego hacer las peticiones de autenticación en el lado del cliente. En teoría, esto significa que la página se carga más rápido, aunque el contenido inicial no esté totalmente listo.

Aquí tienes un ejemplo simplificado de la documentación que muestra un estado de carga mientras el objeto usuario no esté listo:

import useUser from '../lib/useUser'
 
const Profile = () => {
  // Fetch the user client-side
  const { user } = useUser({ redirectTo: '/login' })
 
  // Server-render loading state
  if (!user || user.isLoggedIn === false) {
    // Build some sort of loading page here
    return <div>Loading...</div>
  }
 
  // Once the user request finishes, show the user
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

Ten en cuenta que aquí necesitarías construir algún tipo de interfaz de usuario de carga para mantener el espacio mientras el cliente realiza peticiones tras la carga.

Si quieres simplificar las cosas y ejecutar la autenticación en el lado del servidor, puedes añadir tu solicitud de autenticación en la función `getServerSideProps`, y Next esperará a renderizar la página hasta que se complete la solicitud. En lugar de la lógica condicional del fragmento anterior, ejecutarías algo más sencillo como esta versión simplificada de los documentos de Next:

import withSession from '../lib/session'
 
export const getServerSideProps = withSession(async function ({ req, res }) {
  const { user } = req.session
 
  if (!user) {
    return {
      redirect: {
        destination: '/login',
        permanent: false,
      },
    }
  }
 
  return {
    props: { user },
  }
})
 
const Profile = ({ user }) => {
  // Show the user. No loading state is required
  return (
    <div>
      <h1>Your Account</h1>
      <p>Username: {JSON.stringify(user.username,null)}</p>
      <p>Email: {JSON.stringify(user.email,null)}</p>
      <p>Address: {JSON.stringify(user.address,null)}</p>
    </div>
  )
}
 
export default Profile

Aquí sigue habiendo lógica para gestionar cuando falla la autenticación, pero redirige al inicio de sesión en lugar de mostrar un estado de carga.

Resumen

Entonces, ¿cuál de estas opciones es la adecuada para tu proyecto? Empieza por evaluar hasta qué punto confías en la velocidad de tu esquema de autenticación. Si tus peticiones no tardan nada, puedes ejecutarlas en el lado del servidor y evitar el estado de carga. Si quieres dar prioridad a renderizar algo de inmediato y luego esperar a la solicitud, omite `getServerSideProps` y ejecuta la autenticación en otro lugar.

[1] Cuando utilices Next, ésta es una buena razón para no exigir la autenticación en todas y cada una de las páginas. Es más sencillo hacerlo, pero significa que, en primer lugar, pierdes las ventajas de rendimiento del framework web.

Justin Gage

Justin is a technical writer and author of the popular Technically newsletter. He did his B.S. in Data Science before a stint in full-stack engineering and now focuses on making complex technical concepts accessible to everyone.