Construindo um Twilio Webhook seguro com Python e FastAPI

May 19, 2020
Escrito por
Gabriela Cavalcante
Contribuidor
As opiniões expressas pelos colaboradores da Twilio são de sua autoria

Construindo um webhook Twilio seguro utilizando Python e FastAPI

Alguns dias atrás, eu estava procurando por um Framework Web Python para construir uma aplicação de chat que fosse assíncrona e que tivesse alta performance. Como eu sou bem familiarizada com Flask, não quis gastar tempo aprendendo algo muito diferente.

Foi então que eu encontrei o FastAPI: um framework web moderno para construir APIs com Python 3.6+, baseado no Starlette e inspirado no Flask. Este framework utiliza o padrão das type hints, já traz validações e serialização, além de usar o padrão aberto para APIs, OpenAPI. Foi uma escolha perfeita porque este framework não somente suporta   requisições assíncronas como também geração automática de documentação. Você pode encontrar outros recursos incríveis do FastAPI aqui.

Nesse tutorial, nós vamos construir um webhook Twilio usando esse framework fantástico. Para entender na prática como o FastAPI funciona e como podemos construir aplicações com ele, vamos implementar um webhook seguro que valida toda requisição de entrada, uma vez que um problema comum com webhooks é garantir que as requisições estão vindo da Twilio e não de uma fonte maliciosa. Assim, no nosso exemplo, vamos assumir que qualquer requisição que não venha da Twilio é um bad request. Neste tutorial, Miguel Grinberg nos dá um exemplo de como construir um webhook seguro em Flask para reset de senhas de usuário.k.

Requisitos para o Tutorial

Para seguir este tutorial você precisará dos seguintes componentes:

  • Python 3.7+. Se seu sistema operacional não tem o interpretador Python, você pode ir no link  python.org e instalar a última versão.
  • ngrok. Nós iremos usar esta ferramenta para conectar a aplicação Flask que estará executando na sua máquina a uma URL pública e, com isso, a Twilio vai conseguir se conectar à sua aplicação.. Se você não tem o ngrok instalado, você pode fazer o download para Windows, MacOS ou Linux.
  • Uma conta Twilio. Se você é novo(a) na Twilio, crie uma conta gratuita. Se você usar esse link para abrir sua conta, você irá receber $10 de crédito quando você fizer o upgrade.

Criando um ambiente virtual Python

O primeiro passo é criar um diretório separado para nosso projeto Python. Nele, vamos criar um ambiente virtual (virtual environment) para instalar os pacotes Python necessários. Você não precisa criar um ambiente virtual, mas é uma prática recomendada para que projetos tenham seus ambientes isolados.

Abra o terminal e crie um novo diretório para nosso projeto. Dentro dele vamos criar o ambiente virtual usando o módulo  venv do Python. Para executar essas tarefas, digite os seguintes comandos.

$ mkdir fastapi-webhook
$ python -m venv env 
$ source env/bin/activate   # for Unix and Mac OS
$ env\Scripts\activate         # for Windows
(env) $ pip install fastapi python-multipart uvicorn twilio

Esses comandos vão criar um diretório env, ativar o ambiente virtual e instalar os pacotes que vamos usar no projeto, que são:

  • O Framework FastAPI, para criar a aplicação web;
  • Python-multipart, para extrair os dados vindos como parâmetros da requisição. Essa biblioteca é uma dependência do FastAPI;
  • Uvicorn, um servidor web ASGI para executar nossa aplicação;
  • A biblioteca Twilio Python Helper, para acessar as APIs da Twilio

Usando FastAPI para construir nosso webhook

A Twilio precisa notificar nossa aplicação sempre que certos eventos acontecerem, como, por exemplo, uma mensagem enviada pelo usuário. Para fazer isso, nós usamos um webhook, que é um endpoint no nosso script que a Twilio usa para se comunicar com nossa aplicação. Nós precisamos configurar a URL do nosso webhook no Console da Twilio para permitir essa comunicação.

Quando você logar no Console, você vai ver duas informações importantes: a Account SID e Auth Token da sua conta. Essas são suas credenciais da Twilio, que permitem que o código Python acesse a conta da Twilio e utilize suas a APIs. Para nosso exemplo, nós precisamos guardar o Auth Token de forma segura em uma variável de ambiente para usá-la mais tarde. Do seu terminal, execute os seguintes comandos dentro da pasta fastapi-webhook:

# para Unix e Mac OS
echo "export TWILIO_AUTH_TOKEN=seu-token" >> .env
source .env

# para Windows
set TWILIO_AUTH_TOKEN=seu-token

Para construir o webhook nós iremos usar o FastAPI, um framework web moderno para construir APIs com Python 3.6+. Ele é baseado no padrão Python type hints e no padrão aberto para APIs, OpenAPI.

