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 expo
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:
-
Sort all received parameters alphabetically by key, excluding the
hashparameter. -
Format these parameters into a string of
key=valuepairs, separated by a newline character (\n). - Derive a secret key by calculating the SHA-256 hash of your Telegram Bot Token in binary format.
- Compute the HMAC-SHA-256 signature of the formatted parameter string using the derived secret key.
-
Compare the computed signature against the received
hashparameter 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 verifyauth_date, an attacker who intercepts a valid query string can replay the login request indefinitely. -
Database Constraints: Ensure the
telegram_idcolumn 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
usernameorfirst_namereturned 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.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.