Uwierzytelnianie Telegram WebApp w JavaScript

Dec 09 2022
Dzisiaj opowiem o skomplikowanym mechanizmie uwierzytelniania w Telegram WebApp Bot. Co to jest bot Telegram WebApp? To tylko możliwość uruchomienia WebView z twoją witryną w Telegramie.

Dzisiaj opowiem o skomplikowanym mechanizmie uwierzytelniania w Telegram WebApp Bot. Co to jest bot Telegram WebApp? To tylko możliwość uruchomienia WebView z twoją witryną w Telegramie. Możesz przeczytać więcej tutaj .

Dlaczego potrzebujemy tutaj autoryzacji?

Twoja witryna powinna być otwierana tylko w Telegram WebView. A co jeśli ktoś zrobiłby to w przeglądarce? „Hakerzy” mogą wykorzystywać fałszywe dane, identyfikatory i tak dalej. Musimy chronić nasze API przed osobami spoza Telegramu.

Jak?

Telegram wykorzystuje HMAC (kod uwierzytelniania wiadomości oparty na mieszaniu). Jeśli więc zainicjujesz Telegram SDK w swojej witrynie, będziesz mógł użyć tych danych do identyfikacji użytkownika. Przejdźmy do tworzenia mechanizmu autoryzacji krok po kroku.

Krok 1: przekazywanie danych przez żądania

Telegram SDK konfiguruje dane użytkownika w globalnym zasięgu Twojej aplikacji. Są tam dane o użytkownikach, ich smartfonach, motywach kolorystycznych i nie tylko. Możesz go znaleźć tutaj:

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;

Krok 2: tworzenie oprogramowania pośredniczącego Auth

W swoim projekcie używam Nest.js, ale sposób tworzenia oprogramowania pośredniczącego jest prawie taki sam w Express.js i Nest.js

Po pierwsze, powinniśmy stworzyć oprogramowanie pośredniczące za pomocą kilku linijek kodu:

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();
    }
}

Krok 3: analizowanie pliku initData

Opiszę ten proces, a potem pokażę kod.

  1. Musimy przeanalizować łańcuch initData
  2. Weź pole skrótu z tego ciągu i zachowaj je na przyszłość
  3. Posortuj pozostałe pola według kolejności alfabetycznej
  4. Połącz te pola za pomocą łamacza wierszy (\n). Czemu? Właśnie dlatego! Telegram chce!

Spójrzmy na kod:

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'),
    },
  };
}

To już ostatni rozdział naszej podróży. Musimy przeanalizować initData przy użyciu funkcji z poprzedniego kroku i odrobiny kryptografii.

Powinniśmy podążać tą drogą:

  1. Napisz funkcję do kodowania wiadomości za pomocą algorytmu sh256 i klucza
  2. Przeanalizuj ciąg przy użyciu funkcji z poprzedniego kroku
  3. Utwórz tajny klucz, kodując Telegram Bot Token za pomocą klucza „WebAppData”.
  4. Utwórz skrót sprawdzania poprawności, kodując dataCheckString z poprzedniego pnia za pomocą tajnego klucza
  5. Porównaj hash sprawdzania poprawności z hashem z 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. Możesz dodać buforowanie, ponieważ kryptografia jest dość skomplikowaną rzeczą dla procesora, więc możesz użyć Redis lub nawet pamięci podręcznej w pamięci, aby na przykład zachować ciąg initData jak klucz i JSON userData jako wartość
  2. Możesz wygenerować swój własny token JWT raz po sprawdzeniu poprawności initData i możesz ustawić go w plikach cookie. Myślę, że jest to potężniejszy sposób tworzenia uwierzytelnienia.