JWT Authentication - Guia de Decisões Técnicas¶
🎯 Decisões de Arquitetura¶
1. Como passar contexto da sessão para as ferramentas?¶
Opções avaliadas:
| Opção | Prós | Contras | Decisão |
|---|---|---|---|
| A. Parâmetros explícitos | ✅ Explícito e type-safe | ❌ Modifica signature de todas as tools | ❌ Rejeitado |
| B. ContextVar (threading local) | ✅ Limpo, sem modificar signatures ✅ Thread-safe nativo ✅ Padrão Python |
❌ Pode parecer "mágico" | ✅ ESCOLHIDO |
| C. Singleton global | ✅ Simples | ❌ Não thread-safe ❌ Dificulta testes |
❌ Rejeitado |
Implementação escolhida:
from contextvars import ContextVar
# Definir context var
current_session_context: ContextVar[dict] = ContextVar(
'current_session_context',
default=None
)
# No processor (antes de chamar runner)
current_session_context.set({
"session_id": message.thread_id,
"user_id": message.user_id,
"app_name": self.runner.app_name,
"session_service": self.runner.session_service,
})
# Nas tools
ctx = current_session_context.get()
if ctx:
session_context = await get_session_context(...)
Justificativa: Mais limpo, não requer modificar 20+ ferramentas, thread-safe por padrão.
2. Onde aceitar JWT token nas requisições?¶
Opções avaliadas:
| Local | WebChat | SSE | Slack | |
|---|---|---|---|---|
| Header Authorization | ✅ Recomendado | ✅ Recomendado | ❌ N/A | ❌ N/A |
| Body campo jwt_token | ✅ Alternativa | ✅ Alternativa | ❌ N/A | ❌ N/A |
| Metadata campo jwt_token | ✅ Possível | ❌ Menos comum | ❌ N/A | ❌ N/A |
| Lookup em cache | ⚠️ Desnecessário | ⚠️ Desnecessário | ✅ Única opção | ✅ Única opção |
Implementação escolhida: - WebChat/SSE: Aceitar nos 3 locais (header priority → body → metadata) - WhatsApp/Slack: Lookup em Redis/DB baseado em phone/user_id
Justificativa: Flexibilidade para frontend, mas WhatsApp/Slack não suportam metadata customizado.
3. Como armazenar JWT context na sessão ADK?¶
Opções avaliadas:
| Opção | Prós | Contras | Decisão |
|---|---|---|---|
| A. Novo campo na tabela sessions | ✅ Relacional ✅ Queries SQL fáceis |
❌ Requer migration ❌ Menos flexível |
❌ Rejeitado |
| B. JSON no campo metadata | ✅ Flexível ✅ Sem migration ✅ ADK nativamente suporta |
❌ Queries JSON complexas | ✅ ESCOLHIDO |
Estrutura:
# Session no ADK
{
"session_id": "sess_123",
"user_id": "user_456",
"metadata": {
"jwt_context": {
"has_jwt": true,
"user_id": "789",
"email": "usuario@example.com",
"token": "eyJhbG...",
"token_exp": "2026-02-13T23:59:59Z"
}
}
}
Justificativa: ADK Session Service já tem campo metadata, é JSON, flexível, sem necessidade de migrations.
4. Como validar JWT tokens?¶
Biblioteca: PyJWT (https://pyjwt.readthedocs.io/)
Validações habilitadas:
import jwt
payload = jwt.decode(
token,
key=JWT_SECRET_KEY,
algorithms=[JWT_ALGORITHM], # HS256 ou RS256
verify=True,
options={
"verify_signature": True, # ✅ Obrigatório
"verify_exp": True, # ✅ Obrigatório
"verify_nbf": True, # ✅ Not before
"verify_iat": True, # ✅ Issued at
"verify_aud": True, # ✅ Audience
"require": ["exp", "sub", "email"], # Claims obrigatórios
},
audience="ifriend-api",
issuer="https://theifriend.com",
)
Justificativa: Segurança máxima, previne tokens falsificados ou expirados.
5. Como fazer lookup de JWT para WhatsApp/Slack?¶
Storage: Redis (TTL de 30 dias)
Estrutura de chaves:
# WhatsApp
whatsapp:jwt:{phone_number} → JWT token
whatsapp:jwt:+5511999999999 → "eyJhbG..."
# Slack
slack:jwt:{slack_user_id} → JWT token
slack:jwt:U123ABC456 → "eyJhbG..."
Processo de vinculação:
1. Usuário autentica no site (obtém JWT)
2. Frontend chama POST /whatsapp/link {phone, JWT}
3. Backend valida JWT e armazena: SET whatsapp:jwt:{phone} {JWT} EX 2592000
4. Mensagens futuras do WhatsApp fazem GET whatsapp:jwt:{phone}
5. Se encontrado, usa JWT; senão, usa auth default
Justificativa: Redis é rápido, suporta TTL nativo, perfeito para cache de curto/médio prazo.
6. Algoritmo de assinatura JWT¶
Opções:
| Algoritmo | Tipo | Prós | Contras | Decisão |
|---|---|---|---|---|
| HS256 | Symmetric | ✅ Simples ✅ Rápido |
❌ Uma chave para tudo | ✅ Inicial |
| RS256 | Asymmetric | ✅ Chave pública/privada ✅ Mais seguro |
❌ Mais complexo ❌ Mais lento |
⚠️ Futuro |
Implementação inicial: HS256 com secret compartilhado
Migração futura: RS256 se necessário multi-serviço validation
Configuração:
# .env
JWT_SECRET_KEY=<secret-forte-complexo-256-bits>
JWT_ALGORITHM=HS256
Justificativa: HS256 é suficiente para MVP, RS256 adiciona complexidade sem benefício imediato.
7. TTL (Time To Live) do JWT¶
Padrão: 24 horas
Rationale: - ✅ Usuário não precisa relogar toda hora - ✅ Longo o suficiente para conversas extensas - ✅ Curto o suficiente para segurança - ⚠️ Se expirar, agente volta para auth default (não quebra)
Refresh strategy:
- Frontend monitora expiração (claim exp)
- Se exp - now < 5min, renova token
- Envia novo JWT na próxima mensagem
Futura melhoria: Refresh token para renovação automática.
8. Como lidar com JWT expirado durante conversa?¶
Estratégias:
| Estratégia | Impacto | Decisão |
|---|---|---|
| A. Bloquear conversa | ❌ UX ruim | ❌ Rejeitado |
| B. Fallback para auth default | ✅ Conversa continua ⚠️ Perde context personalizado |
✅ ESCOLHIDO |
| C. Solicitar reauth | ⚠️ UX médio ✅ Mantém context |
⚠️ Futuro |
Implementação:
if jwt_token:
try:
jwt_context = await jwt_manager.get_user_context(jwt_token)
except jwt.ExpiredSignatureError:
logger.warning("[JWT] Token expirado, usando auth default")
jwt_context = None # Fallback
Log para usuário (opcional):
ℹ️ Sua sessão expirou. Para continuar com acesso personalizado,
faça login novamente no site.
Justificativa: Prioriza disponibilidade sobre strict auth. Usuário não fica bloqueado.
9. Estrutura de claims do JWT¶
Claims obrigatórios:
{
"sub": "user_789", // OBRIGATÓRIO: User ID
"email": "user@example.com", // OBRIGATÓRIO: Email
"exp": 1707955800, // OBRIGATÓRIO: Expiration
}
Claims recomendados:
{
"name": "João Silva", // Nome completo
"roles": ["traveler"], // Roles/permissões
"iat": 1707869400, // Issued at
"iss": "https://theifriend.com", // Issuer
"aud": "ifriend-api", // Audience
}
Claims opcionais:
{
"tenant_id": "acme_corp", // Multi-tenancy
"organization": "ACME Inc",
"phone": "+5511999999999",
"locale": "pt-BR",
"timezone": "America/Sao_Paulo",
}
Justificativa: Mínimo viável + extensibilidade para casos avançados.
10. Como logar JWT sem expor tokens?¶
❌ NUNCA fazer:
logger.info(f"Token recebido: {jwt_token}") # ❌❌❌ NUNCA!
✅ Correto:
# Opção 1: Logar apenas claims (sem token)
logger.info(f"[JWT] Usuário autenticado: {user_context.email} (id: {user_context.user_id})")
# Opção 2: Logar parcial do token (debug)
token_partial = f"{jwt_token[:10]}...{jwt_token[-10:]}"
logger.debug(f"[JWT] Token parcial: {token_partial}")
# Opção 3: Hash do token (auditoria)
import hashlib
token_hash = hashlib.sha256(jwt_token.encode()).hexdigest()[:16]
logger.info(f"[JWT] Token hash: {token_hash}")
Justificativa: Segurança. Tokens em logs podem vazar em agregadores, Sentry, etc.
11. Rate limiting por JWT vs Default¶
Estratégia:
| Tipo | Limite | Janela | Decisão |
|---|---|---|---|
| Com JWT | 100 req/min | Por user_id | ✅ Liberal |
| Sem JWT (default) | 20 req/min | Por IP | ✅ Conservador |
Implementação (usando Redis + Lua):
# rate_limiter.py
async def check_rate_limit(user_id: str, has_jwt: bool) -> bool:
key = f"ratelimit:{user_id}"
limit = 100 if has_jwt else 20
window = 60 # segundos
# Lua script atomic increment + expire
current = await redis.incr(key)
if current == 1:
await redis.expire(key, window)
return current <= limit
Justificativa: Usuários autenticados (JWT) têm mais confiança, podem ter limites maiores.
12. Monitoramento e Observabilidade¶
Métricas (Prometheus):
from prometheus_client import Counter, Histogram, Gauge
# Contadores
jwt_messages_total = Counter('ifriend_jwt_messages_total', 'Total messages with JWT', ['platform'])
jwt_validation_errors = Counter('ifriend_jwt_validation_errors_total', 'JWT validation errors', ['error_type'])
# Histogram de latência
jwt_decode_duration = Histogram('ifriend_jwt_decode_duration_seconds', 'Time to decode JWT')
# Gauge de sessões ativas com JWT
active_jwt_sessions = Gauge('ifriend_active_jwt_sessions', 'Active sessions with JWT context')
Logs estruturados:
import structlog
logger = structlog.get_logger()
logger.info(
"jwt_authentication",
user_id=user_context.user_id,
email=user_context.email,
platform=message.platform,
session_id=message.thread_id,
token_expires_in_seconds=token_ttl,
)
Justificativa: Visibilidade completa para troubleshooting e analytics.
13. Testes¶
Estrutura:
tests/
├── unit/
│ ├── test_jwt_context_manager.py # Decode, validate
│ ├── test_session_context_helper.py # Session storage
│ └── test_auth_manager.py # get_auth_headers()
├── integration/
│ ├── test_webchat_jwt.py # WebChat end-to-end
│ ├── test_sse_jwt.py # SSE end-to-end
│ ├── test_whatsapp_jwt_lookup.py # WhatsApp lookup
│ └── test_slack_jwt_lookup.py # Slack lookup
└── e2e/
└── test_full_flow_with_jwt.py # Full conversation flow
Coverage mínimo: 90%
Casos de teste críticos: - ✅ JWT válido → autenticação com JWT - ✅ JWT expirado → fallback para auth default - ✅ JWT inválido (signature) → fallback - ✅ Sem JWT → auth default - ✅ JWT durante conversa (múltiplas mensagens) - ✅ Múltiplas sessões com diferentes JWTs - ✅ WhatsApp lookup (encontrado vs não encontrado) - ✅ Token expira durante conversa → fallback graceful
Justificativa: Confiabilidade e prevenção de regressões.
🔧 Configurações por Ambiente¶
Development (.env.development)¶
JWT_SECRET_KEY=dev-secret-key-not-secure
JWT_ALGORITHM=HS256
JWT_VERIFY_SIGNATURE=false # ⚠️ DEV ONLY
JWT_VERIFY_EXP=false # ⚠️ DEV ONLY
JWT_DEBUG=true
Staging (.env.staging)¶
JWT_SECRET_KEY=${STAGING_JWT_SECRET} # From secrets manager
JWT_ALGORITHM=HS256
JWT_VERIFY_SIGNATURE=true
JWT_VERIFY_EXP=true
JWT_DEBUG=true
Production (.env.production)¶
JWT_SECRET_KEY=${PROD_JWT_SECRET} # From secrets manager
JWT_ALGORITHM=HS256
JWT_VERIFY_SIGNATURE=true # ✅ Obrigatório
JWT_VERIFY_EXP=true # ✅ Obrigatório
JWT_DEBUG=false # ❌ Sem logs debug
JWT_ISSUER=https://theifriend.com
JWT_AUDIENCE=ifriend-api
Justificativa: Flexibilidade dev vs segurança prod.
📊 Matriz de Decisão: JWT Strategy por Plataforma¶
| Plataforma | JWT Source | Cache? | Fallback? | Priority |
|---|---|---|---|---|
| WebChat | Header/Body | ❌ | ✅ Default auth | 🔴 P0 |
| SSE | Header/Body | ❌ | ✅ Default auth | 🔴 P0 |
| Redis lookup | ✅ 30d | ✅ Default auth | 🟡 P1 | |
| Slack | Redis lookup | ✅ 30d | ✅ Default auth | 🟡 P1 |
| Telegram | Redis lookup | ✅ 30d | ✅ Default auth | 🟢 P2 |
Legenda: - 🔴 P0: Implementar primeiro (MVP) - 🟡 P1: Segunda fase - 🟢 P2: Futuro
🚀 Quick Start Checklist¶
Para Backend Developer¶
- [ ] Ler JWT_AUTH_PLAN.md completo
- [ ] Implementar
JWTContextManager - [ ] Implementar
SessionContextHelper - [ ] Modificar
ConversationProcessor - [ ] Modificar
get_auth_headers() - [ ] Modificar adapters (WebChat → SSE → WhatsApp → Slack)
- [ ] Escrever testes (unit + integration + e2e)
Para Frontend Developer¶
- [ ] Ler JWT_INTEGRATION_EXAMPLES.md
- [ ] Implementar envio de JWT no WebChat widget
- [ ] Implementar renovação automática de token
- [ ] Testar fluxo completo com backend
Para DevOps¶
- [ ] Adicionar
JWT_SECRET_KEYno secrets manager - [ ] Configurar variáveis de ambiente por stage
- [ ] Setup monitoramento (Prometheus metrics + logs)
- [ ] Configurar alertas para JWT validation errors
Para QA¶
- [ ] Testar todos os cenários (JWT válido/inválido/expirado/sem JWT)
- [ ] Testar todas as plataformas (WebChat, SSE, WhatsApp, Slack)
- [ ] Validar logs e métricas
- [ ] Teste de carga com JWT
Último update: 13/02/2026
Mantenedor: Tech Lead iFriend Agents
Versão: 1.0