12k
All articles

Como Adicionar Autenticação a um Site Astro

Configure a autenticação no Astro com adapter, middleware, Astro.locals, sessions, rewrite e Actions, incluindo login, logout e proteção de rotas.

OpenReplay Team
OpenReplay Team
Como Adicionar Autenticação a um Site Astro

A autenticação no Astro depende tanto da forma como uma página é renderizada quanto da biblioteca que você escolhe. Por padrão, o Astro pré-renderiza as páginas do seu projeto em tempo de build, e você opta por excluir rotas individuais desse comportamento. Uma página pré-renderizada é gerada antes que qualquer visitante exista, portanto não há requisição, não há cabeçalho de cookie e não há sessão para ler.

Uma primeira tentativa costuma falhar silenciosamente por esse motivo. Você instala uma biblioteca de autenticação, cola o middleware dela em src/middleware.ts, carrega /account, e Astro.locals é um objeto vazio. Nenhum erro, nenhum log, nenhum stack trace para pesquisar.

Este artigo percorre toda a cadeia em ordem, arquivo por arquivo: o adaptador, a exportação prerender por rota, o middleware que resolve o usuário, a página que o lê, a proteção de rotas com rewrite(), login e logout como Astro Actions, e o limite onde as ilhas do lado do cliente deixam de enxergar qualquer coisa disso. O código tem como alvo o Astro 7.x. O Astro 6 elevou o piso do Node para a versão 22 e removeu o suporte ao Node 18 e 20, então use o Node 22.12.0 ou posterior.

Principais Conclusões

  • O Astro pré-renderiza todo o projeto por padrão, então qualquer página que leia uma sessão precisa optar por sair disso com export const prerender = false, e a renderização sob demanda exige um adaptador.
  • Se o seu middleware parece não fazer nada, a rota que ele deveria proteger quase certamente ainda está pré-renderizada.
  • Astro.locals vive por exatamente uma renderização de rota; dados que precisam sobreviver até a próxima requisição pertencem a Astro.session, que requer um driver de sessão.
  • Astro Actions são endpoints HTTP publicamente acessíveis, então cada handler precisa da sua própria verificação em context.locals; proteger a página que renderiza o formulário não protege a action por trás dele.
  • Astro.locals e Astro.session existem apenas no servidor, então um componente com uma diretiva client:* só consegue enxergar o que a página lhe passou como props.

Por Que o “Estático por Padrão” Quebra a Autenticação no Astro?

Um projeto Astro é compilado para HTML estático, a menos que uma rota peça algo diferente, e o HTML estático é produzido uma única vez, em tempo de build, para todos os visitantes. Pré-renderizar o projeto inteiro é o padrão documentado, como estabelece o guia de renderização sob demanda. Uma sessão vive em um cabeçalho de requisição que ainda não existe quando esse HTML é escrito em disco, portanto leituras de cookies, Astro.request e qualquer coisa que um middleware coloque em locals não têm a que se vincular.

A consequência prática: um middleware que parece nunca executar é um sintoma de modo de renderização, não um bug da biblioteca de autenticação.

Como Habilitar a Renderização Sob Demanda?

A renderização sob demanda precisa de duas coisas: um adaptador, que produz um servidor para o runtime de destino, e uma exclusão por rota. Os adaptadores oficiais do Astro são @astrojs/cloudflare, @astrojs/netlify, @astrojs/node e @astrojs/vercel, com adaptadores da comunidade ao lado deles, e a opção adapter está documentada na referência de configuração. Instale um com npx astro add node e consulte a página do próprio adaptador, já que as opções de configuração variam.

Depois escolha um formato. A opção output aceita exatamente dois valores, 'static' e 'server'.

Formato do siteastro.config.mjsFrontmatter da página
Site de conteúdo com algumas páginas autenticadasadaptador instalado, mantenha o padrão output: 'static'export const prerender = false em toda rota que lê uma sessão
Aplicação majoritariamente autenticadaadaptador instalado, defina output: 'server'export const prerender = true nas rotas de marketing e conteúdo

