Telegram. WebApp. Проверка подлинности пользователя. Не сходится хэш

Есть телеграмм-бот с веб-хуком кинутым на веб-сервер джанго. Есть веб-приложение телеграмма, которое при инициации вызывает вьюху джанги и возвращает шаблон. В этом шаблоне, согласно документации телеграма, в head вписан скрипт

<script src="https://telegram.org/js/telegram-web-app.js"></script>

И далее в body мне нужно проверить подлинность пользователя телеграмма. Я следовал документации о проверке данных и советам из инета и в шаблоне прописал следующий скрипт:

<script>
        const WebApp = window.Telegram.WebApp;
        let initDataURLSP = new URLSearchParams(WebApp.initData);
        var hash = initDataURLSP.get('hash');

        initDataURLSP.delete('hash');
        initDataURLSP.sort();
        var checkDataString = initDataURLSP.toString().replaceAll('&', '\n');

        let data_send = {
            'hash': hash,
            'checkDataString': checkDataString,
        };
        fetch(
                'https://address/backend/check_hash/',
                {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json; charset=utf-8'
                    },
                    body: JSON.stringify(data_send),
                }
        );
</script>

На бэке это принимается вьюхой:


    @csrf_exempt
    def check_hash(request: HttpRequest):
        request_dict = json.loads(request.body)
        data_check_string = request_dict['checkDataString']
        tg_hash = request_dict['hash']
    
        import hashlib, hmac, os
        from dotenv import load_dotenv
    
        load_dotenv()
    
        token = os.getenv('TOKEN_DJEM_DEV_BOT')
    
        secret_key = hmac.new(
            key="WebAppData".encode(),
            msg=token.encode(),
            digestmod=hashlib.sha256
        ).digest()
        local_hash = hmac.new(
            key=secret_key,
            msg=data_check_string.encode(),
            digestmod=hashlib.sha256
        ).hexdigest()
        check = tg_hash == local_hash
        ... # Дальше идёт возврат хттп-ответа.

Так вот, check у меня приходит False и tg_hash != local_hash

Пробовал присылать неизменённую initData на бэк:

const WebApp = window.Telegram.WebApp;
let data_send = {
    'checkDataString': WebApp.initData,
};
fetch(
        'https://address/backend/poll_data/',
        {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json; charset=utf-8'
            },
            body: JSON.stringify(data_send),
        }
);

и уже там ещё форматировать:


    data_check_string = request_dict['checkDataString']
    data_check_string_list: list = data_check_string.split('&')
    tg_hash = data_check_string_list.pop(-1)[5:]
    data_check_string_list.sort()
    data_check_string = '\n'.join(data_check_string_list)

Результат неотличим.

Как пройти проверку пользователя средствами python и js?


Ответы (2 шт):

Автор решения: HegoZz

Нашёл решение на просторах Интернета. Там код на пайтоне из библиотеки aiogram. Среди прочего есть функция, которая принимает токен бота и init_data (которую присылает веб-приложение с фронта) и возвращает булево значение, соответствующее подлинности пользователя:

import hashlib
import hmac
from operator import itemgetter
from urllib.parse import parse_qsl


def check_webapp_signature(token: str, init_data: str) -> bool:
    """
    Check incoming WebApp init data signature

    Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app

    :param token:
    :param init_data:
    :return:
    """
    try:
        parsed_data = dict(parse_qsl(init_data))
    except ValueError:
        # Init data is not a valid query string
        return False
    if "hash" not in parsed_data:
        # Hash is not present in init data
        return False

    hash_ = parsed_data.pop('hash')
    data_check_string = "\n".join(
        f"{k}={v}" for k, v in sorted(parsed_data.items(), key=itemgetter(0))
    )
    secret_key = hmac.new(
        key=b"WebAppData", msg=token.encode(), digestmod=hashlib.sha256
    )
    calculated_hash = hmac.new(
        key=secret_key.digest(), msg=data_check_string.encode(), digestmod=hashlib.sha256
    ).hexdigest()
    return calculated_hash == hash_

Пока не разобрался почему мой код не работал, а этот работает, но вопрос решён.

→ Ссылка
Автор решения: Roodewald

Я столкнулся с такой же проблемой. Исходя из кода python который ты приложил выше, я реализовал это на TS.

Надеюсь кому-то помогу в поисках.

import crypto from 'crypto';

const checkWebAppSignature = (token: string, initData: string): boolean => {
  /**
   * Проверяет валидность подписи данных от Telegram WebApp
   *
   * Источник:
     https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
   *
   * @param token Токен бота
   * @param initData Данные, полученные от WebApp
   * @returns true, если подпись валидна, иначе false
   */
  try {
    const parsedData = Object.fromEntries(new URLSearchParams(initData));
    if (!parsedData.hash) {
      // Хэш отсутствует в данных
      return false;
    }

    // Извлечение хэша
    const receivedHash = parsedData.hash;
    delete parsedData.hash;

    // Формируем строку для проверки подписи
    const dataCheckString = Object.keys(parsedData)
      .sort()
      .map((key) => `${key}=${parsedData[key]}`)
      .join('\n');

    // Генерируем секретный ключ
    const secretKey = crypto
      .createHmac('sha256', 'WebAppData')
      .update(token)
      .digest();

    // Вычисляем хэш
    const calculatedHash = crypto
      .createHmac('sha256', secretKey)
      .update(dataCheckString)
      .digest('hex');

    // Сравниваем полученный хэш и вычисленный
    return calculatedHash === receivedHash;
  } catch (error) {
    console.error('Ошибка при разборе initData:', error);
    return false;
  }
};

// Пример использования
const initData = 'query_id=... &user=first_name... и т.д. это нормально';
const token = '77....01:AAH....X';

console.log(checkWebAppSignature(token, initData));

Кратко расскажу логику валидации: Пользователь при подключении выдает свои данные с хешом. Если объединить эти данные, вместе с вашим botApiKey, вы сможете проверить все данные которые сообщил пользователь.

→ Ссылка