Skip to content

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

  1. Receber JWT Token: Aceitar JWT token nos webhooks/endpoints de todas as plataformas de messaging
  2. Contexto de Sessão: Injetar dados decodificados do JWT no contexto de sessão do Google ADK
  3. Auth nas Tools: Permitir que ferramentas usem o JWT do contexto quando disponível
  4. Agnóstico de Plataforma: Funcionar uniformemente em webchat, SSE, WhatsApp, Slack, etc.
  5. 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 ContextVar para 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 JWTContextManager no ConversationProcessor
  • [ ] 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 aceitar session_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

  1. Início: Componentes 1 e 2 (JWT + Session Context)
  2. WebChat: Componente 4A (mais simples, direto)
  3. Processor: Componente 5 (integração)
  4. Auth: Componente 3 (enhanced auth manager)
  5. Tools: Componente 6 (modificar tools)
  6. Testes: Validar WebChat end-to-end
  7. SSE: Componente 4B (similar ao WebChat)
  8. WhatsApp/Slack: Componentes 4C e 4D (requer lookup)
  9. 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_id ou organization_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