12k
All articles

Comment ajouter l'authentification à un site Astro

Configurez l’authentification Astro avec adapter, middleware, Astro.locals, sessions, rewrite et Actions, pour login, logout et protection des routes.

OpenReplay Team
OpenReplay Team
Comment ajouter l'authentification à un site Astro

L’authentification dans Astro dépend autant de la manière dont une page est rendue que de la bibliothèque que vous choisissez. Par défaut, Astro prérend les pages de votre projet au moment du build, et vous devez exclure individuellement certaines routes de ce comportement. Une page prérendue est générée avant même qu’un visiteur n’existe : il n’y a donc ni requête, ni en-tête de cookie, ni session à lire.

Une première tentative échoue souvent en silence pour cette raison. Vous installez une bibliothèque d’authentification, collez son middleware dans src/middleware.ts, chargez /account, et Astro.locals est un objet vide. Aucune erreur, aucun log, aucune stack trace à rechercher.

Cet article parcourt toute la chaîne dans l’ordre, fichier par fichier : l’adaptateur, l’export prerender par route, le middleware qui résout l’utilisateur, la page qui le lit, la protection des routes avec rewrite(), la connexion et la déconnexion sous forme d’Astro Actions, et la frontière où les îlots côté client cessent de voir tout cela. Le code cible Astro 7.x. Astro 6 a relevé le socle Node à la version 22 et abandonné la prise en charge de Node 18 et 20 : utilisez donc Node 22.12.0 ou une version ultérieure.

Points clés à retenir

  • Astro prérend l’intégralité du projet par défaut : toute page qui lit une session doit s’en exclure avec export const prerender = false, et le rendu à la demande nécessite un adaptateur.
  • Si votre middleware semble ne rien faire, la route qu’il devrait protéger est presque certainement encore prérendue.
  • Astro.locals ne vit que le temps d’un seul rendu de route ; les données qui doivent survivre jusqu’à la requête suivante relèvent d’Astro.session, qui nécessite un pilote de session.
  • Les Astro Actions sont des endpoints HTTP publiquement accessibles : chaque handler a donc besoin de sa propre vérification de context.locals ; protéger la page qui affiche le formulaire ne protège pas l’action qui se trouve derrière.
  • Astro.locals et Astro.session sont exclusivement côté serveur : un composant portant une directive client:* ne peut voir que ce que la page lui a transmis en props.

Pourquoi le statique par défaut casse-t-il l’authentification Astro ?

Un projet Astro se compile en HTML statique à moins qu’une route ne demande autre chose, et ce HTML statique est produit une seule fois, au moment du build, pour tous les visiteurs. Le prérendu de l’ensemble du projet est le comportement par défaut documenté, comme l’expose le guide du rendu à la demande. Une session vit dans un en-tête de requête qui n’existe pas encore lorsque ce HTML est écrit sur le disque : les lectures de cookies, Astro.request et tout ce qu’un middleware place dans locals n’ont donc rien à quoi se rattacher.

Conséquence pratique : un middleware qui ne semble jamais s’exécuter est le symptôme d’un mode de rendu inadapté, et non d’un bug de la bibliothèque d’authentification.

Comment activer le rendu à la demande ?

Le rendu à la demande requiert deux choses : un adaptateur, qui produit un serveur pour votre runtime cible, et une exclusion route par route. Les adaptateurs officiels d’Astro sont @astrojs/cloudflare, @astrojs/netlify, @astrojs/node et @astrojs/vercel, auxquels s’ajoutent des adaptateurs communautaires ; l’option adapter est documentée dans la référence de configuration. Installez-en un avec npx astro add node et consultez la page dédiée à cet adaptateur, car les options de configuration diffèrent.

Choisissez ensuite une forme. L’option output accepte exactement deux valeurs : 'static' et 'server'.

Forme du siteastro.config.mjsFrontmatter de la page
Site de contenu avec quelques pages authentifiéesadaptateur installé, conserver output: 'static' par défautexport const prerender = false sur chaque route qui lit une session
Application majoritairement authentifiéeadaptateur installé, définir output: 'server'export const prerender = true sur les routes marketing et de contenu

Quel que soit votre choix, la règle reste la même : la route qui lit la session doit être rendue à la demande, et la documentation de l’adaptateur Node couvre les spécificités du runtime pour l’exemple présenté ici.

