Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Stop Spoofed Telegram Logins by Verifying HMAC and Binding IDs in Yii2

Integrating the Telegram Login Widget into a Yii2 application is an excellent way to streamline user onboarding. Instead of forcing users to fill out long registration forms, verify email addresses, and create complex pa

Integrating the Telegram Login Widget into a Yii2 application is an excellent way to streamline user onboarding. Instead of forcing users to fill out long registration forms, verify email addresses, and create complex passwords, they can authenticate with a single click.

However, implementing this flow incorrectly introduces a critical security vulnerability: account takeover. The Telegram Login Widget operates on the client side, returning user data (such as id, first_name, username, and a cryptographic hash) back to your server via a redirect URL or a JavaScript callback.

If your server simply reads the incoming id parameter and logs the user in, any attacker can easily impersonate any user on your platform. An attacker only needs to know a target's Telegram ID (which is public information) and send a manual request to your callback endpoint with that ID. To prevent this, you must cryptographically verify the data signature on your server using your secret Telegram Bot Token.

This guide walks through setting up a secure verification pipeline in PHP and integrating it into a Yii2 application, covering database migrations, HMAC-SHA-256 verification, replay attack prevention, and account binding.

The Vulnerability: Why Client-Side Data is a Trap

When a user authenticates via the Telegram Login Widget, your callback URL receives a payload that looks like this:

https://example.com/auth/telegram-callback?id=12345678&first_name=John&username=johndoe&photo_url=https%3A%2F%2Ft.me%2Fi%2Fuserpic%2F...&auth_date=1700000000&hash=a1b2c3d4e5f6...

If your application code looks like this:

// DANGER: DO NOT DO THIS
$telegramId = Yii::$app->request->get('id');
$user = User::findOne(['telegram_id' => $telegramId]);
if ($user) {
    Yii::$app->user->login($user);
}

Your application is completely insecure. Anyone can modify the id parameter in their browser address bar to match your administrator's Telegram ID and gain full access to your system.

To secure this flow, Telegram signs the payload. The hash parameter is an HMAC-SHA-256 signature of the other query parameters, signed with a secret key derived from your Telegram Bot Token. Because only your server and Telegram know your Bot Token, a matching hash proves that the data was generated by Telegram and has not been tampered with.

Step 1: Prepare the Database Migration

Before writing the verification logic, your database must be ready to store Telegram user IDs. Telegram user IDs are currently within the 64-bit integer range. Storing them as a standard 32-bit signed integer will cause silent truncation or database errors once an ID exceeds 2,147,483,647.

Run the Yii2 migration generator:

php yii migrate/create add_telegram_id_to_user_table

Modify the generated migration file to add a bigint column and a unique index. The unique index is critical: it prevents multiple local accounts from linking to the same Telegram account, which could otherwise lead to database conflicts or security bypasses.

<?php

use yii\db\Migration;

class m231024_120000_add_telegram_id_to_user_table extends Migration
{
    public function safeUp()
    { 
        $this->addColumn('{{%user}}', 'telegram_id', $this->bigInteger()->unsigned()->null()->defaultValue(null));
        $this->createIndex('idx-user-telegram_id', '{{%user}}', 'telegram_id', true);
    }

    public function safeDown()
    { 
        $this->dropIndex('idx-user-telegram_id', '{{%user}}');
        $this->dropColumn('{{%user}}', 'telegram_id');
    }
}

Run the migration using the console command:

php yii migrate

Step 2: Implement the Cryptographic Verification Service

To verify the payload, we must reconstruct the data-check string that Telegram signed. The rules for constructing this string are strict:

  1. Extract all query parameters except hash.
  2. Sort the remaining parameters alphabetically by key.
  3. Format each key-value pair as key=value.
  4. Join these pairs with a newline character (\n).

Next, we derive our signing key. Telegram does not use the raw Bot Token directly as the HMAC key. Instead, it uses the SHA-256 hash of the raw Bot Token in binary format. Finally, we calculate the hex-encoded HMAC-SHA-256 of our data-check string using this derived key and compare it to the incoming hash parameter.

We must also enforce an expiration window using the auth_date parameter. If an attacker intercepts a valid, signed redirect URL, they could replay it days or weeks later to log in. Restricting the validity of the signature to a short window (e.g., 24 hours) mitigates this risk.

Here is a complete, reusable PHP service class for this verification:

<?php

namespace app\services;

use Yii;
use yii\base\InvalidArgumentException;

class TelegramAuthService
{
    private string $botToken;
    private int $allowedAge;

    /**
     * @param string $botToken Your secret Telegram Bot Token
     * @param int $allowedAge Maximum age of the authentication request in seconds (default: 24 hours)
     */
    public function __construct(string $botToken, int $allowedAge = 86400)
    { 
        if (empty($botToken)) {
            throw new InvalidArgumentException('Telegram Bot Token cannot be empty.');
        }
        $this->botToken = $botToken;
        $this->allowedAge = $allowedAge;
    }

