Verifying Telegram WebApp initData in PHP: Query String Parsing, HMAC-SHA-256, and auth_date Validation

When transmitting data from a Telegram Mini App (TWA) to the backend, it is critically important to ensure that the request was actually generated by Telegram and not spoofed by an attacker. Telegram provides the initData object as a query string containing user data, authorization time, and a cryptographic hash. If this data is processed without verifying the signature, an attacker can send an arbitrary user.id and gain access to other people's accounts or balances.

initData Structure and Authentication Algorithm

The initData parameter is a URL-encoded string formatted as query_id=...&user=...&auth_date=...&hash=.... To verify the signature on the backend, you must perform the following sequence of steps:

  1. Parse the query string into an associative array of parameters.
  2. Extract the hash parameter and remove it from the array of data to be verified.
  3. Sort the remaining key-value pairs alphabetically by key name.
  4. Construct the data_check_string string, where pairs are joined by a newline character \n in key=value format.
  5. Generate the секретный ключ (secret_key) by hashing the bot token using HMAC-SHA-256, using the constant string WebAppData as the key.
  6. Calculate the final HMAC-SHA-256 hash of data_check_string using secret_key.
  7. Compare the resulting hash with the original hash from the request in a timing-attack safe manner.
  8. Check the freshness of the auth_date timestamp.

Creating the HMAC-SHA-256 Secret Key and Signing Data

A feature of the Telegram Mini Apps cryptographic scheme is two-stage hashing. The bot token cannot be used directly as a key for signing data. First, an intermediate binary key is created, using the ASCII literal WebAppData as the control string.

In PHP, the hash_hmac function with the $as_binary = true argument returns raw binary data, which is then passed as the second argument when calculating the final HMAC. The resulting hash is output as a lowercase hex string.

<?php

declare(strict_types=1);

/**
* Валидация подписи initData из Telegram Mini App.
*
* @param string $initData Сырая query-строка от Telegram WebApp
* @param string $botToken Токен бота, полученный от @BotFather
* @param int $maxAgeSec Максимальное время жизни подписи в секундах
* @return array{is_valid: bool, data: array<string, mixed>, error: ?string}
*/
function validateTelegramInitData(string $initData, string $botToken, int $maxAgeSec = 86400): array
{
if (trim($initData) === '') {
return ['is_valid' => false, 'data' => [], 'error' => 'InitData is empty'];
}

// Парсинг query-строки без потери структуры JSON внутри параметров
$params = [];
parse_str($initData, $params);

if (!isset($params['hash']) || !is_string($params['hash'])) {
return ['is_valid' => false, 'data' => [], 'error' => 'Hash missing in payload'];
}

$receivedHash = $params['hash'];
unset($params['hash']);

// Сортировка параметров по ключам в алфавитном порядке
ksort($params);

$dataCheckArr = [];
foreach ($params as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);

// Этап 1: Вычисление secret_key = HMAC_SHA256("WebAppData", bot_token) в бинарном виде
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// Этап 2: Вычисление hash = HMAC_SHA256(secret_key, data_check_string) в hex-формате
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Timing-safe сравнение строк для исключения атаки по времени
if (!hash_equals($calculatedHash, $receivedHash)) {
return ['is_valid' => false, 'data' => [], 'error' => 'Invalid hash signature'];
}

// Проверка наличия и возраста auth_date
if (!isset($params['auth_date']) || !is_numeric($params['auth_date'])) {
return ['is_valid' => false, 'data' => [], 'error' => 'Auth date missing'];
}

$authDate = (int)$params['auth_date'];
if ((time() - $authDate) > $maxAgeSec) {
return ['is_valid' => false, 'data' => [], 'error' => 'InitData standard TTL expired'];
}

// Декодируем объект user из JSON, если он есть
if (isset($params['user']) && is_string($params['user'])) {
$decodedUser = json_decode($params['user'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$params['user'] = $decodedUser;
}
}

return ['is_valid' => true, 'data' => $params, 'error' => null];
}

Protection Against Timing Attacks Using hash_equals()

Standard operator string comparison (===) in PHP terminates on the very first mismatched byte. This allows an attacker to measure microsecond server response delays and brute-force a valid signature hash byte by byte (timing attack).

To eliminate this vulnerability, you must use the hash_equals() function. It performs comparison in constant time, regardless of which specific byte contains a mismatch, making client-side timing analysis impossible.

Controlling auth_date Lifetime and Preventing Replay Attacks

A successful HMAC signature check confirms that the data has not been modified. However, an authentic initData string can be intercepted and resent later (Replay Attack). To prevent the reuse of stale sessions, checking the auth_date field is mandatory.

The auth_date parameter contains the Unix timestamp of when the signature was created on the client. The standard validity window is 86400 seconds (24 hours). In critical operations, such as financial transactions or deducting bonus points, it is recommended to reduce the TTL to 300–600 seconds.

Backend Request Processing: Authorization Controller

Below is a practical example of an API endpoint that receives initData from the client, validates the signature, and returns an authorization response or an access error.

<?php

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

// Получение токена бота из окружения
$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server environment error']);
exit;
}

// Извлечение JSON-тела запроса
$inputRaw = file_get_contents('php://input');
$payload = json_decode($inputRaw ?: '', true);

if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Invalid JSON payload']);
exit;
}

$initData = $payload['init_data'] ?? null;

if (!is_string($initData) || $initData === '') {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Parameter init_data is required']);
exit;
}

// Валидация initData с TTL в 24 часа
$validationResult = validateTelegramInitData($initData, $botToken, 86400);

if (!$validationResult['is_valid']) {
http_response_code(403);
echo json_encode([
'ok' => false,
'error' => 'Authentication failed: ' . $validationResult['error']
]);
exit;
}

$userData = $validationResult['data']['user'] ?? null;

if (!$userData || !isset($userData['id'])) {
http_response_code(422);
echo json_encode(['ok' => false, 'error' => 'User context missing in initData']);
exit;
}

// Успешный ответ: пользователь верифицирован
echo json_encode([
'ok' => true,
'result' => [
'telegram_id' => $userData['id'],
'first_name' => $userData['first_name'] ?? '',
'username' => $userData['username'] ?? null,
'auth_date' => $validationResult['data']['auth_date']
]
]);

Common Errors When Parsing initData

  • Double URL Decoding: If the framework or web server has already decoded the query string, calling urldecode() again will break the structure of JSON fields (for example, strings inside user). Use the raw string received from the client.
  • Incorrect Sort Order: The ksort() sorting must be performed by keys after splitting the pairs. Element values should not be modified before signature verification.
  • Ignoring the HMAC Binary Flag: When calculating the intermediate secret_key, the output of hash_hmac must be binary (the argument $as_binary = true); otherwise, you will get a hex string and the final hash will not match.
  • Server Time Desynchronization: If the server's system clock is running slow or fast, the auth_date check may erroneously reject valid requests. Use NTP synchronization on the application server.

If you need professional integration of Telegram Mini Apps with a turnkey backend architecture, contact the specialists at BotCreator.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.