Skip to content

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 WhatsApp 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
WhatsApp 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_KEY no 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