    /**
     * Validates the incoming Telegram login payload.
     * 
     * @param array $params The query parameters received from the widget
     * @return bool True if the payload is authentic and fresh, false otherwise
     */
    public function validate(array $params): bool
    {
        if (!isset($params['hash']) || !isset($params['auth_date'])) {
            return false;
        }

        // 1. Prevent replay attacks by checking signature age
        $authDate = (int)$params['auth_date'];
        if ((time() - $authDate) > $this->allowedAge) {
            return false;
        }

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

        // 2. Sort parameters alphabetically by key
        ksort($params);

        // 3. Build the data-check string
        $dataCheckArr = [];
        foreach ($params as $key => $value) {
            $dataCheckArr[] = $key . '=' . $value;
        }
        $dataCheckString = implode("\n", $dataCheckArr);

        // 4. Derive the cryptographic key
        $secretKey = hash('sha256', $this->botToken, true);

        // 5. Calculate the expected hash
        $expectedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

        // 6. Compare hashes using a constant-time comparison function to prevent timing attacks
        return hash_equals($expectedHash, $receivedHash);
    }
}

Step 3: Integrate with Yii2 Controller

Now we will create the controller action to handle the callback. This action must handle two distinct business flows:

  1. Account Binding (Authenticated Users): If a user is already logged in via their standard username/password, they can visit their profile settings and click "Link Telegram". The controller should verify the payload and save their telegram_id to their existing record.
  2. Direct Authentication (Guests): If a guest visits the login page and clicks "Sign in with Telegram", the controller verifies the payload, searches for a user with that telegram_id, and logs them into the application.

Configure your bot token in your Yii2 configuration file (config/params.php or config/web.php):

return [
    'telegramBotToken' => getenv('TELEGRAM_BOT_TOKEN'),
];

Now, implement the AuthController action:

<?php

namespace app\controllers;

use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\Response;
use app\services\TelegramAuthService;
use app\models\User;

class AuthController extends Controller
{
    /**
     * Handles the redirect callback from the Telegram Login Widget.
     */
    public function actionTelegramCallback(): Response
    {
        $params = Yii::$app->request->get();
        $token = Yii::$app->params['telegramBotToken'] ?? null;

        if (!$token) {
            throw new BadRequestHttpException('Telegram authentication is not configured on the server.');
        }

        $authService = new TelegramAuthService($token);

        // Perform cryptographic verification
        if (!$authService->validate($params)) {
            Yii::$app->session->setFlash('error', 'Invalid or expired Telegram authentication signature.');
            return $this->redirect(['site/login']);
        }

        $telegramId = (int)$params['id'];

        // Case 1: User is already logged in, attempting to link their Telegram account
        if (!Yii::$app->user->isGuest) {
            /** @var User $currentUser */
            $currentUser = Yii::$app->user->identity;

            // Check if this Telegram ID is already linked to another account
            $existingBinding = User::findOne(['telegram_id' => $telegramId]);
            if ($existingBinding && $existingBinding->id !== $currentUser->id) {
                Yii::$app->session->setFlash('error', 'This Telegram account is already linked to another user.');
                return $this->redirect(['user/profile']);
            }

            $currentUser->telegram_id = $telegramId;
            if ($currentUser->save(false)) {
                Yii::$app->session->setFlash('success', 'Your Telegram account has been successfully linked.');
            } else {
                Yii::$app->session->setFlash('error', 'Failed to save your Telegram association.');
            }

            return $this->redirect(['user/profile']);
        }

        // Case 2: Guest attempting to log in via Telegram
        $user = User::findOne(['telegram_id' => $telegramId]);
        if (!$user) {
            // Optional: Auto-register the user here if your application allows it.
            // For this example, we require an existing account to be linked first.
            Yii::$app->session->setFlash('error', 'No local account is linked to this Telegram account. Please log in normally and link your Telegram profile.');
            return $this->redirect(['site/login']);
        }

        // Log the user in
        if (Yii::$app->user->login($user, 3600 * 24 * 30)) {
            Yii::$app->session->setFlash('success', 'Successfully logged in via Telegram.');
            return $this->goBack();
        }

        Yii::$app->session->setFlash('error', 'An error occurred during login.');
        return $this->redirect(['site/login']);
    }
}

Production Considerations & Edge Cases

When deploying this authentication flow to production, keep the following security and operational practices in mind:

  • Timing Attack Prevention: Always use hash_equals() instead of the standard == or === operators when comparing the generated hash with the incoming hash. Standard string comparisons return false as soon as a character mismatch is found, which allows attackers to deduce the signature character-by-character by measuring response times.
  • Strict Database Types: Always enforce unsigned and bigInteger for the telegram_id column. If your database driver attempts to cast a large Telegram ID into a standard 32-bit signed integer, it can overflow, causing multiple users to resolve to the same truncated ID, leading to critical account cross-overs.
  • Handling Username Changes: The Telegram Login Widget returns the user's current Telegram username. If you store this username in your database for display purposes, update it during every successful login. Users change their Telegram handles frequently, and displaying stale handles can confuse administrators.
  • Secure Token Storage: Never commit your Telegram Bot Token to your version control system. Use environment variables (.env files) to load the token dynamically at runtime.

For more advanced integrations, such as handling deep linking or processing incoming bot updates, refer to the official documentation at https://botservice.biz/telegram-bot-api.

Need assistance building custom Telegram integrations, webhooks, or secure authentication portals? BotCreator — studio that ships Telegram bots / Mini Apps.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.