DEV Community

Serhii
Serhii

Posted on Originally published at botservice.biz

Validate Telegram Login Widget Auth Payloads in PHP and Yii2

Integrating the official Telegram Login Widget allows users to authenticate on a web application using their Telegram account. When a user authorizes via the widget, Telegram passes authentication parameters (such as id, first_name, username, auth_date, and hash) back to your specified redirect URL or JavaScript callback.

In this tutorial, we will construct a backend validation mechanism in PHP and implement it inside a Yii2 application controller. We will compute the expected HMAC-SHA-256 signature, execute a timing-safe hash comparison, check signature freshness, and bind the incoming telegram_id to your user database.

What this guide covers:

  • Constructing the data_check_string according to Telegram Login Widget specifications.
  • Generating the secret key via sha256(bot_token).
  • Validating payload signatures with hash_equals().
  • Binding verified telegram_id records in Yii2.

What this guide does not cover:

  • Setting up frontend JavaScript widgets or HTML embed code.
  • Telegram Mini App initData verification (which uses a different secret key derivation process).

The Telegram Verification Algorithm

Unlike Mini App initData (which uses the literal string "WebAppData" as the HMAC key), the standard Telegram Login Widget derives its secret key by taking the binary SHA-256 hash of your bot token.

The algorithm steps are:

  1. Extract the hash parameter from the incoming payload.
  2. Filter out hash from the remaining keys.
  3. Sort the remaining key-value pairs alphabetically by key name.
  4. Format each pair as key=value and join them with newline characters (\n).
  5. Generate the secret key: hash('sha256', $botToken, true).
  6. Compute hash_hmac('sha256', $dataCheckString, $secretKey).
  7. Verify the output against the received hash using hash_equals().
  8. Verify that auth_date is within an acceptable threshold (e.g., 86,400 seconds).

Here is a reusable PHP service class implementing this verification logic:

<?php

declare(strict_types=1);

namespace app\components;

final class TelegramLoginValidator
{
    public function isValid(array $authData, string $botToken, int $maxAgeSeconds = 86400): bool
    {
        if (!isset($authData['hash'], $authData['auth_date'])) {
            return false;
        }

        $checkHash = (string)$authData['hash'];
        unset($authData['hash']);

        // Check payload freshness
        $authDate = (int)$authData['auth_date'];
        if ((time() - $authDate) > $maxAgeSeconds) {
            return false;
        }

        // Filter non-string values and build key=value list
        $dataCheckArr = [];
        foreach ($authData as $key => $value) {
            if (is_scalar($value)) {
                $dataCheckArr[] = $key . '=' . $value;
            }
        }

        // Sort keys alphabetically
        sort($dataCheckArr, SORT_STRING);
        $dataCheckString = implode("\n", $dataCheckArr);

        // Secret key is the raw binary SHA-256 hash of the bot token
        $secretKey = hash('sha256', $botToken, true);
        $hash = hash_hmac('sha256', $dataCheckString, $secretKey);

        return hash_equals($hash, $checkHash);
    }
}
Enter fullscreen mode Exit fullscreen mode

Integrating with a Yii2 Controller

Next, wire this validator into a Yii2 controller action. We fetch the bot token from environment variables or Yii::$app->params, run the validation check, and either bind the incoming telegram_id to an existing authenticated user session or create a new user account.

Ensure your User database table has an indexed telegram_id column (BIGINT, nullable, unique).

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use app\models\User;
use app\components\TelegramLoginValidator;

class AuthController extends Controller
{
    public function actionTelegramCallback()
    {
        $requestParams = Yii::$app->request->get();
        $botToken = (string)(getenv('TELEGRAM_BOT_TOKEN') ?: (Yii::$app->params['telegramBotToken'] ?? ''));

        if (empty($botToken)) {
            throw new \LogicException('Telegram bot token is not configured.');
        }

        $validator = new TelegramLoginValidator();
        if (!$validator->isValid($requestParams, $botToken)) {
            throw new BadRequestHttpException('Invalid or expired Telegram authentication signature.');
        }

        $telegramId = (int)$requestParams['id'];
        $username = $requestParams['username'] ?? null;

        // Check if a user with this telegram_id already exists
        $user = User::findOne(['telegram_id' => $telegramId]);

        if (!$user) {
            // If currently logged in, link telegram_id to current profile
            if (!Yii::$app->user->isGuest) {
                /** @var User $user */
                $user = Yii::$app->user->identity;
                $user->telegram_id = $telegramId;
                $user->save(false, ['telegram_id']);

                Yii::$app->session->setFlash('success', 'Telegram account connected successfully.');
                return $this->redirect(['/user/profile']);
            }

            // Register a new user account bound to this Telegram identity
            $user = new User();
            $user->telegram_id = $telegramId;
            $user->username = $username ?: 'tg_' . $telegramId;
            $user->generateAuthKey(); // Sets random auth_key for session security

            if (!$user->save()) {
                throw new \RuntimeException('Failed to register user account.');
            }
        }

        // Log the user into the Yii2 application session
        Yii::$app->user->login($user, 3600 * 24 * 30);

        return $this->redirect(['/site/dashboard']);
    }
}
Enter fullscreen mode Exit fullscreen mode

Production Considerations

  1. Environment Variable Configuration: Never hardcode bot tokens into code. Store TELEGRAM_BOT_TOKEN in system environment variables or safely loaded .env files using getenv().
  2. Replay Protection: The auth_date check enforces a max window (e.g., 24 hours). If your application handles high-value transactions, consider storing consumed hash strings in Redis for the duration of the expiration window to prevent replay attacks entirely.
  3. Account Linking Logic: If a user is already authenticated via traditional credentials (email/password), prompt them to link their Telegram account from inside their profile page rather than overwriting existing records automatically.

Need custom bot architectures or specialized backend integrations built for production workloads? BotCreator — studio that ships Telegram bots / Mini Apps.

Top comments (0)