12k
All articles

Cómo agregar autenticación a un sitio Astro

Configura autenticación en Astro con adapters, middleware, Astro.locals, sessions, rewrite y Actions, incluyendo login, logout y protección de rutas.

OpenReplay Team
OpenReplay Team
Cómo agregar autenticación a un sitio Astro

La autenticación en Astro depende tanto de cómo se renderiza una página como de la librería que elijas. Por defecto, Astro prerrenderiza las páginas de tu proyecto en tiempo de compilación, y tú excluyes rutas individuales de ese comportamiento. Una página prerrenderizada se genera antes de que exista cualquier visitante, por lo que no hay request, ni encabezado de cookie, ni sesión que leer.

Un primer intento suele fallar de forma silenciosa por este motivo. Instalas una librería de autenticación, pegas su middleware en src/middleware.ts, cargas /account y Astro.locals es un objeto vacío. Sin error, sin log, sin stack trace que buscar.

Este artículo recorre toda la cadena en orden, archivo por archivo: el adaptador, la exportación prerender por ruta, el middleware que resuelve el usuario, la página que lo lee, la protección de rutas con rewrite(), el inicio y cierre de sesión como Astro Actions, y el límite donde las islas del lado del cliente dejan de ver todo esto. El código apunta a Astro 7.x. Astro 6 elevó el mínimo de Node a 22 y eliminó el soporte para Node 18 y 20, así que ejecuta Node 22.12.0 o posterior.

Puntos clave

  • Astro prerrenderiza todo el proyecto por defecto, así que cualquier página que lea una sesión debe excluirse con export const prerender = false, y el renderizado on demand requiere un adaptador.
  • Si tu middleware parece no hacer nada, la ruta que debería proteger casi con seguridad sigue estando prerrenderizada.
  • Astro.locals vive exactamente durante el renderizado de una ruta; los datos que deben sobrevivir hasta el siguiente request pertenecen a Astro.session, que requiere un driver de sesión.
  • Las Astro Actions son endpoints HTTP accesibles públicamente, por lo que cada handler necesita su propia verificación de context.locals; proteger la página que renderiza el formulario no protege la action que hay detrás.
  • Astro.locals y Astro.session existen únicamente en el servidor, así que un componente con una directiva client:* solo puede ver lo que la página le pasó como props.

¿Por qué el comportamiento estático por defecto rompe la autenticación en Astro?

Un proyecto de Astro se compila a HTML estático a menos que una ruta pida algo distinto, y el HTML estático se produce una sola vez, en tiempo de compilación, para todos los visitantes. Prerrenderizar todo el proyecto es el comportamiento documentado por defecto, tal como lo expone la guía de renderizado on demand. Una sesión vive en un encabezado de request que aún no existe cuando ese HTML se escribe en disco, así que las lecturas de cookies, Astro.request y cualquier cosa que un middleware ponga en locals no tienen a qué adherirse.

La consecuencia práctica: un middleware que parece nunca ejecutarse es un síntoma del modo de renderizado, no un bug de la librería de autenticación.

¿Cómo se habilita el renderizado on demand?

El renderizado on demand necesita dos cosas: un adaptador, que produce un servidor para tu runtime de destino, y una exclusión por ruta. Los adaptadores oficiales de Astro son @astrojs/cloudflare, @astrojs/netlify, @astrojs/node y @astrojs/vercel, junto con adaptadores de la comunidad, y la opción adapter está documentada en la referencia de configuración. Instala uno con npx astro add node y revisa la página propia de ese adaptador, ya que las opciones de configuración difieren.

Después elige una forma. La opción output acepta exactamente dos valores: 'static' y 'server'.

Forma del sitioastro.config.mjsFrontmatter de la página
Sitio de contenido con unas pocas páginas con sesiónadaptador instalado, mantén el valor por defecto output: 'static'export const prerender = false en cada ruta que lea una sesión
Aplicación mayoritariamente con sesiónadaptador instalado, define output: 'server'export const prerender = true en las rutas de marketing y contenido

Cualquiera que elijas, la regla es la misma: la ruta que lee la sesión debe renderizarse on demand, y la documentación del adaptador Node cubre los detalles de runtime para el ejemplo de aquí.

El middleware: src/middleware.ts

El middleware resuelve el usuario una vez por request y lo entrega a todo lo que está aguas abajo. Coloca el archivo en src/middleware.js|ts, o en src/middleware/index.js|ts si prefieres una carpeta, y dale una exportación nombrada llamada onRequest. Una exportación por defecto no será detectada, una regla sobre la que la guía de middleware es explícita.

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

export const onRequest = defineMiddleware(async (context, next) => {
  const stored = (await context.session?.get('user')) ?? null;
  context.locals.user = stored as User | null;
  return next();
});