Vamos escrever uma implementação para nosso webhook. Dentro da pasta fastapi-webhook, crie um arquivo chamado main.py com o seguinte conteúdo:

from fastapi import FastAPI, Form, Response
from twilio.twiml.messaging_response import MessagingResponse

app = FastAPI()

@app.post("/hook")
async def chat(From: str = Form(...), Body: str = Form(...)):
   response = MessagingResponse() 
   msg = response.message(f"Hi {From}, you said: {Body}")
   return Response(content=str(response), media_type="application/xml")

Primeiro, nós importamos a classe FastAPI, que provê as funcionalidades necessárias para construir nosso endpoint. Você pode ver que nós usamos essa classe para criar o objeto app, que é a instância da nossa aplicação. O decorator @app.post(“/hook”) estabelece que a função chat (definida logo abaixo) irá processar as requisições enviadas para a URL  /hook usando o método POST.  

Nossos dados estão vindo em campos do tipo form. No FastAPI nós definimos dados do tipo form criando parâmetros do tipo Form. Se você não está familiarizado com type hints, esse código pode parecer um pouco estranho. Python 3.6+ suporta type hints (é um recurso opcional), que permite declarar os tipos das variáveis e dos argumentos. Isso pode ser útil para deixar mais claro qual o tipo de dado que a variável deve ter. Isso também permite que editores de código e linters chequem se o seu código tem algum bug relacionado a tipos. No nosso hook, nós usamos Python type hints para permitir que o FastAPI valide os dados. Você pode ler um rápido tutorial sobre type hints e como usá-las na validação dos dados no FastAPI aqui.

Diferente do Flask, o FastAPI não tem um servidor embutido. Para rodar a aplicação, você precisa de um servidor web ASGI, como o Uvicorn. Inicie o servidor com:

$ uvicorn main:app --reload

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.

No comando acima, main está fazendo referência ao arquivo main.py, app se refere ao objeto app que criamos na linha app = FastAPI(), e --reload faz o servidor reiniciar automaticamente depois de mudanças no código.

Definimos os argumento da função chat com os nomes From e Body que correspondem exatamente com campos enviados pela Twilio. Se, por exemplo, você tentar chamar esse endpoint sem passar o campo Body, receberá um erro informando que Body é um campo obrigatório. Mantenha o Uvicorn rodando e, a partir de outro terminal, tente enviar uma requisição que possua apenas o campo From:

$ curl -X POST -d "From=+8499999999999" http://127.0.0.1:8000/hook           
{"detail":[{"loc":["body","Body"],"msg":"field required","type":"value_error.missing"}]}

Com o servidor ainda em execução, você pode acessar no seu navegador o link http://127.0.0.1:8000/docs e ver a documentação automática da API fornecida pelo Swagger UI, ou ir para http://127.0.0.1:8000/redoc para ver uma alternativa de formato da documentação provida pela ReDoc

Swagger UI Documentation

ReDoc Documentation

 

Testando o webhook

Agora nós vamos testar nosso webhook enviando um SMS. Se o servidor Uvicorn não está executando, inicie-ocom este comando:

uvicorn main:app --reload

Seu servidor agora está rodando na porta 8000, mas apenas dentro do seu computador e como um serviço privado, que está disponível apenas para você. Nós iremos usar o ngrok para fazer com que nosso servidor possa ser encontrado a partir da internet. Abra um segundo terminal e execute ./ngrok http 8000 para atribuir uma URL pública temporária ao servidor. As linhas iniciadas com “Forwarding” mostram a URL pública que o ngrok usa para redirecionar requisições ao nosso serviço.

ngrok window

Vá no Console da Twilio e clique em Phone Numbers. Selecione um número de telefone, se você já tiver um ou mais em sua conta, ou compre um novo clicando no sinal “mais” em vermelho.

Observe que, se você estiver usando uma conta de teste da Twilio, você não será cobrado por esta compra. Um requisito adicional das contas de teste é que você verifique seu número de telefone pessoal. Nas contas de teste, o Twilio envia SMS apenas para números verificados. Você pode verificar seu número de telefone here.   

Na configuração do número de telefone, na seção Mensagens, copie a URL https:// do ngrok e cole no campo “A message comes in”, acrescentando a URL / hook do nosso endpoint no final. Clique no botão vermelho "Save" para armazenar esta alteração.

Twilio webhook

Para testar a aplicação, envie um SMS para o seu número de telefone da Twilio e veja a resposta!

Lembre-se de que, como estamos usando o ngrok gratuitamente, você não pode manter uma sessão ativa por mais de 8 horas. Quando você reiniciar o ngrok, a URL atribuída a você será diferente, portanto, será necessário atualizá-la no Console da Twilio. Quando você fizer o deploy do seu aplicativo para uso em produção, você o fará em um servidor diretamente conectado à Internet, assim, o ngrok não será necessário..