Le middleware : src/middleware.ts

Le middleware résout l’utilisateur une fois par requête et le transmet à tout ce qui se trouve en aval. Placez le fichier dans src/middleware.js|ts, ou dans src/middleware/index.js|ts si vous préférez un dossier, et exportez-y une fonction nommée onRequest. Un export par défaut ne sera pas pris en compte, une règle sur laquelle le guide du middleware est explicite.

// 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();
});

Ce code lit la session intégrée d’Astro plutôt que de parser un cookie à la main. Cette même session apparaît sous deux noms, comme le décrit le guide des sessions d’Astro : les pages et composants y accèdent via Astro.session, tandis que les middlewares, les endpoints d’API et les handlers d’actions l’obtiennent depuis context.session. Le stockage n’est pas automatique. Trois adaptateurs choisissent un pilote par défaut à votre place — Node, Cloudflare et Netlify ; avec tout autre adaptateur, vous en désignez un vous-même, en suivant la référence des pilotes de session. Une contrainte à connaître avant de déployer sur un runtime edge : les sessions ne fonctionnent pas dans le middleware edge.

Typez locals en augmentant l’espace de noms App dans src/env.d.ts :

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

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

Lire l’utilisateur avec Astro.locals

Une page lit ce que le middleware lui a assigné, à condition qu’elle soit rendue à la demande. Astro.locals ne vit que le temps d’un seul rendu de route : c’est donc l’endroit approprié pour transporter un objet utilisateur du middleware vers une page, et le mauvais endroit pour conserver quoi que ce soit qui doive survivre jusqu’à la requête suivante.

---
// 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>

Supprimez la première ligne et la page réintègre l’ensemble prérendu : elle se compile en HTML statique, Astro.locals.user n’est jamais renseigné, et tous les visiteurs reçoivent le même fichier. Cette seule ligne fait la différence entre une authentification fonctionnelle et un écran de connexion que personne ne peut franchir.

Comment protéger les routes avec context.rewrite() ?

Protégez les routes en centralisant le contrôle dans le middleware et en affichant la page de connexion sur place plutôt qu’en y redirigeant. context.rewrite('/login') affiche un contenu différent à l’URL demandée par le visiteur, ce qui conserve le chemin protégé dans la barre d’adresse.

// 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();
});

Comme la réécriture relance le rendu depuis le début, votre middleware s’exécute une seconde fois au passage : gardez donc /login en dehors de protectedPaths, sans quoi ce second passage échouera au même contrôle et bouclera. Ce type de dysfonctionnement d’authentification lève rarement d’exception : le session replay d’un parcours de connexion montre l’utilisateur rebondir entre la page de connexion et la route protégée, ce à quoi ressemble concrètement, vu de l’extérieur, un cookie de session mal cadré ou un contrôle placé dans la mauvaise branche.

Évitez ici l’autre forme de réécriture. Lorsque vous passez un Request à next(), Astro construit une requête de remplacement à partir de l’ancienne, et toute tentative de lire le corps après ce point (ou avant) lève une exception à l’exécution. C’est particulièrement problématique quand une Action est déclenchée par un formulaire HTML, raison pour laquelle la documentation vous oriente plutôt vers context.rewrite() ou Astro.rewrite().

Connexion et déconnexion avec les Astro Actions

Les Astro Actions, introduites dans astro@4.15, constituent le moyen intégré de gérer la connexion et la déconnexion, et remplacent les routes d’API artisanales. Définissez-les dans un objet server exporté depuis src/actions/index.ts, indiquez accept: 'form', et envoyez-y des requêtes POST depuis du HTML brut.

// 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 provient d’astro/zod, qui réexporte Zod v4 : les validateurs de premier niveau comme z.email() et la clé error pour les messages personnalisés constituent donc la syntaxe actuelle. Régénérer l’identifiant de session à la connexion protège contre la fixation de session, et destroy() efface le cookie et supprime les données enregistrées côté serveur.

Notez deleteAccount. Les actions sont des endpoints publiquement accessibles avec leurs propres URL : n’importe qui peut donc en appeler une directement sans jamais charger la page qui affiche son formulaire. Chaque handler qui touche à des données utilisateur vérifie lui-même context.locals.

