The Telegram Login Widget is an efficient way to authenticate users on a website without forcing them to fill out registration forms or verify email addresses. However, because authentication happens entirely on the client side before redirecting to your server, accepting the payload at face value opens your application to immediate account takeover.
When a user authenticates through the widget, Telegram passes parameters such as id, first_name, username, auth_date, and a cryptographic hash to your specified callback URL or JavaScript event handler. If your backend simply reads Yii::$app->request->get('id') and creates a session, an attacker can modify the query string to contain any victim's Telegram ID. To secure this process, you must verify the signature on your server using your bot token, ensure the auth payload has not expired, and map the 64-bit Telegram user ID to your application database.
Here is how to safely validate the Telegram Login Widget payload in PHP within a Yii2 application, prevent replay attacks, and bind verified IDs to user accounts.
The Cryptographic Mechanism Behind Telegram Login Verification
A common mistake developers make when implementing the Telegram Login Widget is applying the HMAC algorithm used for Telegram Mini App initData. The verification rules for the Login Widget are slightly different, and confusing them will cause all valid signature checks to fail.
For the Login Widget, Telegram formats the payload validation string by collecting all key-value pairs received in the query or POST body (excluding the hash field itself). These parameters must be sorted alphabetically by key and concatenated into a string using the key=value format joined by newline characters (\n).
Unlike Mini Apps—where the secret key is calculated using an HMAC signature of the bot token with the string WebAppData—the Login Widget derives its secret key by taking a plain SHA-256 binary hash of the raw bot token string. The server then computes an HMAC-SHA-256 signature of the constructed data-check string using that binary secret key and compares it against the hash parameter sent by the widget.
In addition to checking the signature, your backend must validate the auth_date timestamp. If an attacker intercepts a legitimate authentication payload from a user's browser URL bar, they could reuse that link indefinitely unless your system rejects payloads older than a tight time window (for example, 86,400 seconds or 24 hours).
Step 1: Create a Signature and Expiry Validator in Yii2
To keep controller logic clean, encapsulate the verification algorithm into a reusable Yii2 application component or helper service class. This component will handle string sorting, binary secret generation, constant-time hash comparison, and timestamp expiration checking.
Create a component file at components/TelegramAuthValidator.php:
<?php
namespace app\components;
yii\base\Component;
use yii\base\InvalidArgumentException;
class TelegramAuthValidator extends Component
{
public string $botToken;
public int $maxAuthAge = 86400; // 24 hours in seconds
public function init(): void
{
parent::init();
if (empty($this->botToken)) {
throw new InvalidArgumentException('Bot token must be configured in TelegramAuthValidator.');
}
}
/**
* Validates incoming parameters from Telegram Login Widget.
*/
public function validatePayload(array $data): bool
{
if (empty($data['hash']) || empty($data['auth_date'])) {
return false;
}
// Reject expired authentication attempts
$authDate = (int)$data['auth_date'];
if ((time() - $authDate) > $this->maxAuthAge) {
return false;
}
$checkHash = $data['hash'];
unset($data['hash']);
// Sort parameters alphabetically by key
$dataCheckArray = [];
foreach ($data as $key => $value) {
$dataCheckArray[] = $key . '=' . $value;
}
sort($dataCheckArray);
$dataCheckString = implode("\n", $dataCheckArray);
// Secret key is the SHA256 raw binary hash of the bot token
$secretKey = hash('sha256', $this->botToken, true);
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
// Use constant-time comparison to prevent timing attacks
return hash_equals($calculatedHash, $checkHash);
}
}
Register this component in your Yii2 application configuration (config/web.php or config/main.php), sourcing the bot token strictly from environment variables:
'components' => [
'telegramValidator' => [
'class' => 'app\components\TelegramAuthValidator',
'botToken' => getenv('TELEGRAM_BOT_TOKEN'),
'maxAuthAge' => 86400,
],
],
Step 2: Prepare the User Database Schema for 64-Bit Telegram IDs
Telegram user IDs have scaled well beyond standard 32-bit signed integers (2,147,483,647). Storing a modern Telegram ID in a standard INTEGER column in PostgreSQL or MySQL will cause silent integer overflow errors or fatal database exceptions during sign-up.
Use a Yii2 database migration to add a 64-bit BIGINT column with a unique index to your existing user table. A unique constraint is required to prevent multiple web accounts from binding to the same Telegram account.
Run the migration generator or create the migration class migrations/m240320_000000_add_telegram_id_to_user_table.php:
<?php
use yii\db\Migration;
class m240320_000000_add_telegram_id_to_user_table extends Migration
{
public function safeUp()
{
$this->addColumn('{{%user}}', 'telegram_id', $this->bigInteger()->null()->unique());
$this->createIndex('idx-user-telegram_id', '{{%user}}', 'telegram_id');
}
public function safeDown()
{
$this->dropIndex('idx-user-telegram_id', '{{%user}}');
$this->dropColumn('{{%user}}', 'telegram_id');
}
}
Apply the migration using the console command php yii migrate.
Step 3: Implement Account Binding and Login Logic in Controller
When handling the incoming redirect or AJAX request from the Telegram Login Widget, your controller action must address two distinct functional paths:
1. Logged-in user linking their profile: A user logged into an existing password-based account clicks the Telegram widget to bind their account. The system must verify that no other account is already using that telegram_id. 2. Guest authentication or registration: An unauthenticated user clicks the widget to log in. The system checks if a record with the matching telegram_id exists. If found, log the user in; if not found, create a new local user account before establishing the session.
Create or update your controllers/AuthController.php:
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\web\BadRequestHttpException;
use app\models\User;
use app\components\TelegramAuthValidator;
class AuthController extends Controller
{
public function actionTelegramCallback()
{
$request = Yii::$app->request;
$queryParams = $request->get();
/** @var TelegramAuthValidator $validator */
$validator = Yii::$app->get('telegramValidator');
// Cryptographically verify payload before doing anything else
if (!$validator->validatePayload($queryParams)) {
throw new BadRequestHttpException('Invalid signature or stale authentication request.');
}
$telegramId = (int)$queryParams['id'];
$currentUser = Yii::$app->user->identity;
// Scenario 1: Authenticated user linking account
if (!Yii::$app->user->isGuest) {
$existingOwner = User::findOne(['telegram_id' => $telegramId]);
if ($existingOwner && $existingOwner->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, ['telegram_id'])) {
Yii::$app->session->setFlash('success', 'Telegram profile linked successfully.');
}
return $this->redirect(['user/profile']);
}
// Scenario 2: Guest logging in via Telegram
$user = User::findOne(['telegram_id' => $telegramId]);
if (!$user) {
$user = new User();
$user->username = 'tg_' . $telegramId;
$user->telegram_id = $telegramId;
// Set random auth key or status according to app policies
$user->generateAuthKey();
if (!$user->save()) {
Yii::error('Failed to register user via Telegram: ' . json_encode($user->getErrors()), __METHOD__);
throw new \RuntimeException('Account creation failed.');
}
}
// Log the user in for 30 days
Yii::$app->user->login($user, 3600 * 24 * 30);
return $this->goHome();
}
}
Production Notes: Security and Architecture Guardrails
1. Timing Attacks: Always use PHP's hash_equals() instead of the standard string equality operator (===). Standard string comparison exits early on the first non-matching byte, allowing attackers to measure execution time differences down to microseconds to reconstruct expected signature hashes. 2. Avoid Query Parameter Leaks: If your widget uses data-auth-url to redirect the user to /auth/telegram-callback?id=..., the hash and payload remain in browser history and server access logs. If possible, handle the payload via JavaScript callback (data-onauth) and POST the parameters over an HTTPS endpoint to keep access tokens out of access logs. 3. Sanitize Optional User Attributes: The widget may supply first_name, last_name, and username. Never insert raw Telegram usernames directly into HTML views without escaping via yii\helpers\Html::encode(), as users can embed XSS payloads in their Telegram profile display names. 4. Handling Rebind Race Conditions: If two logged-in sessions try to claim the same telegram_id simultaneously, your database unique index will throw an exception. Wrap the assignment logic in a database transaction or catch yii\db\Exception to show a user-friendly error message if a race condition occurs.
Building reliable web authentication tied to Telegram requires strict adherence to cryptographic validation rules. If you are scaling beyond static websites into complex Mini Apps or bot integrations, BotCreator provides dedicated engineering expertise for shipping production-ready Telegram products.