Proteja-se contra bots de compra com autenticação de 2 fatores

Veja como a autenticação de 2 fatores (2FA) pode ajudar a lidar com bots de compra.

Produtos com vendas extremamente rápidas, lançamentos esgotados em poucos minutos, os ingressos esgotados para um show. Para se ter uma ideia, nem sempre você perdeu uma compra por que outra pessoa foi mais ágil.

Na verdade, isso pode muito bem ser obra de um bot cambista. Tais robôs executam tarefas específicas e repetitivas em milésimos de segundos, o que facilita muito o ato de compra.

Antes de nos aprofundarmos no asssunto, confira o que você encontrará neste artigo:

1. Bots de compras;

2. Adquirindo as credenciais para utilizar a API de Verificação (2FA);

    2.1. Verificação (2FA);

    2.2. Como funcionam as chamadas para esta API;

3. Criando a aplicação Node.js

Bots de Compras

O uso de bots na realização de compras é uma prática com pelo menos 10 anos nos EUA, mas ficou ainda mais evidente no final de 2020 com o lançamento Playstation 5 e o XBox Series X. Na época os consumidores se deparam com um obstáculo para adquirir o produto, já que os estoques ficaram vazios rapidamente. 

Pelo produto em questão, não havia dúvidas de que a busca seria alta, mas os clientes tinham pouquíssimas chances de adquirir um PS5. Logo depois, ficou claro que o resultado não se devia a uma ação meramente humana. O grande responsável por acabar com os estoques foram bots de compras. 

Os bots cambistas visam produtos de alta demanda, focando em sites de grandes varejistas. Só na rede Walmart, os robôs tentaram comprar 20 milhões de vezes nos primeiros 30 minutos de venda do PS5. No início da pandemia os produtos-alvo eram papel higiênico e desinfetantes, itens muito procurados. 

Tais bots foram desenvolvidos para fazer uma rápida transação dos produtos, ou seja, comprá-los antes de todo mundo, especialmente antes dos consumidores humanos que estavam atrás do produto. Para assim, tais produtos serem revendidos a um preço bem maior. 

É evidente o impacto que bots cambistas tem no mercado, principalmente no bolso do consumidor. Dessa forma, cada vez mais varejistas tem buscado medidas de segurança para inibir essa prática. Uma ferramenta muito versátil e útil em termos de segurança, atuando contra fraude e detecção de bots é a conhecida Autenticação de 2 Fatores (2FA).

Tutorial

Neste tutorial, vamos aprender a construir uma aplicação Node.js do zero, com um sistema de login com autenticação de 2 fatores, podendo ser tanto por SMS ou por Voz, utilizando a API da Zenvia.

Criaremos uma loja fictícia chamada Prensa Store, onde nesta aplicação, teremos a tela de finalização de uma hipotética compra feita nessa loja, onde para concluí-la, será necessário fazer uma checagem via celular para evitar bots de compras.

O projeto final criado neste tutorial está disponível no nosso repositório do github.

Adquirindo as credenciais para utilizar a API de Verificação (2FA)

Para consumir a API da Zenvia e todos os seus recursos, acesse aqui e clique em “CRIAR CONTA”. Na sequência, é preciso confirmar o número de celular e o endereço de e-mail para ter acesso ao painel. Após confirmar suas informações é possível acessar o painel e adquirir o Access Token para se autenticar nas requisições à API através dos headers da requisição, você precisará desta informação mais tarde.

Iremos utilizar o recurso Verificação (2FA –  Two Factor Authentication) e as informações sobre os endpoints da API estão disponíveis na documentação.

Verificação (2FA)

A funcionalidade de Verificação (2FA –  Two Factor Authentication) da API de voz da Zenvia gera um PIN de 4 a 10 dígitos, com expiração de 1 hora e permite enviar este PIN por meio de SMS ou torpedo de voz. Assim, o usuário pode nos enviar este código numérico e retornamos para a API para realizarmos a verificação deste PIN para saber se ele está correto.

