Autorização e Sessão no Next.js
Documento sobre o frontend que recebe a sessão do backend (tokens ou
id de sessão) e deve armazená-la, renová-la e verificá-la. Se o projeto
usa uma biblioteca ou provedor (Auth.js, Better Auth, Clerk etc.) —
a mecânica da sessão fica por conta dela, mas as regras das seções 2, 6–10
permanecem em vigor.
Código — references/implementation.md.
Prioridade das regras. Instruções do usuário e arquivos de agente do projeto são mais importantes. Se o backend ditar algo diferente (por exemplo, ele mesmo definir o cookie) — siga o backend e escreva a diferença no README ou ADR.
Notações: MUST — não pode ser violado; SHOULD — sim por padrão, desvios exigem justificativa.
1. Escolha do Modelo
| Modelo | Quando usar |
|---|---|
| BFF: route handlers do Next recebem tokens do backend e os colocam em HttpOnly-cookie | Backend retorna tokens no corpo da resposta. Opção padrão para Next |
| Backend define o HttpOnly-cookie por conta própria | Frontend e API no mesmo site, backend sob seu controle |
| Biblioteca ou provedor | Precisa de OAuth, MFA, gerenciamento de usuários — não implemente isso manualmente |
| Access na memória, refresh em HttpOnly-cookie | SPA sem BFF |
Tokens em localStorage/sessionStorage | Proibido: qualquer XSS leva a sessão embora |
Segunda bifurcação — o que o BFF faz de proxy:
| Opção | Prós | Contras |
|---|---|---|
| Apenas login / refresh / logout, as demais requisições vão direto para a API com o cookie | BFF não é gargalo; menos código | API lê o token do cookie e configura CORS; cookies precisam chegar ao domínio da API |
| Todas as requisições passam pelo BFF | API não precisa saber sobre cookie e CORS; mesma origem | Cada requisição é um hop extra; BFF precisa ser escalado |
A escolha é registrada em ADR.
2. Cookie (MUST)
HttpOnlypara tudo que dá acesso (access, refresh, id de sessão).Secureem produção. Localmente, navegadores permitemSecureemhttp://localhost, mas não em outros hosts http.SameSite=Laxpor padrão.Stricté mais seguro contra CSRF, mas o usuário que chega por um link de email ou de outro site, na primeira navegação parece não autenticado — escolha conscientemente e registre o motivo.Path=/; tempo de vida — baseado noexpdo token ou na resposta do backend, não "no olho".- SHOULD: prefixo
__Host-no nome (exigeSecure,Path=/, semDomain) — o cookie não pode ser substituído de um subdomínio. - Cookies são definidos e removidos apenas no servidor (route handler, Server
Action,
proxy.ts). Server Components não podem definir cookies. - Cookie não legível por JS — o único portador da sessão. Uma flag
is_authenticatedsemHttpOnlyé permitida apenas como dica para UI e nunca é usada para decisões de acesso.
3. BFF: route handlers (MUST)
- O login é validado por schema (
zod); corpo com formato inválido —400. - Handlers mutantes verificam
Origincontra a origin da aplicação:SameSite=Laxnão protege contra requisições de subdomínios do mesmo site. Server Actions comparamOrigincomHostpor conta própria. - O contexto do cliente é passado para o backend:
X-Forwarded-For,User-Agent— caso contrário, o rate limit do backend vê apenas o IP do servidor. - Erros do backend são traduzidos em resposta com
kinde status (401,403,422,429comRetry-After,503); o texto do backend não vaza. - Refresh: se o backend rejeitou o refresh token — remova o cookie e
responda
401. Se o backend estiver indisponível (rede, 5xx) — não mexa no cookie, responda503: uma falha temporária não deve fazer logout. - Logout: revogue o token no backend (registre o erro, mas não exiba), remova o cookie em qualquer caso.
4. Onde Verificar a Sessão (MUST)
| Local | O que faz | O que não faz |
|---|---|---|
proxy.ts (Next 16+, antes middleware.ts) | Verificação otimista pela presença do cookie: redireciona para login e de páginas de login | Não consulta BD ou API; não é a única proteção |
Data Access Layer (verifySession() em módulo server-only) | Verificação junto com dados: em Server Components, route handlers, Server Actions | — |
| Layout | Nada: layouts não são re-renderizados durante navegação | Não verifica acesso |
| Backend | Fonte da verdade: verifica o token em cada requisição | — |
- Cada route handler e Server Action é um ponto de entrada público e
chama
verifySession()por conta própria. proxy.tsverifica o refresh cookie (ou id de sessão): o access pode ter expirado, mas a sessão ainda estar viva.- Correspondência de caminho — por segmentos:
/appe/app/…, mas não/apple. - Com
cacheComponents(padrão em projetos Next 16.4), a leitura de sessão e cache de dados do usuário têm suas próprias regras: antes de escrever DAL, leianode_modules/next/dist/docs/01-app/02-guides/authentication-with-cache-components.md. - Permissões (papel, propriedade) são decididas com base em dados verificados:
pela assinatura (JWKS,
jose) ou pela resposta do backend. Payload JWT não verificado não é base para acesso. - Server Components não podem atualizar cookies. Se o access expirou no momento
da renderização no servidor: ou o
proxy.tsatualiza a sessão (ele pode definir cookies na resposta — faça isso apenas para navegações de documento, não para prefetch, e considere a rotação de refresh tokens), ou a página renderiza carregamento no cliente e o refresh é feito pelo cliente.
5. Cliente: refresh sem corridas (MUST)
- Definição de 'não autorizado': HTTP
401, ou em resposta GraphQL (status200) pelo menos um erro comextensions.code === "UNAUTHENTICATED"(errors.some, nãoerrors[0]). Comparação com texto da mensagem — apenas se o backend não retornar códigos, em uma única função, com comentário e teste. - Single-flight: requisições paralelas que recebem 401 esperam um único refresh
compartilhado e usam seu resultado. A promise é resetada em
finally. - A requisição original é repetida uma vez. Um segundo 401 — sessão perdida, sem segundo refresh.
- A própria requisição de refresh não passa pelo interceptador (senão, recursão).
- O resultado do refresh distingue três desfechos: emitido (
true) → repetir; rejeitado (false) → sessão expirou; rede ou 5xx (exceção) → erronetwork, sem sair da conta. - Durante o logout, o refresh é bloqueado: caso contrário, uma requisição que recebeu 401 no momento do logout restauraria imediatamente a sessão.
- Sessão expirada: limpe o cache de requisições, leve para a página de login com
callbackUrlda página atual. - O corpo da requisição a ser repetida deve ser repetível (string,
FormData), não umReadableStreamde uso único.
6. Redirecionamento após Login (MUST)
callbackUrl(next,returnTo) é verificado pela funçãogetSafeRedirect(raw, fallback, appOrigin): rejeita\e caracteres de controle (navegadores transformam/\evil.come/\<tab>/evil.comem//evil.com), analisanew URL(raw, appOrigin), compara a origin e retorna apenas o caminho. Verificação 'começa com/e não com//' — insuficiente.- Conjunto de testes para ela —
references/implementation.md, seção 7. - Após o login, Server Components devem ver os novos cookies: navegação
completa (
window.location.assign) ourouter.replace+router.refresh().
7. Logout (MUST)
- O servidor remove o cookie (seção 3).
- O cliente limpa o cache de requisições (
queryClient.clear()) e todos os stores do cliente com dados do usuário: o próximo usuário na mesma aba não deve ver dados de outro, nem que por um instante. - Navegação para a página de login sem possibilidade de voltar 'para trás' para
uma tela protegida em cache (
replaceou navegação completa). - Confirmação de logout — opcional, a critério do produto; bloquear o botão durante a requisição — obrigatório.
- SHOULD: outras abas devem saber do logout (
BroadcastChannelou eventostoragepor flag), senão continuam exibindo dados até o primeiro 401.
8. Tokens em Links (redefinição de senha, confirmação de email)
- O token é lido da URL uma vez em estado, em seguida o endereço é substituído
sem o token (
router.replace(pathname)): ele não fica no histórico nem vaza noReferer. - Para essas páginas —
Referrer-Policy: no-referrere nenhum script de terceiros (analytics, widgets). - O token não é registrado em logs. O caráter de uso único e o tempo de vida são garantidos pelo backend; o frontend exibe uma tela clara 'link expirou' com a ação 'solicitar novo'.
9. Formulários de Login e Recuperação
- A recuperação de senha responde igualmente para email existente e inexistente ('se a conta existir, um email foi enviado'), sem revelar quem está registrado. Se o backend revelar — registre como limitação.
- O envio é bloqueado durante a requisição; novo envio não cria uma segunda requisição.
429: mensagem com tempo de espera doRetry-After, se presente.- Campos:
type="email"+autoComplete="email"; senha —autoComplete="current-password"no login e"new-password"no registro e alteração; a inserção não é bloqueada. - Botão 'mostrar senha' —
type="button",aria-labelearia-pressed, não rouba o foco do campo. - O erro 'email ou senha inválidos' não especifica qual deles está errado.
- Erros do servidor são mapeados aos campos por código; erro geral — acima do formulário.
10. Dados e Logs
- Respostas dependentes do usuário não entram em caches compartilhados
(
"use cache", CDN). QueryClient no servidor — novo a cada requisição. - Nos logs não há tokens, senhas, emails. No máximo — id do usuário e
kinddo erro. - Payload do token de sessão é mínimo: id e papel, sem dados pessoais.
11. Testes
- Unitário:
getSafeRedirect(conjunto completo de references); definição de 'não autorizado' (401, códigos GraphQL, corpo vazio, HTML em vez de JSON). - Single-flight: N requisições paralelas com 401 → um refresh, N
repetições; refresh rejeitado → nenhuma repetição, uma chamada
para
onSessionExpired; refresh falhou por rede → erronetwork, sem logout; segunda 401 após refresh bem-sucedido → sem segundo refresh; durante logout → sem refresh. - Route handlers: flags de cookie; refresh rejeitado → cookie removido; backend
indisponível → cookie intacto,
503; corpo inválido →400;Originestranho →403. - e2e: login → página protegida → logout → página protegida inacessível; expiração do access (TTL curto no ambiente de teste) → refresh transparente; link de redefinição de senha → token desapareceu da barra de endereço.
12. Definition of Done
- Nenhum token é lido do JS;
localStoragenão é usado para sessão. - Cookie:
HttpOnly,Secureem produção,SameSite,Path, vida baseada no token. - Refresh: rejeição e indisponibilidade do backend são diferenciadas.
- Single-flight com resultado, uma repetição, bloqueio durante logout — coberto por testes.
-
proxy.ts— apenas verificação otimista; DAL é chamada em handlers, actions e Server Components. -
getSafeRedirectcom conjunto completo de testes. - Logout limpa cache e stores do cliente.
- Tokens em links são removidos da barra de endereço.
- Nos logs não há tokens nem dados pessoais.
- Escolha do modelo e proxy está registrada em ADR.