Plano de Implementação: Autenticação JWT Agnóstica de Plataforma¶
📋 Visão Geral¶
Implementar um sistema de autenticação JWT que permita que usuários autenticados em plataformas externas (webchat, whatsapp, slack, etc.) utilizem o agente iFriend com suas próprias credenciais, ao invés do usuário default da API.
🎯 Objetivos¶
- Receber JWT Token: Aceitar JWT token nos webhooks/endpoints de todas as plataformas de messaging
- Contexto de Sessão: Injetar dados decodificados do JWT no contexto de sessão do Google ADK
- Auth nas Tools: Permitir que ferramentas usem o JWT do contexto quando disponível
- Agnóstico de Plataforma: Funcionar uniformemente em webchat, SSE, WhatsApp, Slack, etc.
- Backward Compatible: Manter compatibilidade com autenticação via usuário default (API_EMAIL/API_PASSWORD)
🏗️ Arquitetura Proposta¶
1. Fluxo de Dados¶
Cliente (Web/WhatsApp/Slack)
├─ Envia mensagem + JWT token no metadata
↓
Adapter (WebChat/SSE/WhatsApp/Slack)
├─ Parse request → IncomingMessage
├─ Extrai JWT token do request
├─ Adiciona JWT ao metadata da IncomingMessage
↓
ConversationProcessor
├─ Recebe IncomingMessage
├─ Decodifica JWT token (se presente)
├─ Enriquece contexto da sessão ADK com dados do JWT
├─ Passa mensagem para Runner
↓
Runner ADK
├─ Processa mensagem
├─ Mantém contexto de sessão (incluindo JWT data)
├─ Executa ferramentas
↓
Tools (booking, pagamento, etc)
├─ Consulta contexto da sessão
├─ Se JWT presente → usa JWT token
├─ Se não → autentica com API_EMAIL/API_PASSWORD (default)
├─ Faz chamadas para API iFriend
📦 Componentes a Implementar¶
Componente 1: JWT Context Manager¶
Arquivo: ifriend_agent/session/jwt_context.py
Responsabilidades: - Decodificar JWT tokens - Validar assinatura do token (usando chave pública ou secret) - Extrair claims do token (user_id, email, name, roles, etc.) - Gerenciar contexto JWT por sessão
Interface:
class JWTContextManager:
async def decode_token(self, token: str) -> dict:
"""Decodifica e valida JWT token"""
async def get_user_context(self, token: str) -> UserContext:
"""Extrai contexto do usuário do token"""
def is_token_valid(self, token: str) -> bool:
"""Verifica se token é válido e não expirou"""
@dataclass
class UserContext:
user_id: str
email: str
name: Optional[str]
roles: List[str]
custom_claims: Dict[str, Any]
token: str # Token original para usar nas chamadas de API
Configuração (variáveis de ambiente):
JWT_SECRET_KEY=<chave para validar assinatura>
JWT_ALGORITHM=HS256 # ou RS256 se usar chave pública/privada
JWT_VERIFY_SIGNATURE=true
JWT_VERIFY_EXP=true # Verificar expiração
Componente 2: Session Context Extension¶
Arquivo: ifriend_agent/session/session_context.py
Responsabilidades: - Estender metadados da sessão ADK para incluir JWT context - Fornecer interface para ferramentas acessarem contexto JWT
Estrutura de dados da sessão:
{
"session_id": "sess_123",
"user_id": "user_456",
"app_name": "ifriend_agent",
"created_at": "...",
"jwt_context": { # NOVO
"has_jwt": true,
"user_id": "789",
"email": "usuario@example.com",
"name": "João Silva",
"roles": ["traveler", "premium"],
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_exp": "2026-02-13T23:59:59Z",
"custom_claims": {}
}
}
Interface:
class SessionContextHelper:
@staticmethod
async def set_jwt_context(
session_service,
app_name: str,
user_id: str,
session_id: str,
jwt_context: UserContext
):
"""Adiciona JWT context à sessão"""
@staticmethod
async def get_jwt_context(
session_service,
app_name: str,
user_id: str,
session_id: str
) -> Optional[UserContext]:
"""Recupera JWT context da sessão"""
@staticmethod
async def has_third_party_context(
session_service,
app_name: str,
user_id: str,
session_id: str
) -> bool:
"""Verifica se sessão tem contexto de terceiro"""
Componente 3: Enhanced Auth Manager¶
Arquivo: ifriend_agent/tools/booking/auth.py (modificado)
Modificações: - Permitir passar JWT token como argumento - Se JWT fornecido → usa direto (não autentica) - Se não → autentica com API_EMAIL/API_PASSWORD (comportamento atual)
Nova interface:
async def get_auth_headers(
jwt_token: Optional[str] = None,
session_context: Optional[dict] = None
) -> dict:
"""
Obtém headers de autenticação.
Args:
jwt_token: JWT token do usuário (se disponível)
session_context: Contexto da sessão ADK (para extrair JWT)
Returns:
dict: Headers com Authorization Bearer token
Comportamento:
- Se jwt_token fornecido → usa direto
- Se session_context tem JWT → extrai e usa
- Senão → autentica com API_EMAIL/API_PASSWORD
"""
Exemplo de uso nas tools:
# Ferramenta com acesso ao contexto da sessão
async def buscar_usuario_tool(email: str, session_context: dict = None) -> dict:
# Obtém headers com JWT do contexto (se existir)
headers = await get_auth_headers(session_context=session_context)
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
...
Componente 4: Message Adapter Extensions¶
Arquivos:
- messaging/adapters/webchat_adapter.py
- messaging/adapters/sse_adapter.py
- messaging/adapters/whatsapp_official_adapter.py
- messaging/adapters/slack_adapter.py
Modificações em cada adapter:
A. WebChat Adapter¶
async def parse_webhook_request(self, request: Request) -> Optional[IncomingMessage]:
data = await request.json()
# Extrai JWT do header ou body
jwt_token = (
request.headers.get("Authorization", "").replace("Bearer ", "") or
data.get("jwt_token") or
data.get("metadata", {}).get("jwt_token")
)
# Adiciona ao metadata
metadata = data.get("metadata", {})
if jwt_token:
metadata["jwt_token"] = jwt_token
return IncomingMessage(
platform="webchat",
user_id=data["user_id"],
channel_id=data.get("session_id", str(uuid.uuid4())),
thread_id=data.get("session_id", str(uuid.uuid4())),
text=data["message"],
metadata=metadata # JWT incluído aqui
)
Formato de request esperado:
// Opção 1: JWT no header
POST /webchat/message
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
{
"user_id": "user123",
"message": "Olá!",
"metadata": {}
}
// Opção 2: JWT no body
POST /webchat/message
{
"user_id": "user123",
"message": "Olá!",
"jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"metadata": {}
}
// Opção 3: JWT no metadata
POST /webchat/message
{
"user_id": "user123",
"message": "Olá!",
"metadata": {
"jwt_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
B. SSE Adapter¶
# Similar ao WebChat - aceita JWT no request inicial
async def parse_webhook_request(self, request: Request) -> Optional[IncomingMessage]:
# JWT pode vir no header ou body
jwt_token = (
request.headers.get("Authorization", "").replace("Bearer ", "") or
data.get("jwt_token")
)
# ... resto similar ao webchat
C. WhatsApp Adapter¶
# JWT pode ser extraído de:
# 1. Metadata customizado da mensagem (se plataforma suportar)
# 2. Lookup em serviço externo baseado no phone number
# 3. Integração com webhook customizado que enriquece com JWT
async def parse_webhook_request(self, request: Request) -> Optional[IncomingMessage]:
# Para WhatsApp, pode precisar de lookup:
# - Mapear phone_number → user no sistema
# - Buscar JWT armazenado para esse usuário
phone_number = self.extract_phone_number(data)
jwt_token = await self.lookup_user_jwt(phone_number) # Novo método
metadata = {
"phone_number": phone_number,
}
if jwt_token:
metadata["jwt_token"] = jwt_token
return IncomingMessage(...)
Nota WhatsApp: Como WhatsApp não permite enviar metadata customizado facilmente, pode ser necessário: - Manter mapeamento phone_number → JWT em cache/DB - Usuário autentica uma vez via web, sistema armazena associação - Mensagens subsequentes do WhatsApp usam JWT do cache
D. Slack Adapter¶
# Slack pode usar:
# 1. User ID do Slack → lookup em mapeamento
# 2. Comando slash customizado para usuário se autenticar
# 3. Integração OAuth do Slack se relevante
async def parse_webhook_request(self, request: Request) -> Optional[IncomingMessage]:
slack_user_id = data.get("user_id")
jwt_token = await self.lookup_user_jwt(slack_user_id)
metadata = {
"slack_user_id": slack_user_id,
}
if jwt_token:
metadata["jwt_token"] = jwt_token
return IncomingMessage(...)
Componente 5: Conversation Processor Enhancement¶
Arquivo: messaging/processor.py (modificado)
Modificações: - Detectar JWT no metadata da IncomingMessage - Decodificar JWT e validar - Injetar contexto JWT na sessão ADK antes de processar mensagem
class ConversationProcessor:
def __init__(self, runner: Runner, jwt_manager: JWTContextManager):
self.runner = runner
self.jwt_manager = jwt_manager
async def process_message(
self,
message: IncomingMessage,
adapter: MessagingAdapter
) -> bool:
# ... código existente ...
# NOVO: Processar JWT se presente
jwt_token = message.metadata.get("jwt_token")
jwt_context = None
if jwt_token:
try:
# Decodifica e valida token
jwt_context = await self.jwt_manager.get_user_context(jwt_token)
logger.info(
f"[JWT] Usuário autenticado via JWT: "
f"{jwt_context.email} (id: {jwt_context.user_id})"
)
except Exception as e:
logger.warning(f"[JWT] Token inválido ou expirado: {e}")
# Continua sem JWT (usa auth default)
jwt_context = None
# Garante sessão existe
session = await self.runner.session_service.get_session(
app_name=self.runner.app_name,
user_id=message.user_id,
session_id=message.thread_id
)
if not session:
await self.runner.session_service.create_session(
app_name=self.runner.app_name,
user_id=message.user_id,
session_id=message.thread_id
)
# NOVO: Injeta JWT context na sessão
if jwt_context:
await SessionContextHelper.set_jwt_context(
session_service=self.runner.session_service,
app_name=self.runner.app_name,
user_id=message.user_id,
session_id=message.thread_id,
jwt_context=jwt_context
)
# Processa mensagem normalmente
user_content = types.Content(
role="user",
parts=[types.Part.from_text(text=message.text)]
)
async for event in self.runner.run_async(
session_id=message.thread_id,
user_id=message.user_id,
new_message=user_content
):
# ... resto do processamento ...
Componente 6: Tool Context Injection¶
Problema: Como passar o contexto da sessão para as ferramentas?
Opção A: Modificar Signature das Tools (mais explícito)¶
@tool
async def buscar_usuario_tool(
email: str,
session_id: str = None, # Novo parâmetro
user_id: str = None, # Novo parâmetro
) -> dict:
"""Busca usuário na API"""
# Acessa contexto da sessão
if session_id and user_id:
session_context = await get_session_context(
session_service=runner.session_service,
app_name="ifriend_agent",
user_id=user_id,
session_id=session_id
)
# Usa JWT do contexto se disponível
headers = await get_auth_headers(session_context=session_context)
else:
# Fallback para auth default
headers = await get_auth_headers()
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
...
Como ADK passa session_id/user_id para tools? - Usar callback ou middleware do ADK - Injetar automaticamente nos parâmetros da tool
Opção B: Context Variable (Threading Local - mais limpo)¶
# ifriend_agent/session/context_vars.py
from contextvars import ContextVar
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:
from ifriend_agent.session.context_vars import current_session_context
@tool
async def buscar_usuario_tool(email: str) -> dict:
# Acessa contexto automaticamente
ctx = current_session_context.get()
if ctx:
session_context = await get_session_context(
session_service=ctx["session_service"],
app_name=ctx["app_name"],
user_id=ctx["user_id"],
session_id=ctx["session_id"]
)
headers = await get_auth_headers(session_context=session_context)
else:
headers = await get_auth_headers()
# ... resto da tool
Recomendação: Usar Opção B (ContextVar) por ser mais limpo e não requerer modificação de signature de todas as tools.
🔐 Segurança¶
1. Validação de JWT¶
- ✅ Verificar assinatura do token
- ✅ Verificar expiração (exp claim)
- ✅ Verificar issuer (iss claim) se relevante
- ✅ Verificar audience (aud claim) se relevante
2. Logs¶
- ✅ Logar quando JWT é usado (com user_id/email do JWT)
- ✅ Logar quando auth default é usado
- ✅ Logar tokens inválidos/expirados
- ⚠️ Nunca logar o token completo (apenas primeiros/últimos chars)
3. Rate Limiting¶
- Considerar rate limiting por JWT user_id
- Diferentes limites para auth default vs JWT
4. Revogação de Tokens¶
- Manter blacklist de tokens revogados (Redis/DB)
- Verificar blacklist antes de aceitar token
📝 Checklist de Implementação¶
Fase 1: Infraestrutura Base¶
- [ ] 1.1. Criar
JWTContextManager(ifriend_agent/session/jwt_context.py) - [ ] 1.2. Criar
SessionContextHelper(ifriend_agent/session/session_context.py) - [ ] 1.3. Criar
ContextVarpara session context (ifriend_agent/session/context_vars.py) - [ ] 1.4. Adicionar dependências JWT (PyJWT) no
requirements.txt - [ ] 1.5. Adicionar variáveis de ambiente para JWT config
Fase 2: Adapters¶
- [ ] 2.1. Modificar
WebChatAdapter.parse_webhook_request()para extrair JWT - [ ] 2.2. Modificar
SSEAdapter.parse_webhook_request()para extrair JWT - [ ] 2.3. Criar serviço de lookup JWT para WhatsApp (
whatsapp_jwt_lookup.py) - [ ] 2.4. Modificar
WhatsAppOfficialAdapter.parse_webhook_request()para lookup JWT - [ ] 2.5. Criar serviço de lookup JWT para Slack (
slack_jwt_lookup.py) - [ ] 2.6. Modificar
SlackAdapter.parse_webhook_request()para lookup JWT
Fase 3: Processamento¶
- [ ] 3.1. Injetar
JWTContextManagernoConversationProcessor - [ ] 3.2. Modificar
ConversationProcessor.process_message()para: - [ ] Extrair JWT do metadata
- [ ] Decodificar e validar JWT
- [ ] Injetar JWT context na sessão ADK
- [ ] Configurar ContextVar com session context
- [ ] 3.3. Limpar ContextVar após processar mensagem
Fase 4: Autenticação¶
- [ ] 4.1. Modificar
get_auth_headers()para aceitarsession_context - [ ] 4.2. Implementar lógica de fallback (JWT → default auth)
- [ ] 4.3. Adicionar logs de auditoria
Fase 5: Tools¶
- [ ] 5.1. Modificar todas as tools em
ifriend_agent/tools/booking/para usar context - [ ]
buscar_usuario_tool.py - [ ]
buscar_agencia_tool.py - [ ]
criar_conta_agencia_tool.py - [ ]
criar_conta_viajante_tool.py - [ ] Outras tools de booking/payment
- [ ] 5.2. Modificar tools que chamam API iFriend para usar JWT do contexto
Fase 6: Testes¶
- [ ] 6.1. Criar testes unitários para
JWTContextManager - [ ] 6.2. Criar testes unitários para
SessionContextHelper - [ ] 6.3. Criar testes de integração WebChat com JWT
- [ ] 6.4. Criar testes de integração SSE com JWT
- [ ] 6.5. Criar testes de fallback (sem JWT)
- [ ] 6.6. Testar tokens expirados/inválidos
- [ ] 6.7. Testar múltiplas sessões simultâneas com diferentes JWTs
Fase 7: Documentação¶
- [ ] 7.1. Atualizar README com instruções de JWT
- [ ] 7.2. Criar guia de integração para frontend (como enviar JWT)
- [ ] 7.3. Documentar formato esperado do JWT (claims obrigatórios)
- [ ] 7.4. Criar exemplos de uso para cada plataforma
📊 Métricas de Sucesso¶
- [ ] 100% dos adapters suportam JWT
- [ ] Backward compatibility mantida (apps sem JWT continuam funcionando)
- [ ] Logs claros indicando fonte da autenticação
- [ ] Zero vazamento de tokens em logs
- [ ] Testes passando para todos os cenários
- [ ] Documentação completa
🚀 Ordem de Implementação Recomendada¶
- Início: Componentes 1 e 2 (JWT + Session Context)
- WebChat: Componente 4A (mais simples, direto)
- Processor: Componente 5 (integração)
- Auth: Componente 3 (enhanced auth manager)
- Tools: Componente 6 (modificar tools)
- Testes: Validar WebChat end-to-end
- SSE: Componente 4B (similar ao WebChat)
- WhatsApp/Slack: Componentes 4C e 4D (requer lookup)
- Finalização: Testes completos e documentação
⚠️ Considerações Importantes¶
1. JWT para WhatsApp/Slack¶
Essas plataformas não enviam metadata customizado facilmente. Opções:
- A. Autenticação prévia via web: Usuário autentica no site, sistema armazena
mapeamento phone_number → JWT ou slack_user_id → JWT
- B. Comando de autenticação: Usuário envia comando /auth <token> para registrar JWT
- C. Integração OAuth: Para Slack, usar OAuth flow nativo
2. Expiração de Tokens¶
- JWT pode expirar durante conversa longa
- Implementar refresh automático ou notificar usuário para reautenticar
- Armazenar refresh token se disponível
3. Performance¶
- Cache de JWT decodificados (para evitar decodificar a cada tool call)
- Usar ContextVar (já thread-safe e eficiente)
4. Multi-tenancy¶
- JWT pode incluir
tenant_idouorganization_id - Ferramentas podem filtrar dados por tenant
5. Roles e Permissões¶
- JWT pode incluir roles (
["admin", "user", "premium"]) - Ferramentas podem verificar permissões antes de executar
📖 Exemplo de JWT Esperado¶
{
"sub": "user_789", // user_id
"email": "usuario@example.com",
"name": "João Silva",
"roles": ["traveler", "premium"],
"tenant_id": "acme_corp", // Opcional: multi-tenancy
"iat": 1707869400, // Issued at
"exp": 1707955800, // Expiration (24h)
"iss": "https://theifriend.com", // Issuer
"aud": "ifriend-api" // Audience
}
Claims obrigatórios:
- sub (user ID)
- email
- exp (expiração)
Claims opcionais:
- name, roles, tenant_id, custom claims
🔄 Diagrama de Fluxo Completo¶
┌─────────────────────────────────────────────────────────────────┐
│ 1. Cliente envia mensagem com JWT │
│ POST /webchat/message │
│ Authorization: Bearer eyJhbG... │
│ { │
│ "user_id": "user123", │
│ "message": "Quero reservar uma experiência" │
│ } │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. WebChatAdapter extrai JWT │
│ - Parse request │
│ - Extrai JWT do header/body │
│ - Cria IncomingMessage com JWT no metadata │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. ConversationProcessor processa │
│ - Recebe IncomingMessage │
│ - Decodifica JWT (JWTContextManager) │
│ - Valida assinatura e expiração │
│ - Extrai user context (email, roles, etc) │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Injeta JWT context na sessão │
│ - SessionContextHelper.set_jwt_context() │
│ - Armazena em session_service │
│ - Configura ContextVar │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. Runner ADK processa mensagem │
│ - Passa mensagem para agente │
│ - Agente decide chamar tool │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. Tool executa com contexto JWT │
│ - Lê ContextVar (session_id, user_id, etc) │
│ - Obtém session context do session_service │
│ - Extrai JWT token do context │
│ - get_auth_headers(session_context) │
│ → Retorna "Authorization: Bearer <JWT do usuário>" │
│ - Faz chamada para API iFriend COM JWT │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. API iFriend recebe request autenticado │
│ - Valida JWT (mesmo usuário que está no site) │
│ - Retorna dados do usuário autenticado │
│ - Não precisa reautenticar! │
└────────────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 8. Resposta retorna ao usuário │
│ - Tool retorna resultado │
│ - Agente gera resposta │
│ - Processor envia via adapter │
│ - Usuário recebe resposta personalizada │
└─────────────────────────────────────────────────────────────────┘
🎓 Exemplo Prático: Reserva com JWT¶
Sem JWT (comportamento atual)¶
Usuário: "Quero reservar a experiência 123"
↓
Tool buscar_usuario_tool(email="usuario@example.com")
↓
get_auth_headers() → autentica com API_EMAIL/API_PASSWORD (usuário default)
↓
API retorna dados do usuário default (não é o usuário do site!)
↓
❌ Problema: usando credenciais erradas
Com JWT (novo comportamento)¶
Usuário (autenticado no site): "Quero reservar a experiência 123"
+ JWT token no header
↓
Tool buscar_usuario_tool(email="usuario@example.com")
↓
get_auth_headers(session_context) → usa JWT do contexto
↓
API recebe request com JWT do usuário autenticado
↓
API retorna dados do PRÓPRIO usuário
↓
✅ Sucesso: usuário correto, sem reautenticação!
📚 Referências¶
Status: 📋 PLANEJAMENTO
Próximo passo: Aprovação para iniciar implementação
Estimativa: 2-3 semanas para implementação completa