O acesso à API está disponível em Node.js através de uma dependência chamada totalvoice-node, porém ela não tem suporte a typescript e para fins didáticos e para que este tutorial possa ajudar as pessoas que não utilizam Node.js, vamos fazer as chamadas através de requisições http (Hypertext Transfer Protocol).

Como funcionam as chamadas para esta API

A API recebe as requisições através do URL (Uniform Resource Locator) base https://voice-app.zenvia.com e vamos utilizar o endpoint /verificacao para acessar o recurso Verificação (2FA).

Para que a API reconheça o que queremos e quem está solicitando, precisamos enviar uma requisição POST com os seguintes headers:

Content-Type: application/json
access-token: <SEU_ACCESS_TOKEN_DISPONÍVEL_NO_PAINEL>

E um JSON no corpo da requisição neste modelo:

{
"numero_destino": "11999999999",
"nome_produto": "Prensa Store",
"tts": true
}

Por padrão, a API irá enviar um SMS, caso queira que o PIN seja enviado via torpedo de voz, envie o campo tts, como valor verdadeiro, assim como no exemplo acima.

Você também pode alterar o tamanho do PIN de 4 até 10 dígitos, através do campo “tamanho” devendo ser passado como string entre aspas (ex: “tamanho”: “5”).

A API então retorna um ID da transação realizada:    

{
 "status": 200,
 "sucesso": true,
 "motivo": 0,
 "mensagem": "dados retornados com sucesso",
 "dados": {
  "id": 0123456789
 }
}                           

Esta chamada então irá gerar um PIN e enviar ao número mandado para a API.

Após o usuário enviar este PIN de volta para nós, podemos checar com a API se este PIN é válido, enviando como parâmetros de consulta na requisição GET ao URL da chamada o ID retornado pela API e PIN enviado pelo usuário, conforme o exemplo: https://voice-app.zenvia.com/verificacao/?id=0123456789&pin=1234

São enviados os headers nesta requisição da mesma forma da outra.

Retorno:                      

{
 "status": 200,
 "sucesso": true,
 "motivo": 0,
 "mensagem": "dados retornados com sucesso",
 "dados": {
  "resultado": "valido"
 }
}        

Criando a aplicação Node.js

1- Instale o Node.js no seu computador (de preferência a versão LTS);

1.1- opcional – Instale o yarn. O Node.js possui um gerenciador de pacotes padrão chamado npm (Node Package Manager), porém existe um gerenciador diferente chamado yarn. No restante deste artigo, haverão as opções de instalação para ambos gerenciadores de pacote como no passo a seguir.

2- Inicie o projeto criando o package.json com um dos seguintes comandos no terminal (caso tenha instalado o yarn, sempre utilize apenas o segundo comando):

npm init -y
yarn init -y

3- Instale o ts-nodets-node-dev e o typescript como dependências de desenvolvimento:

npm i ts-node ts-node-dev typescript -D

yarn add ts-node ts-node-dev typescript -D

3.1 – Instale o arquivo de configuração do typescript:

npx typescript --init

4- Abra o arquivo package.json e insira o script para iniciar o servidor assim como na área grifada em cinza:

{
  "name": "zenvia-sms-voice-2fa",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "ts-node-dev --transpile-only --ignore-watch node_modules --respawn src/server.ts"
  },
  "devDependencies": {
    "ts-node": "^9.1.1",
    "ts-node-d": "^1.1.1",
    "typescript": "^4.1.3"
  }
}

4.1- Sempre que você quiser executar a aplicação basta executar no terminal:

npm run dev

yarn dev

5- Instale o expressexpress-session, body-parser, ejs e o dotenv para criarmos nossa API:

npm i express express-session body-parser ejs dotenv && npm i --save-dev @types/express @types/express-session @types/ejs

yarn add express express-session body-parser ejs dotenv && yarn add @types/express @types/express-session @types/ejs -D