Validando a assinatura da Twilio

Nosso webhook está criado e testado! Mas, agora tente executar este comando (lembre-se de atualizar o domínio ngrok abaixo pela sua URL atribuída):

$ curl -X POST -d "From=+8499999999999&Body=attack" https://1ff28f03.ngrok.io/hook
<?xml version="1.0" encoding="UTF-8"?><Response><Message>Hi  8499999999999, you said: attack</Message></Response>

Ow! Fizemos uma solicitação POST para o nosso webhook, passando dados nos campos From e Body e obtivemos uma resposta. Portanto, nosso webhook está recebendo requisições e respondendo-as, mesmo que não sejam do Twilio.

A resposta está no formato TwiML. TwiML é uma linguagem baseada em XML com tags definidas pela Twilio com instruções para executar eventos como enviar mensagens WhatsApp e SMS. No exemplo acima, a mensagem de texto está dentro da tag <Message>.

Em nosso exemplo, não é um grande problema que a resposta TwiML seja enviada a terceiros, porque não incluímos nenhuma informação confidencial, mas na vida real,  sua  aplicação pode retornar informações pessoais, fotos particulares ou outros detalhes confidenciais e, nestes casos, precisamos nos preocupar com quem está fazendo a requisição e, se não for a Twilio, devemos assumir que é um “bad request” e ignorá-la.

Para ajudar a validar as requisições enviadas pela Twilio, uma assinatura é incluída na requisição. A Twilio gera e inclui essa assinatura e em todas as requisições enviadas aos webhooks. Vamos ver como validar essa assinatura em nosso webhook.

Substitua o conteúdo do seu arquivo main.py pelo conteúdo a seguir:

import os
from fastapi import FastAPI, Form, Response, Request, HTTPException
from twilio.twiml.messaging_response import MessagingResponse
from twilio.request_validator import RequestValidator  

app = FastAPI()

@app.post("/hook")
async def chat(
    request: Request, From: str = Form(...), Body: str = Form(...) 
):
    validator = RequestValidator(os.environ["TWILIO_AUTH_TOKEN"])
    form_ = await request.form()
    if not validator.validate(
        str(request.url), 
        form_, 
        request.headers.get("X-Twilio-Signature", "")
    ):
        raise HTTPException(status_code=400, detail="Error in Twilio Signature")

    response = MessagingResponse()
    msg = response.message(f"Hi {From}, you said: {Body}")
    return Response(content=str(response), media_type="application/xml")

Adicionamos o objeto Request como argumento em nossa função chat. Usando este objeto, podemos obter os cabeçalhos, a URL da requisição e a lista completa de variáveis enviadas no form pela Twilio; todas essas informações são necessárias para calcular e verificar a assinatura. Embora agora possamos obter os dados From e Body no dicionário form_, se fizermos dessa maneira, eles não serão automaticamente validados nem documentados, por isso continuamos usando os argumentos From e o Body como antes, para não perdermos esses recursos.

Esta versão do código tem mais importações. O RequestValidator é responsável por verificar a assinatura do Twilio e nós o inicializamos passando o Twilio Auth Token. Como nós armazenamos o token de autenticação na variável de ambiente TWILIO_AUTH_TOKEN no início do artigo, agora basta fazer referência a esta variável.

Chamamos o método validate com três argumentos: a URL da solicitação, um dicionário com os dados do formulário que recebemos e a assinatura Twilio, que está presente no cabeçalho X-Twilio-Signature.

Se a assinatura Twilio for válida, o método validate retornará True e, se não, retornará False. Se a assinatura que é gerada pelo RequestValidator a partir  dos argumentos que passamos não corresponder à X-Twilio-Signature anexada à solicitação, nosso aplicativo retornará um erro 400. Vamos fazer um teste passando uma assinatura Twilio falsa:

$ curl -X POST -H 'X-Twilio-Signature: fake-signature' -d 'From=+8499999999999&Body=attack' https://1ff28f03.ngrok.io/hook
{"detail":"Error in Twilio Signature"}

Para verificar que a validação da assinatura está funcionando, basta enviar outra mensagem de texto para seu número da Twilio.

Conclusão

Nesse tutorial nós construímos um webhook para se comunicar com a Twilio utilizando FastAPI. Mesmo tendo uma implementação simples, nosso webhook é seguro, e já possui documentação, validação e serialização que são fornecidas pelo Framework. Espero que você utilize esta implementação como base e construa seus próprios projetos usando FastAPI.

Boa sorte!

Gabriela Cavalcante é entusiasta Python e uma fã do Flask. Você pode encontrar mais projetos dela no GitHub.