Rate limiting e JWT WebSocket: proteger antes do lançamento
Tenho uma convicção: a segurança é feita antes do lançamento, não depois. Esperar ter utilizadores para proteger a sua aplicação é como instalar uma fechadura depois do assalto. Eis como implementei o rate limiting e a autenticação JWT nos WebSockets do TAMSIV — mesmo antes de ter um único utilizador.
Pontos chave a reter:
- O rate limiting HTTP (100 req/15min/IP) e WebSocket (10 conn/min + 60 msg/min/user) protegem contra abusos e custos imprevistos.
- A autenticação JWT é exigida desde a conexão WebSocket — não depois.
- Proteger cedo custa menos tempo e dinheiro do que proteger tarde.
- Cada mensagem WebSocket pode acionar 3 APIs pagas — o rate limiting protege o teu orçamento.
Porquê proteger uma aplicação antes de ter utilizadores?
A resposta curta: por causa dos custos. Cada chamada WebSocket no TAMSIV pode potencialmente acionar três APIs pagas: Deepgram para o STT, OpenRouter para o LLM, e OpenAI para o TTS. Sem rate limiting, um script malicioso — ou mesmo um simples bug — poderia gerar centenas de euros em faturas numa noite.
A resposta longa: por causa da arquitetura. Adicionar o rate limiting depois é refatorar código por todo o lado. Fazê-lo desde o início é um middleware limpo que se integra naturalmente no pipeline vocal.
E se tu começares com "veremos mais tarde" para a segurança, nunca o farás. É um princípio que aprendi ao construir o TAMSIV como desenvolvedor solo — cada atalho técnico é pago a cem vezes o seu valor.
Como implementar o rate limiting HTTP no Express?
O backend Express do TAMSIV expõe alguns endpoints REST (geração de imagens, push notifications, administração). Sem proteção, estes endpoints são vulneráveis a ataques de força bruta e DDoS.
Configurei um rate limiter simples mas eficaz: 100 requisições por 15 minutos por IP. Eis o raciocínio:
- 100 requisições: suficiente para um uso normal (um utilizador ativo raramente faz mais de 20 requisições em 15 minutos), com uma margem para picos.
- 15 minutos: janela deslizante. Mais curto (1 minuto) seria demasiado restritivo. Mais longo (1 hora) deixaria passar demasiadas requisições abusivas.
- Por IP: o método mais simples e universal. As soluções baseadas no user-id não protegem contra utilizadores não autenticados.
Na prática, o middleware express-rate-limit instala-se em 5 linhas. O cabeçalho Retry-After informa o cliente do tempo de espera. O código HTTP 429 (Too Many Requests) é a resposta padrão. É uma recomendação OWASP que todo o backend deveria implementar.
Porquê o rate limiting WebSocket é um verdadeiro desafio?
O HTTP é fácil. O WebSocket é outra história. A conexão é persistente e as mensagens chegam em fluxo contínuo. Tu não podes simplesmente contar as "requisições" — cada segmento de áudio gera uma mensagem, e o STT envia resultados intermédios em streaming.
Implementei dois níveis de proteção:
- 10 conexões por minuto por utilizador: impede o flood de conexões. Um utilizador normal abre 1-2 conexões por sessão. 10 é uma margem confortável que apanha os scripts.
- 60 mensagens por minuto por utilizador: limita o débito de mensagens numa conexão ativa. Parece muito, mas em streaming de áudio, os chunks chegam rapidamente.
Porquê 60 mensagens por minuto? Porque durante uma conversa vocal ativa, o cliente envia chunks de áudio a cada segundo. Com o STT nativo ou Deepgram, os resultados intermédios são adicionados. 60 mensagens deixam uma margem de segurança enquanto bloqueiam abusos óbvios.
A dificuldade adicional: a contagem deve ser por utilizador, não por conexão. Um utilizador malicioso poderia abrir 9 conexões e enviar 59 mensagens em cada uma, contornando um rate limit por conexão. A contagem por user-id (extraído do JWT) resolve este problema.
Como proteger a autenticação JWT nos WebSockets?
A autenticação WebSocket é fundamentalmente diferente do HTTP. Com HTTP, tu envias o token no cabeçalho Authorization em cada requisição. Com WebSocket, a autenticação é feita apenas uma vez, na conexão.
No TAMSIV, o token JWT Supabase é exigido desde a conexão:
ws://backend:3001?token=eyJhbGciOiJIUzI1NiIs...
O servidor verifica o token antes de aceitar a conexão. Se o token for inválido, expirado ou ausente: conexão recusada imediatamente. Nenhuma mensagem de erro detalhada (para evitar a fuga de informações), apenas um código de fecho WebSocket 4401.
Porquê o token está na query string e não num cabeçalho? Porque a API WebSocket do navegador não suporta cabeçalhos personalizados na conexão. É uma limitação conhecida do protocolo. A alternativa é enviar o token como primeira mensagem, mas isso deixa uma janela onde a conexão não está autenticada — inaceitável para o TAMSIV.
O token também está disponível através do cabeçalho Authorization: Bearer xxx para clientes que o suportam (como as bibliotecas Node.js). O servidor aceita ambos os métodos.
Quais são os custos reais de uma falha de segurança para um dev solo?
Para uma startup ou um dev solo, uma falha de segurança não é apenas um problema técnico — é um problema financeiro. Eis os cenários que o rate limiting previne:
- Cenário 1 — Bug do cliente: um loop infinito no frontend envia milhares de mensagens WebSocket. Sem rate limiting: fatura API explosiva. Com: 60 mensagens no máximo, depois bloqueio.
- Cenário 2 — Script malicioso: alguém usa a API para gerar conteúdo via LLM. Sem rate limiting: uso ilimitado às tuas custas. Com: bloqueado após 100 requisições/15min.
- Cenário 3 — DDoS leve: um bot martela os endpoints. Sem rate limiting: backend sobrecarregado, aplicação inacessível. Com: as requisições excessivas são rejeitadas antes de atingir a lógica de negócio.
O sistema de alertas de administração que implementei envia um email via Resend quando um utilizador atinge 80% do limite de taxa. Isso permite-me intervir antes que o problema se torne crítico.
Como testar a segurança de uma aplicação antes do lançamento?
A auditoria de segurança do TAMSIV cobriu vários eixos:
- Verificação de rate limiting: scripts de teste que enviam rajadas de requisições e verificam se o bloqueio é acionado no limite correto.
- Validação JWT: testes com tokens expirados, malformados, modificados (tampering), e tokens de outros projetos Supabase.
- Políticas RLS: verificação de que cada tabela do esquema da base de dados tem políticas de acesso corretas. Mais de 30 políticas RLS testadas individualmente.
- Sanitização de entrada: verificação de que as mensagens WebSocket são válidas e que as injeções são bloqueadas.
- Configuração CORS: apenas os domínios autorizados (tamsiv.com, IPs de desenvolvimento) podem conectar-se ao backend.
Estes testes fazem parte do conjunto de testes automatizados do backend. Cada implantação via railway up verifica se a segurança não regrediu.
Quais são as boas práticas de segurança para WebSockets em produção?
Aqui está a checklist que segui para o TAMSIV, baseada nas recomendações OWASP:
- Autenticação na conexão: JWT verificado antes de aceitar o upgrade WebSocket
- Rate limiting em dois níveis: conexões e mensagens, ambos por utilizador
- Validação de mensagens: cada mensagem é válida (formato, tamanho máximo, tipo) antes do processamento
- Timeouts de segurança: conexões inativas fechadas após 5 minutos. O AudioPlayerService também tem um timeout de 30 segundos para a limpeza.
- Registo de anomalias: cada limite de taxa atingido, cada tentativa de conexão falhada é registada para análise
- CORS estrito: lista branca de origens autorizadas, atualizada a cada mudança de domínio
- TLS obrigatório: em produção, apenas
wss://é aceite
Esta abordagem de segurança-primeiro alinha-se com a filosofia de arquitetura limpa do projeto. A segurança não é uma camada adicionada — é um componente arquitetónico ao mesmo nível do roteamento ou da base de dados.
Que impacto o rate limiting tem na experiência do utilizador?
Um rate limiting bem configurado é invisível para o utilizador normal. Os limites são calibrados para que um uso legítimo nunca os atinja. Mas quando um utilizador os atinge, a experiência deve permanecer clara:
- No lado HTTP: código 429 com cabeçalho
Retry-Aftere mensagem explicativa - No lado WebSocket: mensagem de erro genérica antes do fecho da conexão
- No lado frontend: notificação não-bloqueante informando o utilizador para abrandar
O sistema de notificações gere estes casos graciosamente. O utilizador sabe o que está a acontecer sem ser bloqueado abruptamente.
FAQ
O rate limiting afeta as conversas de voz em streaming?
Não, para um uso normal. O limite de 60 mensagens por minuto é calibrado para acomodar o streaming de áudio e os resultados intermédios do STT. Um utilizador que fala normalmente nunca excede este limite. Se o limite for atingido, é sinal de um bug do cliente ou de um uso abusivo.
Como o TAMSIV gere os tokens JWT expirados durante uma sessão?
O token JWT Supabase tem uma duração de uma hora. O cliente frontend atualiza o token automaticamente antes da expiração. Se a conexão WebSocket usar um token expirado, o servidor envia um código de fecho específico e o cliente reconecta-se com um novo token.
O rate limiting é configurável por utilizador?
Os limites padrão aplicam-se a todos os utilizadores. O painel de administração permite ver os utilizadores que se aproximam dos limites e ajustar os limites globais, se necessário. Uma evolução prevista permitirá limites diferenciados por plano de subscrição (Free vs Pro vs Team).
Porquê não usar um serviço de terceiros como o Cloudflare para o rate limiting?
O Cloudflare gere bem o rate limiting HTTP, mas não cobre os WebSockets de forma granular. O rate limiting da aplicação no TAMSIV permite um controlo fino por utilizador e por tipo de mensagem, o que um WAF externo não pode oferecer. As duas abordagens são complementares.
Como verificar se o rate limiting funciona corretamente?
O backend inclui testes automatizados que simulam rajadas de requisições e verificam se as respostas HTTP 429 chegam no limite correto. Os logs de produção mostram o número de requisições bloqueadas por dia, o que permite ajustar os limites continuamente.