ADR-005 — Auth via Cookie HttpOnly + Worker Reescrita
| Campo | Valor |
|---|---|
| Status | ✅ Aceito |
| Data | 2026-04-30 |
| Autores | Equipe OLP |
| Implementação | supabase/functions/_shared/auth-helpers.ts, _shared/supabase-client.ts, workers/docs-auth/, Cloudflare Worker Gateway |
Contexto
Aplicações SPA tradicionais armazenam JWT em localStorage ou sessionStorage. Isso é vulnerável a XSS: qualquer script injetado consegue ler o token.
A plataforma OLP lida com:
- Dados pessoais sensíveis (CPF, telefone, dependentes menores de idade)
- Multi-escola e multi-papel — vazamento de token = bypass cross-school
- LGPD: vazamento de token implica notificação obrigatória
Adicionalmente, frontend (Lovable subdomain) e backend (Supabase subdomain) são cross-origin, o que historicamente foi uma barreira para cookies.
Decisão
JWT vive exclusivamente em cookie HttpOnly (olp_auth para sistema, olp_mural para portal). Nunca toca JavaScript.
Regras
- Backend (Edge Functions):
extractAuthenticatedUser(req)lê apenas o cookie. O fallback viaAuthorizationheader foi removido para fechar bypass por XSS (mesmo improvável). - Cliente Supabase para RLS:
createSupabaseClient(req)extrai o token do cookie e injeta noAuthorizationheader internamente ao SDK. O navegador nunca vê. - Frontend: nenhum código JS lê ou escreve
olp_auth. Sinal de "estou logado" élocalStorage.olp_last_activity(timestamp não-sensível) — ver ADR-010. - Cross-origin via Cloudflare Worker: o Worker (
gateway.olp.digital) reescreve cookies de Supabase domain → domain do app, comSameSite=None; Secure. - Atributos do cookie:
HttpOnly; Secure; SameSite=None; Path=/; Max-Age=28800(8h sistema, 2h portal). - Logout = Edge Function
logoutretornaSet-Cookieexpirando +queryClient.clear()no front.
Segregação de tokens
| Cookie | Origem | TTL | Usuários |
|---|---|---|---|
olp_auth | Login admin/coord/diretor/escola | 8h | Sistema |
olp_mural | Login portal aluno/responsável | 2h | Portal — bloqueado em Edge Functions de sistema (memória jwt-mural-segregation-guard) |
Alternativas consideradas
A. JWT em localStorage
Pros: trivial, padrão "comum". Cons: 1 XSS = token vazado. Inaceitável dado o escopo de dados. Veredicto: rejeitado.
B. JWT em cookie não-HttpOnly (Lovable padrão antigo)
Pros: SDK do Supabase consome direto. Cons: ainda lido por JS — mesmo problema do localStorage. Veredicto: rejeitado.
C. JWT no Authorization header + token em memória (zustand/context)
Pros: nunca persiste. Cons: refresh exige refazer login a cada reload; sem solução para multi-tab. Veredicto: rejeitado.
D. Sessões server-side com cookie de session ID opaco
Pros: máxima segurança (token nunca cruza fronteira). Cons: exige store de sessão (Redis), incompatível com edge stateless. Veredicto: reavaliar quando Worker V3 + Redis chegar (docs/architecture/WORKER_V3_REDIS_PLAN.md).
Consequências
Positivas
- XSS não vaza token (HttpOnly bloqueia leitura)
- CSRF mitigado por
SameSite=None+ verificação de origem no Worker + state na Edge Function - Sem necessidade de "logout em todas as abas" — cookie é compartilhado
- Re-assinatura de JWT em
select-roletroca papel sem reload (memóriaatomic-state-and-hydration-standard)
Negativas
- Cross-origin exigiu Worker dedicado (complexidade infra)
- Debug local mais difícil (DevTools precisa "Show All Cookies")
- Frontend não pode "saber" diretamente se está logado — precisa do
/meendpoint (ADR-010)
Trade-offs
| Eixo | Custo | Benefício |
|---|---|---|
| Complexidade infra | Worker Cloudflare obrigatório | XSS-safe + LGPD-compliant |
| DevX | "Por que não consigo ler o cookie?" | Onboarding ensina o porquê |
| Multi-domínio | Reescrita de cookie no Worker | Frontend + portal + docs no mesmo domínio lógico |
Conformidade
Como verificar:
# Frontend NÃO pode ler olp_auth
rg "document\.cookie" src --type ts # nenhuma referência a olp_auth
rg "localStorage.*olp_auth" src --type ts # zero
# Edge Functions NÃO podem cair em Authorization header como fallback de auth
rg "headers\.get\(['\"]authorization" supabase/functions --type ts -iSinais de violação:
localStorage.setItem('olp_auth', ...)document.cookie.includes('olp_auth')- Edge Function aceitando
Authorization: Bearercomo fonte primária de auth
Débito técnico conhecido
Varredura em 2026-04-30: limpa. Todas as referências a Authorization em Edge Functions são para outbound (chamada ao Supabase com token extraído do cookie).
Referências
supabase/functions/_shared/auth-helpers.tsdocs/security/AUTHENTICATION.mddocs/architecture/CLOUDFLARE_WORKER_GATEWAY.mdmem://security/jwt-mural-segregation-guard- ADR-001 — quem extrai o token
- ADR-010 — sinal de bootstrap sem ler cookie