Côté page, il s’agit d’un formulaire et de la lecture d’un résultat :

---
// 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>

La déconnexion suit la même structure : <form method="POST" action={actions.logout}>, sans aucun JavaScript côté client.

Le piège des îlots : les îlots ne voient jamais locals

Astro.locals et Astro.session sont exclusivement côté serveur. Les middlewares, les pages et layouts .astro, les routes d’API et les handlers d’actions s’exécutent tous sur le serveur et les partagent. Un composant portant une directive client:* s’hydrate dans le navigateur, se situe en dehors de cette chaîne, et ne voit que ce que la page lui a transmis en 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} />

Rien ne lève d’exception dans la version défectueuse. La page affiche un contenu authentifié tandis que l’îlot à côté affiche un bouton de connexion : une incohérence qui ne se manifeste que visuellement.

Cet exemple utilise les sessions intégrées d’Astro, et la même séquence vaut pour une bibliothèque : le guide d’authentification d’Astro renvoie vers des bibliothèques d’authentification telles que Better Auth et Clerk pour la connexion par e-mail et OAuth, et la conception agnostique du framework de Better Auth s’étend à Astro, comme l’explique notre présentation de BetterAuth. Quel que soit votre choix, il faudra toujours un adaptateur, une route non prérendue et un middleware qui écrit dans context.locals.

Conclusion

L’authentification dans Astro est une chaîne dont le maillon faible se situe tout en amont : pas d’adaptateur et pas de prerender = false signifie pas de requête, et tout ce qui se trouve en aval ne fait silencieusement rien. Commencez par choisir le bon mode de rendu, puis occupez-vous du middleware, et enfin des contrôles par handler sur vos actions. Ouvrez le guide du rendu à la demande à côté de votre astro.config.mjs, installez un adaptateur, et ajoutez export const prerender = false à la première page qui a besoin d’un utilisateur.

FAQ

Les Astro Actions fonctionnent-elles sur une page prérendue ?

Non. Une page doit être rendue à la demande pour appeler une action via une action de formulaire : ajoutez donc 'export const prerender = false' à la page qui contient le formulaire, et installez un adaptateur afin qu'un serveur existe pour exécuter le handler. Les corps de requête des actions ont également un plafond par défaut de 1 Mo (1048576 octets) ; augmentez security.actionBodySizeLimit si un handler doit accepter quelque chose de plus volumineux, comme un envoi de fichier.

Une page statique prérendue peut-elle afficher une interface connectée ou déconnectée ?

Pas côté serveur. Une page prérendue est écrite sur le disque au moment du build et chaque visiteur reçoit un fichier identique : il n'y a donc aucun en-tête de cookie sur lequel se baser. Deux solutions fonctionnent : exclure cette route du prérendu avec 'export const prerender = false', ou conserver la page statique et récupérer l'utilisateur dans le navigateur depuis un endpoint rendu à la demande, en transmettant le résultat à un composant client.

Astro protège-t-il automatiquement les formulaires de connexion contre le CSRF ?

En partie. Sur les pages rendues à la demande, Astro compare l'en-tête origin envoyé par le navigateur à l'URL vers laquelle la requête a été adressée, et répond par un 403 lorsque les deux divergent. Ce comportement est activé par défaut depuis Astro 5, via l'option security.checkOrigin, et ne couvre que les soumissions de formulaires intersites. Vous devez toujours régénérer l'identifiant de session à la connexion, et toujours autoriser individuellement chaque handler d'action et chaque route d'API. Définir security.checkOrigin sur false désactive ce contrôle.

Faut-il utiliser context.rewrite ou Astro.redirect pour envoyer les utilisateurs déconnectés vers la page de connexion ?

Utilisez context.rewrite dans le middleware lorsque vous souhaitez servir le contenu de connexion à l'URL demandée par le visiteur, car cela conserve le chemin protégé dans la barre d'adresse et évite un second aller-retour du navigateur. Astro.redirect renvoie une réponse de redirection et le navigateur navigue vers /login. Une réécriture déclenche un nouveau rendu et votre middleware s'exécute à nouveau : excluez donc /login de vos chemins protégés, sinon le même contrôle échouera en boucle.

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.