Qualquer que seja a escolha, a regra é a mesma: a rota que lê a sessão precisa ser renderizada sob demanda, e a documentação do adaptador Node cobre as especificidades de runtime para o exemplo aqui.

O Middleware: src/middleware.ts

O middleware resolve o usuário uma vez por requisição e o entrega a tudo que vem depois. Coloque o arquivo em src/middleware.js|ts, ou em src/middleware/index.js|ts se preferir uma pasta, e forneça uma exportação nomeada chamada onRequest. Uma exportação default não será reconhecida, uma regra sobre a qual o guia de middleware é explícito.

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

Isso lê a sessão nativa do Astro em vez de fazer o parsing de um cookie manualmente. A mesma sessão aparece sob dois nomes, como descreve o guia de sessões do Astro: páginas e componentes a acessam por meio de Astro.session, enquanto middleware, endpoints de API e handlers de actions a obtêm de context.session. O armazenamento não é automático. Três adaptadores escolhem um driver padrão para você — Node, Cloudflare e Netlify; com qualquer outro adaptador você mesmo indica um, seguindo a referência de drivers de sessão. Uma restrição que vale conhecer antes de fazer deploy em um runtime de edge: sessões não funcionam em edge middleware.

Tipe locals estendendo o namespace App em src/env.d.ts:

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

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

Lendo o Usuário Com Astro.locals

Uma página lê o que o middleware atribuiu, desde que essa página seja renderizada sob demanda. Astro.locals vive por exatamente uma renderização de rota, então é o lugar certo para carregar um objeto de usuário do middleware até uma página e o lugar errado para guardar qualquer coisa que precise sobreviver até a próxima requisição.

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

Apague a primeira linha e a página volta para o conjunto pré-renderizado: ela é compilada em HTML estático, Astro.locals.user nunca é preenchido, e todos os visitantes recebem o mesmo arquivo. Essa única linha é a diferença entre uma autenticação funcional e uma tela de login que ninguém consegue ultrapassar.

Como Proteger Rotas Com context.rewrite()?

Proteja rotas centralizando a barreira no middleware e renderizando a página de login no lugar, em vez de redirecionar para ela. context.rewrite('/login') exibe conteúdo diferente na URL que o visitante solicitou, o que mantém o caminho protegido na barra de endereços.

// 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 o rewrite reinicia a renderização, seu middleware executa uma segunda vez no caminho, então mantenha /login fora de protectedPaths ou essa segunda passagem falhará na mesma verificação e entrará em loop. Uma autenticação quebrada desse tipo raramente lança exceções: o session replay de um fluxo de login mostra o usuário quicando entre a página de login e a rota protegida, que é exatamente a aparência externa de um cookie de sessão com escopo incorreto ou de uma barreira posicionada no ramo errado.

Evite a outra forma de rewrite aqui. Quando você passa um Request para next(), o Astro constrói uma requisição substituta a partir da antiga, e qualquer tentativa de ler o corpo depois desse ponto (ou antes dele) lança um erro em tempo de execução. Isso é mais doloroso quando uma Action é acionada por um formulário HTML, que é o motivo pelo qual a documentação direciona você para context.rewrite() ou Astro.rewrite().

Login e Logout Com Astro Actions

As Astro Actions, adicionadas no astro@4.15, são a maneira nativa de lidar com login e logout, e substituem rotas de API feitas à mão. Defina-as em um objeto server exportado de src/actions/index.ts, configure accept: 'form', e envie requisições a elas a partir de HTML puro.

// 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 vem de astro/zod, que reexporta o Zod v4, então validadores de nível superior como z.email() e a chave error para mensagens personalizadas são a grafia atual. Regenerar o ID da sessão no login protege contra fixação de sessão, e destroy() limpa o cookie e descarta os dados salvos no servidor.

