Autenticação do Telegram WebApp em JavaScript

Dec 09 2022
Hoje vou falar sobre o complicado mecanismo de autenticação no Telegram WebApp Bot. O que é o bot do Telegram WebApp? É apenas uma capacidade de executar o WebView com seu site dentro do Telegram.

Hoje vou falar sobre o complicado mecanismo de autenticação no Telegram WebApp Bot. O que é o bot do Telegram WebApp? É apenas uma capacidade de executar o WebView com seu site dentro do Telegram. Você pode ler mais aqui .

Por que precisamos de autenticação aqui?

Seu site deve ser aberto apenas no Telegram WebView. Então, e se alguém fizesse isso em um navegador? “Hackers” podem usar dados de usuários falsos, ids e assim por diante. Precisamos proteger nossa API de pessoas fora do Telegram.

Como?

O Telegram usa HMAC (código de autenticação de mensagem baseado em hash). Então, se você inicializar o SDK do Telegram em seu site, poderá usar esses dados para identificar o usuário. Vamos criar o mecanismo de autenticação passo a passo.

Etapa 1: passando dados por meio de solicitações

O Telegram SDK configura os dados do usuário no escopo global do seu aplicativo. Existem dados sobre usuários, seus smartphones, temas de cores e muito mais. Você pode encontrá-lo aqui:

window.Telegram.WebApp

const { initData } = window.Telegram.WebApp

auth_date=<auth_date>&query_id=<query_id>&user=<user>&hash=<hash>

axios.defaults.headers.common['Telegram-Data'] = window?.Telegram?.WebApp?.initData;

Etapa 2: criando o middleware Auth

Eu uso o Nest.js no meu projeto, mas a forma de criar middleware é quase a mesma no Express.js e no Nest.js

Primeiramente, devemos criar o middleware com algumas linhas de código:

export function telegramAuthMiddleware(req, res, next) {
    // take initData from headers
    const iniData = req.headers[
      'telegram-data'
      ];
    // use our helpers (see bellow) to validate string
    // and get user from it
    const user = checkAuthorization(iniData);

    // add uses to  the request "context" for the future
    if (user) {
      req.user = user;
      next();
    // or if the validation is failed response 401
    } else {
      res.writeHead(401, { 'content-type': 'application/json' });
      res.write('unauthorized');
      res.end();
    }
}

Etapa 3: analisando o initData

Vou descrever o processo e depois mostrarei o código.

  1. Precisamos analisar a string initData
  2. Pegue o campo hash dessa string e guarde-o para o futuro
  3. Classifique o restante dos campos por ordem alfabética
  4. Una esses campos usando o separador de linha (\n). Por quê? Só porque! Telegram quer!

Vejamos o código:

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);
  
  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');

  
  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}

Este é o último capítulo da nossa jornada. Precisamos analisar initData usando a função da etapa anterior e um pouco de criptografia.

Devemos seguir este caminho:

  1. Escreva uma função para codificar mensagem usando o algoritmo sh256 e uma chave
  2. Analisar string usando a função da etapa anterior
  3. Crie uma chave secreta codificando o Telegram Bot Token com a chave “WebAppData”
  4. Crie um hash de validação codificando dataCheckString do tronco anterior com uma chave secreta
  5. Compare o hash de validação com o hash de initData

const crypto = require('crypto')

const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

// encoding message with key
// we need two types of representation here: Buffer and Hex 
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

const crypto = require('crypto')
const WEB_APP_DATA_CONST = "WebAppData"
const TELEGRAM_BOT_TOKEN = "so secret token!!"

export function telegramAuthMiddleware(req, res, next) {
  // take initData from headers
  const iniData = req.headers[
    'telegram-data'
    ];
  // use our helpers (see bellow) to validate string
  // and get user from it
  const user = checkAuthorization(iniData);

  // add uses to  the request "context" for the future
  if (user) {
    req.user = user;
    next();
    // or if the validation is failed response 401
  } else {
    res.writeHead(401, { 'content-type': 'application/json' });
    res.write('unauthorized');
    res.end();
  }
}

function parseAuthString(iniData) {
  // parse string to get params
  const searchParams = new URLSearchParams(iniData);

  // take the hash and remove it from params list
  const hash = searchParams.get('hash');
  searchParams.delete('hash');

  // sort params
  const restKeys = Array.from(searchParams.entries());
  restKeys.sort(([aKey, aValue], [bKey, bValue]) => aKey.localeCompare(bKey));

  // and join it with \n
  const dataCheckString = restKeys.map(([n, v]) => `${n}=${v}`).join('\n');


  return {
    dataCheckString,
    hash,
    // get metaData from params
    metaData: {
      user: JSON.parse(searchParams.get('user')),
      auth_date: searchParams.get('auth_date'),
      query_id: searchParams.get('query_id'),
    },
  };
}


// encoding message with key
// we need two types of representation here: Buffer and Hex
function encodeHmac(message, key, repr=undefined) {
  return crypto.createHmac('sha256', key).update(message).digest(repr);
}

function checkAuthorization(iniData){
  // parsing the iniData sting
  const authTelegramData = parseAuthString(iniData);

  // creating the secret key and keep it as a Buffer (important!)
  const secretKey = encodeHmac(
    TELEGRAM_BOT_TOKEN,
    WEB_APP_DATA_CONST,
  );

  // creating the validation key (and transform it to HEX)
  const validationKey = encodeHmac(
    authTelegramData.dataCheckString,
    secretKey,
    'hex',
  );

  // the final step - comparing and returning
  if (validationKey === authTelegramData.hash) {
    return authTelegramData.metaData.user;
  }

  return null;
}

  1. Você pode adicionar cache porque a criptografia é uma coisa muito complicada para um processador, portanto, você pode usar Redis ou até mesmo in-memory-cache para manter a string initData como uma chave e userData JSON como um valor, por exemplo
  2. Você pode gerar seu próprio token JWT uma vez após validar o initData e pode configurá-lo em cookies. É uma maneira mais poderosa de criar autenticação, eu acho.