express: faz o gerenciamento do nosso servidor e das rotas presentes nele;

express-session: permite o uso de sessão no servidor, para que sejam armazenadas variáveis temporariamente;

body-parser: melhora o manuseio das informações enviadas nas requisições;

ejs: permite renderizar páginas estáticas com o uso de variáveis vindas do servidor;

dotenv: permite a utilização de variáveis de ambiente.

6- Instale o Axios para realizarmos as requisições http:

npm i axios
export const api = axios.create({
    baseURL: 'https://voice-app.zenvia.com',
})

axios: facilita o envio de requisições http (Hypertext Transfer Protocol).

7- Vamos começar criando o arquivo de configuração do axios para fazer requisições. Crie src/config/api.ts:

import axios from 'axios'

export const api = axios.create({
    baseURL: 'https://voice-app.zenvia.com',
})

8- Crie um arquivo chamado .env, nele conterá seu token de acesso em uma variável de ambiente, coloque seu token logo após o sinal de =:

VOICE_TOKEN=

Lembra daquela tela onde copiamos o token da API? É agora que precisamos dele. Se você já rodou a aplicação, será necessário reiniciá-la para carregar essa nova variável de ambiente.

9- Vamos criar nosso arquivo src/server.ts e criarmos nossa API:

:import bodyParser from 'body-parser'
import express from 'express'
import session from 'express-session'
import ejs from 'ejs'

import routes from './routes'

// Inicializa o express e define uma porta
const app = express()

// Configuração do ejs
app.engine('html', ejs.renderFile) // ejs
app.set('view engine', 'html') // ejs
app.set('views', __dirname + '/html') // ejs

app.use(session({ secret: 'shhh' })) // Inicialização do express-session

app.use(bodyParser.urlencoded({ extended: true })) // Indica o uso do body-parser

app.use(routes) // Indica para o express usar o as rotas do arquivo routes

// Indica as rotas estáticas
app.use('/assets', express.static(__dirname + '/html/assets'))
app.use('/css', express.static(__dirname + '/html/css'))

// Indica para o express escutar a porta 1000
app.listen(1000, () => console.log(`Server running on port 1000`))

10- Crie o arquivo de rotas src/routes.ts:

import express, { Request, Response } from 'express'

const routes = express.Router() // Inicializa o router do express

// Rota para exibir o index.html
routes.get('/', (request: Request, response: Response) => {
 response.render('index')
}) 

export default routes

Com isso, definimos a rota que irá mostrar o nosso index.html

11- Vamos criar a tela de finalização da nossa compra fictícia no arquivo src/html/index.html:

<!DOCTYPE html>
<html lang="pt-br">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Cabin&display=swap" rel="stylesheet">

  <link rel="stylesheet" href="./css/styles.css">

  <title>Fechar pedido</title>
</head>

<body>
  <header>
    <h1 class="brand">Prensa Store</h1>
    <p style="align-self: flex-end;">Olá, usuário</p>
  </header>
  <main>
    <div class="content">
      <h1>Revisar compra</h1>
      <div class="purchase">
        <img src="http://localhost:1000/assets/prensa-mug.jpg" alt="Caneca Prensa">
        <div class="details">
          <h1>Caneca Prensa</h1>
          <p>Preço: R$29,99</p>
          <br />
          <p>Linda caneca da Prensa, minimalista <br /> e ótima para beber aquele cafézinho<br /> enquanto faz sua leitura diária.</p>
        </div>
      </div>
      <h1>Finalizar compra</h1>

      <form action="http://localhost:1000/verificacao" method="POST">
        <p>
          <label for="#numero_destino">Número de celular:</label>
          <input id="numero_destino" type="text" name="numero_destino" placeholder="11987654321" minlength="11" maxlength="11" required>
        </p>
        <p>
          <label for="#tts">Método de Envio do código:</label>
          <select id="tts" name="tts" required>
            <option value="false">SMS</option>
            <option value="true">Voz</option>
          </select>
        </p>

        <button type="submit">Enviar código de Verificação</button>
      </form>
    </div>
  </main>