Esto lee la sesión integrada de Astro en lugar de parsear una cookie a mano. La misma sesión aparece bajo dos nombres, como describe la guía de sesiones de Astro: las páginas y componentes la alcanzan a través de Astro.session, mientras que el middleware, los endpoints de API y los handlers de actions la obtienen desde context.session. El almacenamiento no es automático. Tres adaptadores eligen un driver por defecto para ti —Node, Cloudflare y Netlify—; con cualquier otro adaptador debes nombrarlo tú mismo, siguiendo la referencia de drivers de sesión. Una restricción que conviene conocer antes de desplegar a un runtime edge: las sesiones no funcionan en middleware edge.

Tipa locals ampliando el namespace App en src/env.d.ts:

// src/env.d.ts
type User = {
  id: string;
  email: string;
};

declare namespace App {
  interface Locals {
    user: User | null;
  }
}

Leer el usuario con Astro.locals

Una página lee lo que el middleware le asignó, siempre que esa página se renderice on demand. Astro.locals vive exactamente durante el renderizado de una ruta, así que es el lugar correcto para transportar un objeto de usuario desde el middleware hasta una página, y el lugar equivocado para guardar cualquier cosa que deba sobrevivir hasta el siguiente request.

---
// src/pages/account.astro
/* On-demand rendering */ export const prerender = false;

const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
---
<h1>Signed in as {user.email}</h1>

Elimina la primera línea y la página vuelve al conjunto prerrenderizado: se compila a HTML estático, Astro.locals.user nunca se llena y todos los visitantes reciben el mismo archivo. Esa única línea es la diferencia entre una autenticación que funciona y una pantalla de login que nadie puede superar.

¿Cómo se protegen rutas con context.rewrite()?

Protege las rutas centralizando el control en el middleware y renderizando la página de login en el mismo lugar en vez de redirigir a ella. context.rewrite('/login') muestra contenido distinto en la URL que pidió el visitante, lo que mantiene la ruta protegida en la barra de direcciones.

// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';

const protectedPaths = ['/account', '/dashboard'];

export const onRequest = defineMiddleware(async (context, next) => {
  const stored = (await context.session?.get('user')) ?? null;
  context.locals.user = stored as User | null;

  const needsAuth = protectedPaths.some((path) =>
    context.url.pathname.startsWith(path),
  );

  if (needsAuth && !context.locals.user) {
    return context.rewrite('/login');
  }

  return next();
});

Como el rewrite reinicia el renderizado, tu middleware se ejecuta una segunda vez en el trayecto, así que mantén /login fuera de protectedPaths o esa segunda pasada fallará la misma verificación y entrará en bucle. Una autenticación rota de este tipo rara vez lanza una excepción: la reproducción de sesión de un flujo de login muestra al usuario rebotando entre la página de login y la ruta protegida, que es exactamente cómo se ve desde fuera una cookie de sesión con alcance mal configurado o un control colocado en la rama equivocada.

Evita la otra forma de rewrite aquí. Cuando le pasas a next() un Request, Astro construye un request de reemplazo a partir del anterior, y cualquier intento de leer el body después de ese punto (o antes de él) lanza un error en tiempo de ejecución. Eso golpea con más fuerza cuando una Action se dispara desde un formulario HTML, y es la razón por la que la documentación te orienta hacia context.rewrite() o Astro.rewrite().

Inicio y cierre de sesión con Astro Actions

Las Astro Actions, añadidas en astro@4.15, son la forma integrada de manejar el inicio y cierre de sesión, y sustituyen a las rutas de API hechas a mano. Defínelas en un objeto server exportado desde src/actions/index.ts, establece accept: 'form' y envíales datos desde HTML plano.

// src/actions/index.ts
import { ActionError, defineAction } from 'astro:actions';
import { z } from 'astro/zod';

// Replace with your own credential lookup.
async function verifyCredentials(email: string, password: string): Promise<User | null> {
  return null;
}

export const server = {
  login: defineAction({
    accept: 'form',
    input: z.object({
      email: z.email({ error: 'Enter a valid email address.' }),
      password: z.string(),
    }),
    handler: async ({ email, password }, context) => {
      const user = await verifyCredentials(email, password);
      if (!user) throw new ActionError({ code: 'UNAUTHORIZED' });

      await context.session?.regenerate();
      await context.session?.set('user', user);
      return { ok: true };
    },
  }),

  logout: defineAction({
    accept: 'form',
    handler: async (_input, context) => {
      await context.session?.destroy();
      return { ok: true };
    },
  }),

  deleteAccount: defineAction({
    accept: 'form',
    handler: async (_input, context) => {
      if (!context.locals.user) throw new ActionError({ code: 'UNAUTHORIZED' });
      return { ok: true };
    },
  }),
};

z proviene de astro/zod, que reexporta Zod v4, por lo que validadores de nivel superior como z.email() y la clave error para mensajes personalizados son la escritura vigente. Regenerar el ID de sesión al iniciar sesión protege contra la fijación de sesión, y destroy() limpia la cookie y descarta los datos guardados en el servidor.

