Securely Authenticate Users with the Telegram Login Widget in PHP and Yii2

Integrating the Telegram Login Widget provides a frictionless authentication flow for web applications. However, accepting raw authentication parameters from the client side without strict cryptographic verification exposes your application to identity spoofing.

This guide demonstrates how to build a secure backend validator in PHP to verify the Telegram Login Widget payload using HMAC-SHA-256, enforce strict expiration checks on the authentication timestamp, and map the validated Telegram ID to a user record in the Yii2 framework. We do not cover the frontend HTML widget rendering or session storage configuration.

The Telegram Verification Algorithm

When a user authenticates via the Telegram Login Widget, your redirect URL or callback receives a set of query parameters (such as id, first_name, username, auth_date, and hash). To verify this payload:

1. Sort all received parameters alphabetically by key, excluding the hash parameter. 2. Format these parameters into a string of key=value pairs, separated by a newline character (\n). 3. Derive a secret key by calculating the SHA-256 hash of your Telegram Bot Token in binary format. 4. Compute the HMAC-SHA-256 signature of the formatted parameter string using the derived secret key. 5. Compare the computed signature against the received hash parameter using a timing-attack-resistant comparison function.

Step 1: Implementing the PHP Validator Class

This standalone class handles the cryptographic verification of the incoming payload. It uses hash_equals to mitigate timing attacks and checks the age of the auth_date parameter to prevent replay attacks.

<?php

namespace app\components;

use InvalidArgumentException;

class TelegramAuthValidator
{
private string $botToken;

public function __construct(string $botToken)
{
if (empty($botToken)) {
throw new InvalidArgumentException("Telegram bot token cannot be empty.");
}
$this->botToken = $botToken;
}

/**
* Validates the Telegram authentication payload.
*
* @param array $params The query parameters received from Telegram.
* @param int $maxAge Maximum allowed age of the authentication in seconds (default: 24 hours).
* @return bool True if the payload is valid and fresh, false otherwise.
*/
public function validate(array $params, int $maxAge = 86400): bool
{
if (!isset($params['hash']) || !isset($params['auth_date'])) {
return false;
}

// Prevent replay attacks by checking the age of the authentication
$authDate = (int)$params['auth_date'];
if ((time() - $authDate) > $maxAge) {
return false;
}

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

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

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

// Derive the secret key from the bot token
$secretKey = hash('sha256', $this->botToken, true);

// Calculate the HMAC-SHA-256 signature
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Compare hashes securely
return hash_equals($calculatedHash, $receivedHash);
}
}

Step 2: Integrating with Yii2 Controller and Database Mapping

To map the authenticated Telegram user to your local database, you must first ensure your user table contains a column for the Telegram ID.

Run a migration to add the column and a unique index:

# Create migration
./yii migrate/create add_telegram_id_to_user_table

Update the migration file to include the column:

<?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->string(64)->unique()->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');
}
}

Next, implement the controller action that handles the callback from the Telegram Login Widget. This action validates the payload, checks if a user with the given telegram_id exists, registers them if they do not, and logs them into the Yii2 application.

<?php

namespace app\controllers;

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

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

// Retrieve the bot token securely from environment variables or application parameters
$botToken = Yii::$app->params['telegram.bot_token'] ?? getenv('TELEGRAM_BOT_TOKEN');
if (empty($botToken)) {
throw new ServerErrorHttpException("Telegram bot configuration is missing.");
}

$validator = new TelegramAuthValidator($botToken);

// Validate signature and enforce a 12-hour expiration window
if (!$validator->validate($params, 43200)) {
throw new BadRequestHttpException("Invalid or expired Telegram authentication payload.");
}

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

// Attempt to find an existing user mapped to this Telegram ID
$user = User::findOne(['telegram_id' => $telegramId]);

if (!$user) {
// If the user is already logged in, link their current account
if (!Yii::$app->user->isGuest) {
$user = Yii::$app->user->identity;
$user->telegram_id = $telegramId;
if (!$user->save(true, ['telegram_id'])) {
throw new ServerErrorHttpException("Failed to link Telegram account.");
}
} else {
// Otherwise, register a new user account
$user = new User();
$user->telegram_id = $telegramId;
$user->username = $params['username'] ?? 'tg_' . $telegramId;
// Generate a random password and email placeholder if required by your schema
$user->password_hash = Yii::$app->security->generatePasswordHash(random_bytes(32));

if (!$user->save()) {
Yii::error("Failed to register Telegram user: " . json_encode($user->getErrors()), __METHOD__);
throw new ServerErrorHttpException("Failed to register user account.");
}
}
}

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

return $this->goBack();
}
}

Production Considerations

* Strict Expiration (auth_date): Always enforce a reasonable expiration window (e.g., 1 to 24 hours). If you do not verify auth_date, an attacker who intercepts a valid query string can replay the login request indefinitely. * Database Constraints: Ensure the telegram_id column has a unique constraint at the database level. This prevents race conditions where two concurrent requests might attempt to register the same Telegram ID to different local accounts. * Data Sanitization: When saving the username or first_name returned by Telegram, sanitize the input to prevent XSS if you display these values on your website.

For developers building complex integrations, refer to the official Telegram documentation at https://botservice.biz/telegram-bot-api for details on managing user sessions and bot interactions.

Need assistance deploying production-ready Telegram integrations? Contact BotCreator — studio that ships Telegram bots / Mini Apps.

New articles on Telegram

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