</body>
</html>

12- Essa tela depende de um arquivo de estilização. Crie-o src/html/css/styles.css:

* {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  font: inherit;
  vertical-align: baseline;
}
body {
  line-height: 1;
}

header {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  padding: 25px 50px;
  box-shadow: #000000 0px 2px 2px -2px;
}

h1, p {
  font-family: 'Cabin', sans-serif !important;
  color: #505b64 !important;
}

h1 {
  font-size: 1.85rem;
  color: #222 !important;
}

h1.brand {
  font-family: Georgia, Times, times new roman, serif !important;
  font-stretch: expanded;
  font-weight: 700;
  letter-spacing: 0;
  padding: 0;
  margin: 0;
  line-height: 1;
}

main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  margin-top: 50px;
}

div.content {
  display: flex;
  flex-direction: column;
}

div.purchase {
  display: flex;
  flex-direction: row;
  align-items: center;
  border: 1px #acacac solid;
  padding: 25px 40px;
  margin: 30px 0;
}


div.purchase img {
  width: 200px;
}

div.purchase div {
  margin: 0 20px;
}


div.purchase div.purchase-details {
  display: flex;
  flex-direction: column;
  align-items: center;
}

form {
  margin: 30px auto;
  width: 450px;
}

form p {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}


form label {
  align-self: center;
  font-size: 1.2em;
}

input, select {
  background-color: #f8f9ff;
  padding: 7px 15px;
  font-size: 1.2em;
  border-radius: 5px;
  border: 1px #acacac7c solid;
}