Fíjate en deleteAccount. Las actions son endpoints accesibles públicamente con sus propias URLs, así que cualquiera puede invocar una directamente sin haber cargado nunca la página que renderiza su formulario. Todo handler que toque datos de usuario verifica context.locals por sí mismo.

El lado de la página es un formulario y una lectura del resultado:

---
// src/pages/login.astro
/* On-demand rendering */ export const prerender = false;
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.login);
if (result && !result.error) return Astro.redirect('/account');
---
{result?.error && <p class="error">Those details did not match.</p>}

<form method="POST" action={actions.login}>
  <input type="email" name="email" required />
  <input type="password" name="password" required />
  <button>Log in</button>
</form>

El cierre de sesión tiene la misma forma: <form method="POST" action={actions.logout}>, sin necesidad de JavaScript del lado del cliente.

La trampa de las islas: las islas nunca ven locals

Astro.locals y Astro.session existen únicamente en el servidor. El middleware, las páginas y layouts .astro, las rutas de API y los handlers de actions se ejecutan todos en el servidor y los comparten. Un componente que lleva una directiva client:* se hidrata en el navegador, queda fuera de esa cadena y solo ve lo que la página le pasó como props.

---
// Renders logged-out UI forever. The island cannot reach locals.
import UserMenu from '../components/UserMenu.jsx';
---
<UserMenu client:load />
---
// Correct: the page reads locals on the server and passes the value down.
import UserMenu from '../components/UserMenu.jsx';
const user = Astro.locals.user;
---
<UserMenu client:load user={user} />

En la versión defectuosa no se lanza ningún error. La página renderiza contenido de usuario autenticado mientras la isla que está a su lado renderiza un botón de inicio de sesión, una discrepancia que solo se manifiesta visualmente.

Este ejemplo usa las sesiones integradas de Astro, y la misma secuencia aplica para una librería: la guía de autenticación de Astro señala librerías de autenticación como Better Auth y Clerk para inicio de sesión por email y OAuth, y el diseño agnóstico al framework de Better Auth se extiende a Astro, como explica nuestra descripción general de BetterAuth. Cualquiera que elijas, sigue necesitando un adaptador, una ruta no prerrenderizada y un middleware que escriba en context.locals.

Conclusión

La autenticación en Astro es una cadena con un eslabón débil al principio: sin adaptador y sin prerender = false no hay request, y todo lo que está aguas abajo silenciosamente no hace nada. Acierta primero con el modo de renderizado, después con el middleware y luego con las verificaciones por handler en tus actions. Abre la guía de renderizado on demand junto a tu astro.config.mjs, instala un adaptador y añade export const prerender = false a la primera página que necesite un usuario.

Preguntas frecuentes

¿Funcionan las Astro Actions en una página prerrenderizada?

No. Una página debe renderizarse on demand para invocar una action mediante un form action, así que añade 'export const prerender = false' a la página que contiene el formulario e instala un adaptador para que exista un servidor que ejecute el handler. Los bodies de los requests de actions también tienen un límite por defecto de 1 MB (1048576 bytes); aumenta security.actionBodySizeLimit si un handler debe aceptar algo mayor, como una subida de archivos.

¿Puede una página estática prerrenderizada mostrar UI de sesión iniciada o cerrada?

No en el servidor. Una página prerrenderizada se escribe en disco en tiempo de compilación y todos los visitantes reciben el archivo idéntico, así que no hay encabezado de cookie sobre el que ramificar. Funcionan dos soluciones: excluir esa ruta del prerrenderizado con 'export const prerender = false', o mantener la página estática y obtener el usuario en el navegador desde un endpoint on demand, pasando el resultado a un componente cliente.

¿Astro protege los formularios de login contra CSRF automáticamente?

En parte. En páginas renderizadas on demand, Astro compara el encabezado origin que envía el navegador con la URL a la que fue el request, y responde con un 403 cuando ambos no coinciden. Ese comportamiento está activado por defecto desde Astro 5, a través de la opción security.checkOrigin, y cubre únicamente los envíos de formularios entre sitios. Aun así debes regenerar el ID de sesión al iniciar sesión, y debes autorizar cada handler de action y cada ruta de API individualmente. Establecer security.checkOrigin en false desactiva la verificación.

¿Debo usar context.rewrite o Astro.redirect para enviar a los usuarios sin sesión a la página de login?

Usa context.rewrite en el middleware cuando quieras servir el contenido del login en la URL que solicitó el visitante, ya que mantiene la ruta protegida en la barra de direcciones y evita un segundo viaje de ida y vuelta del navegador. Astro.redirect devuelve una respuesta de redirección y el navegador navega a /login. Un rewrite inicia un renderizado nuevo y tu middleware se ejecuta otra vez, así que excluye /login de tus rutas protegidas o la misma verificación seguirá fallando.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.