Observe deleteAccount. Actions são endpoints publicamente acessíveis com suas próprias URLs, então qualquer pessoa pode chamar uma diretamente sem jamais carregar a página que renderiza o formulário correspondente. Todo handler que toca dados de usuário verifica context.locals por conta própria.

Do lado da página, temos um formulário e a leitura do 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>

O logout tem o mesmo formato: <form method="POST" action={actions.logout}>, sem necessidade de JavaScript no lado do cliente.

A Armadilha das Ilhas: Ilhas Nunca Enxergam locals

Astro.locals e Astro.session existem apenas no servidor. Middleware, páginas e layouts .astro, rotas de API e handlers de actions todos rodam no servidor e os compartilham. Um componente que carrega uma diretiva client:* é hidratado no navegador, fica fora dessa cadeia e enxerga apenas o que a página lhe passou 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} />

Nada lança erro na versão quebrada. A página renderiza conteúdo de usuário autenticado enquanto a ilha ao lado renderiza um botão de login, uma incoerência que só aparece visualmente.

Este exemplo usa as sessões nativas do Astro, e a mesma sequência vale para uma biblioteca: o guia de autenticação do Astro aponta para bibliotecas de autenticação como Better Auth e Clerk para login por e-mail e OAuth, e o design agnóstico de framework do Better Auth se estende ao Astro, como explica nossa visão geral do BetterAuth. Qualquer que seja a sua escolha, ela ainda precisa de um adaptador, de uma rota não pré-renderizada e de um middleware que escreva em context.locals.

Conclusão

A autenticação no Astro é uma cadeia com um elo fraco logo no início: sem adaptador e sem prerender = false não há requisição, e tudo o que vem depois silenciosamente não faz nada. Acerte primeiro o modo de renderização, depois o middleware, depois as verificações por handler nas suas actions. Abra o guia de renderização sob demanda ao lado do seu astro.config.mjs, instale um adaptador e adicione export const prerender = false à primeira página que precisa de um usuário.

Perguntas Frequentes

As Astro Actions funcionam em uma página pré-renderizada?

Não. Uma página precisa ser renderizada sob demanda para chamar uma action por meio de um action de formulário, então adicione 'export const prerender = false' à página que contém o formulário e instale um adaptador para que exista um servidor capaz de executar o handler. Os corpos de requisição das actions também têm um limite padrão de 1 MB (1048576 bytes); aumente security.actionBodySizeLimit se um handler precisar aceitar algo maior, como um upload.

Uma página estática pré-renderizada pode exibir UI de usuário autenticado ou não autenticado?

Não no servidor. Uma página pré-renderizada é escrita em disco em tempo de build e todos os visitantes recebem o arquivo idêntico, portanto não há cabeçalho de cookie para ramificar a lógica. Duas soluções funcionam: excluir essa rota da pré-renderização com 'export const prerender = false', ou manter a página estática e buscar o usuário no navegador a partir de um endpoint sob demanda, passando o resultado para um componente cliente.

O Astro protege formulários de login contra CSRF automaticamente?

Parcialmente. Em páginas renderizadas sob demanda, o Astro compara o cabeçalho origin enviado pelo navegador com a URL para a qual a requisição foi feita, e responde com 403 quando os dois divergem. Esse comportamento está ativado por padrão desde o Astro 5, por meio da opção security.checkOrigin, e cobre apenas envios de formulários entre sites. Você ainda deve regenerar o ID da sessão no login, e ainda deve autorizar individualmente cada handler de action e cada rota de API. Definir security.checkOrigin como false desativa a verificação.

Devo usar context.rewrite ou Astro.redirect para enviar usuários não autenticados à página de login?

Use context.rewrite no middleware quando quiser que o conteúdo de login seja servido na URL que o visitante solicitou, já que isso mantém o caminho protegido na barra de endereços e evita uma segunda ida e volta do navegador. Astro.redirect retorna uma resposta de redirecionamento e o navegador navega para /login. Um rewrite inicia uma nova renderização e seu middleware executa novamente, então exclua /login dos seus caminhos protegidos ou a mesma verificação continuará falhando.

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.