button {
  background-image: linear-gradient(to right, #232526 0%, #414345 51%, #232526 100%);
  margin: 10px auto;
  padding: 15px 45px;
  font-family: 'Cabin', sans-serif;
  text-align: center;
  text-transform: uppercase;
  transition: 0.5s;
  background-size: 200% auto;
  color: white;
  box-shadow: 0 0 20px #eee;
  border-radius: 10px;
  display: block;
  cursor: pointer;
}

button:hover {
  background-position: right center; /* change the direction of the change here */
  color: #fff;
  text-decoration: none;
}

13- Se sua aplicação já está rodando, acesse o URL http://localhost:1000/ e veja o resultado da nossa tela:

Ela possui um formulário com um input para ser inserido o número de telefone no qual deverá ser enviado o código de verificação e um select, para que selecione se deseja que o envia seja feito através de SMS ou torpedo de voz.

Estas informações são enviadas para a rota /verificacao pelo método POST.

14- Crie a rota /verificacao no arquivo src/routes.ts:

// @ts-nocheck
import express, { Request, Response } from 'express'

import { checkVerificationCode } from './UseCases/CheckVerificationCode'

const routes = express.Router() // Inicializa o router do express

// Rota para exibir o index.html
routes.get('/', (request: Request, response: Response) => {
 response.render('index')
})

// Rota para receber as informações do form, realizar a requição à API e exibição da tela para o envio do código
routes.post('/verificacao', async (request: Request, response: Response) => {
 const { numero_destino, tts } = request.body // Armazena as informações do form em variáveis

 // Verifica se elas foram realmente passadas na requisição
 if (numero_destino && tts) {
  // Chama a nossa classe que faz a chamada à API e armazena o ID retornado pela chamada
  const returnID = await sendVerificationCode.execute(
   numero_destino,
   tts === 'false' ? false : tts === 'true' && true
  )

  // Verifica se foi feita a chamada com sucesso
  if (returnID !== '') {
   request.session.verificacaoId = returnID // Armazena o ID em uma variável de sessão
  }
 }

 response.render('verificacao') // Renderiza o arquivo verificacao.html com o ejs
})
  
export default routes

15- Crie o arquivo src/UseCases/SendVerificationCode/index.ts que conterá uma classe responsável por realizar a chamada ao recurso /verificacao à API da Zenvia:

import { config } from 'dotenv' // Importação do config do dotenv
import { api } from '../../config/api' // Importação da api configurada no arquivo do axios

config() // Inicialização do dotenv para usarmos variáveis de ambiente

class SendVerificationCode {
 async execute(numero_destino: string, tts: boolean) {
  try {
   // Tente realizar a chamada
   const response = await api.post(
    '/verificacao',
    {
     numero_destino,
     nome_produto: 'Prensa Store',
     tamanho: '4',
     tts,
    },
    {
     headers: {
      'Content-Type': 'application/json',
      'access-token': `${process.env.VOICE_TOKEN}`,
     },
    }
   )

   // Armazena o campo sucesso da resposta da API de voz
   const {
    sucesso,
    dados: { id },
   } = response.data

   // Verifica se o sucesso é verdadeiro
   if (sucesso === true) {
    // Se sim, exibe status de sucesso no console da aplicação
    console.log('Sucesso! A chamada está sendo realizada.')

    // Retorna o id da transação
    return id
   } else {
    // Se não, exibe status de erro no console da aplicação
    console.log('Erro na chamada.')

    // Retorna string vazia
    return ''
   }
  } catch (error) {
   // Caso não seja possível chamar a API de voz, exibe um status de erro no console
   console.log('Error:', error)

   // Retorna false
   return false
  }
 }
}

// Exportação da insância da classe
export const sendVerificationCode = new SendVerificationCode()   

Após o envio do formulário para a rota /verificacao, nossa aplicação irá chamar a função execute daclasse criada no arquivo acima que fará a chamada à API da Zenvia a qual se encarregará de gerar um PIN e enviar via SMS ou torpedo de voz dependendo do que o usuário colocou no formulário.

16- Agora que fizemos o funcionamento do envio do código de verificação, vamos criar a tela em que o usuário enviará o código recebido por ele src/html/verificacao.html:

<!DOCTYPE html>
<html lang="pt-br">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Cabin&display=swap" rel="stylesheet">

  <link rel="stylesheet" href="./css/styles.css">

  <title>Fechar pedido</title>
</head>

<body>
  <header>
    <h1 class="brand">Prensa Store</h1>
    <p style="align-self: flex-end;">Olá, usuário</p>
  </header>
  <main>
    <div class="content">
      <h1>Verificação</h1>
      <br />
      <p>Enviamos um código de verificação ao seu número de telefone. <br /> Insira-o no campo abaixo para verificarmos.
      </p>

      <form action="http://localhost:1000/verifica" method="POST">
        <input type="text" name="codigo" minlength="4" maxlength="10" placeholder="1234"
        <input type="submit" value="Verificar" style="cursor: pointer">
      </form>
    </div>
  </main>
</body>

</html>

Após a aplicação enviar a chamada, ela irá renderizar a tela que acabamos de criar.

17-  Crie a rota POST /verifica no arquivo src/routes.ts:

// @ts-nocheck
import express, { Request, Response } from 'express'

import { checkVerificationCode } from './UseCases/CheckVerificationCode'
import { sendVerificationCode } from './UseCases/SendVerificationCode'

const routes = express.Router() // Inicializa o router do express

// Rota para exibir o index.html
routes.get('/', (request: Request, response: Response) => {
 response.render('index')
})

// Rota para receber as informações do form, realizar a requição à API e exibição da tela para o envio do código
routes.post('/verificacao', async (request: Request, response: Response) => {
 const { numero_destino, tts } = request.body // Armazena as informações do form em variáveis

 // Verifica se elas foram realmente passadas na requisição
 if (numero_destino && tts) {
  // Chama a nossa classe que faz a chamada à API e armazena o ID retornado pela chamada
  const returnID = await sendVerificationCode.execute(
   numero_destino,
   tts === 'false' ? false : tts === 'true' && true
  )

  // Verifica se foi feita a chamada com sucesso
  if (returnID !== '') {
   request.session.verificacaoId = returnID // Armazena o ID em uma variável de sessão
  }
 }

 response.render('verificacao') // Renderiza o arquivo verificacao.html com o ejs
})

// Rota para receber o código enviado pelo usuário, realizar a requição à API e exibição da tela do resultado
routes.post('/verifica', async (request: Request, response: Response) => {
 const { codigo } = request.body // Armazena o código em uma variável

 // Verifica se o código foi passado
 if (codigo) {
  // Chama a nossa classe que faz a chamada à API e armazena o resultado retornado pela chamada
  // É passado o ID que armazenamos na seção e o código enviado pelo usuário
  const validade = await checkVerificationCode.execute(
   request.session.verificacaoId,
   codigo
  )

  // Verifica se o retorno da API foi igual a 'valido'
  if (validade === 'valido') {
   // Se sim, renderiza a tela resultado.html e envia a variável validade como 'concluída com sucesso!'
   response.render('resultado', {
    validade: 'concluída com sucesso!',
   })
  } else {
   // Se não, renderiza a tela resultado.html e envia a variável validade como 'cancelada! Seu código de verificação está inválido'
   response.render('resultado', {
    validade: 'cancelada! Seu código de verificação está inválido',
   })
  }
 }
})

export default routes    

18- Crie o arquivo src/UseCases/CheckVerificationCode/index.ts que conterá uma classe responsável por realizar a chamada ao recurso /verificacao/?id=<id>?pin=<pin> à API da Zenvia:

import { config } from 'dotenv'
import { api } from '../../config/api'

config()

class CheckVerificationCode {
 async execute(id: string, codigo: string) {
  try {
   // Tente realizar a chamada
   const response: any = await api.get(`/verificacao/?id=${id}&pin=${codigo}`, {
    headers: {
     'Content-Type': 'application/json',
     'access-token': `${process.env.VOICE_TOKEN}`,
    },
   })

   // Armazena o campo sucesso da resposta da API de voz
   const {
    dados: { resultado },
   } = response.data

   // Retorna o resultado da verificação
   return resultado
  } catch (error) {
   // Caso não seja possível chamar a API de voz, exibe um status de erro no console
   console.log('Error:', error)

   // Retorna false
   return false
  }
 }
}

export const checkVerificationCode = new CheckVerificationCode()

Após o envio do código (PIN) para a rota /verifica, nossa aplicação irá chamar a função execute daclasse criada no arquivo acima que fará a chamada à API da Zenvia a qual se encarregará de checar o PIN e retornar se o PIN é valido ou não.

19- Agora que fizemos o funcionamento do envio do código de verificação, vamos criar a telaque mostra se o código é valido ou não src/html/resultado.html:

<!DOCTYPE html>
<html lang="pt-br">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">

  <link rel="preconnect" href="https://fonts.gstatic.com">
  <link href="https://fonts.googleapis.com/css2?family=Cabin&display=swap" rel="stylesheet">

  <link rel="stylesheet" href="./css/styles.css">

  <title>Resultado</title>
</head>

<body>
  <header>
    <h1 class="brand">Prensa Store</h1>
    <p style="align-self: flex-end;">Olá, usuário</p>
  </header>
  <main>
    <div class="content">
      <h1>Sua compra foi <%= validade %>
      </h1>

      <br />
    </div>
  </main>
</body>

</html>

Em caso de código válido:

Em caso inválido:

Pronto! nossa aplicação está pronta.

Além de ser utilizado contra bots de compras, a autenticação de dois fatores (2FA) também pode ser usada na geração de tokens dinâmicos, ou seja, a cada nova autenticação, por meio da 2FA, haverá uma geração de um token alfanumérico único, garantindo mais proteção do usuário.

Conheça mais  o módulo de autenticação de dois fatores da Zenvia

Categorias:
Escrito por

Vitor Pereira

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Leia também

Fique por dentro e confira as nossas dicas sobre o mercado mobile e interação digital.