Prevent Account Takeover by HMAC-Verifying Telegram Login Payload in Yii2

Integrating the Telegram Login Widget onto an existing web application provides a low-friction authentication mechanism for users. Instead of filling out registration forms or confirming email addresses, visitors click a single button, approve a prompt inside their Telegram app, and arrive back on your site with a payload containing their id, first_name, username, auth_date, and hash.

However, a dangerous anti-pattern frequently slips into PHP codebases: trusting the incoming id parameter directly from $_GET or $_POST without cryptographic verification. Because the frontend redirect or callback carries standard URL parameters, an attacker can manually craft a request containing your administrator's Telegram ID. If your Yii2 controller immediately queries User::findOne(['telegram_id' => $id]) and sets an authentication cookie, your entire authorization model collapses.

To prevent account impersonation, your backend must calculate an HMAC-SHA256 signature using your Telegram Bot Token, verify it against the incoming hash using a timing-safe comparison, check that the auth_date has not expired, and safely link the identity to the application user table.

The Vulnerability: Naive Parameter Trust

When a user authenticates via the Telegram Login Widget, the browser receives a JSON object or query parameters depending on whether you use data-onauth or redirect mode. Consider a standard callback route in a Yii2 application designed to log in a user:

namespace app\controllers;

use Yii;
use yii\web\Controller;
use app\models\User;

class VulnerableAuthController extends Controller
{
public function actionTelegramCallback()
{
$request = Yii::$app->request;
$telegramId = $request->get('id');

// DANGER: No hash verification! An attacker can pass any 'id'.
$user = User::findOne(['telegram_id' => $telegramId]);

if ($user) {
Yii::$app->user->login($user, 3600 * 24 * 30);
return $this->redirect(['/dashboard']);
}

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

If an attacker opens their browser devtools or uses curl to issue a request to /auth/telegram-callback?id=987654321, where 987654321 is an administrative user's Telegram ID, the code fetches the admin model and creates a logged-in session. The backend accepts arbitrary identity claims because it fails to enforce cryptographic integrity.

Even if the developer adds a basic check ensuring id is numeric, the system remains completely open. Front-end input must be treated as hostile until proven valid by the bot token secret.

Implementing Step-by-Step Cryptographic Verification

The official Telegram algorithm for verifying widget data differs subtly from the method used in Telegram Mini Apps (initData). Misunderstanding this distinction leads to broken validation code.

For the Telegram Login Widget, the verification process requires six steps:

1. Extract all parameters sent by Telegram (id, first_name, last_name, username, photo_url, auth_date, hash). 2. Strip the hash parameter from the key-value map. 3. Filter out any null or unset values. 4. Sort the remaining keys alphabetically in ascending order (ksort). 5. Format each pair into a string joined by newlines: key=value\nkey=value. 6. Compute the SHA-256 hash of the raw bot token to create the secret key, then compute the HMAC-SHA256 of the data-check string using that secret key.

Notice the critical step: the secret key for the Login Widget is hash('sha256', $botToken, true)—a raw binary SHA-256 digest of the bot token. In contrast, Telegram Mini Apps use hash_hmac('sha256', $botToken, 'WebAppData', true). Copying validation logic from a Mini App tutorial into a Login Widget controller will cause every valid login signature check to fail.

Here is a dedicated, reusable validation component implemented for Yii2:

namespace app\components;

use yii\base\Component;
use yii\base\InvalidArgumentException;

class TelegramAuthValidator extends Component
{
public string $botToken;
public int $maxAuthAge = 86400; // Reject logins older than 24 hours

public function init(): void
{
parent::init();
if (empty($this->botToken)) {
throw new InvalidArgumentException('Bot token must be configured.');
}
}

/**
* Validates incoming array of parameters from Telegram Login Widget.
*/
public function validate(array $authData): bool
{
if (!isset($authData['hash'], $authData['id'], $authData['auth_date'])) {
return false;
}

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

// Step 1: Sort parameters alphabetically by key
$dataCheckArray = [];
foreach ($authData as $key => $value) {
if ($value !== null && $value !== '') {
$dataCheckArray[] = $key . '=' . $value;
}
}
sort($dataCheckArray);

// Step 2: Assemble data check string
$dataCheckString = implode("\n", $dataCheckArray);

// Step 3: Compute secret key (SHA256 hash of bot token in raw binary format)
$secretKey = hash('sha256', $this->botToken, true);

// Step 4: Calculate HMAC-SHA256 digest
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Step 5: Timing-safe string comparison to prevent timing attacks
if (!hash_equals($calculatedHash, $checkHash)) {
return false;
}

// Step 6: Verify auth_date fresh state
if ((time() - (int)$authData['auth_date']) > $this->maxAuthAge) {
return false;
}

return true;
}
}

Using hash_equals rather than standard string comparison (===) protects against side-channel timing attacks, where an attacker measures slight differences in execution time to deduce characters of valid signatures.

Safe Account Binding and Callback Handling in Yii2

With a robust validator in place, we must handle two business scenarios cleanly: 1. User Login: An existing user presents a valid Telegram identity to authenticate into their account. 2. Account Link: An already authenticated web user binds their Telegram account to their database record.

Additionally, race conditions must be handled when two users attempt to link the same telegram_id, or when a user clicks the login button twice simultaneously. Setting a database unique index on telegram_id is mandatory.

Below is the complete controller action incorporating validation, session management, and database transaction safety:

namespace app\controllers;

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

class AuthController extends Controller
{
public function actionTelegramCallback()
{
$requestParams = Yii::$app->request->get();

/** @var TelegramAuthValidator $validator */
$validator = Yii::$app->get('telegramValidator');

if (!$validator->validate($requestParams)) {
throw new BadRequestHttpException('Invalid or expired Telegram authentication payload.');
}

$telegramId = (int)$requestParams['id'];
$currentUser = Yii::$app->user;

// Scenario A: User is already logged in, attempting to link account
if (!$currentUser->isGuest) {
/** @var User $identity */
$identity = $currentUser->identity;

// Check if this telegram_id is tied to another user
$existingOwner = User::find()->where(['telegram_id' => $telegramId])->andWhere(['!=', 'id', $identity->id])->one();
if ($existingOwner) {
Yii::$app->session->setFlash('error', 'This Telegram account is already linked to another user.');
return $this->redirect(['/user/profile']);
}

$identity->telegram_id = $telegramId;
$identity->telegram_username = $requestParams['username'] ?? null;
if ($identity->save(true, ['telegram_id', 'telegram_username'])) {
Yii::$app->session->setFlash('success', 'Telegram account successfully linked.');
} else {
Yii::$app->session->setFlash('error', 'Failed to update account integrity record.');
}
return $this->redirect(['/user/profile']);
}

// Scenario B: Guest user logging in via Telegram
$user = User::findOne(['telegram_id' => $telegramId]);
if ($user) {
// Update cached username if changed
if (isset($requestParams['username']) && $user->telegram_username !== $requestParams['username']) {
$user->telegram_username = $requestParams['username'];
$user->save(true, ['telegram_username']);
}

Yii::$app->user->login($user, 3600 * 24 * 30);
return $this->goBack();
}

Yii::$app->session->setFlash('error', 'No local account found matching this Telegram profile. Please register first.');
return $this->redirect(['/site/login']);
}
}

To wire the validation component into the application hierarchy, declare it inside config/web.php using environment variables to store the sensitive bot credentials:

return [
'components' => [
'telegramValidator' => [
'class' => 'app\components\TelegramAuthValidator',
'botToken' => getenv('TELEGRAM_BOT_TOKEN'),
'maxAuthAge' => 86400, // 24 hours
],
],
];

Production Notes

- Replay Attack Mitigation Beyond Expiration: A valid signature with an unexpired auth_date can be reused by an attacker who intercepted the GET request within the $maxAuthAge window. If your application handles critical financial transactions, reduce $maxAuthAge to 300 seconds (5 minutes) or store the processed hash values in Redis for their validity duration to ensure single-use enforcement. - Handling Missing Optional Parameters: Telegram does not guarantee that username, last_name, or photo_url will exist in the authentication payload. When iterating keys to construct $dataCheckArray, explicitly exclude null or empty string fields. Leaving an empty string key in the array during sorting changes the resulting string order and produces validation failures for users who do not set public usernames. - Clock Drift Anomalies: Ensure the host operating system synchronizes time via NTP. If your server clock drifts behind Telegram's timestamp servers, legitimate auth_date timestamps arriving from the future can fail range checks if you add lower-bound validation ($authDate > time()). Allow a small tolerance (e.g., 60 seconds) for negative clock skew. - Database Unique Constraints: Never rely purely on application-level checks to enforce unique Telegram bindings. Database tables must define a UNIQUE index on the telegram_id column to prevent duplicate account creation caused by concurrent asynchronous HTTP requests.

Building web authentication on top of Telegram requires applying rigid cryptographic boundaries to incoming payloads. By computing binary SHA-256 HMAC signatures on sorted parameters and enforcing short token expiration lifetimes, you protect user accounts from client-side manipulation and administrative takeover.

Need to construct secure Telegram Mini Apps, webhooks, or automated backend bots without spending engineering cycles on boilerplate cryptography? BotCreator is a specialized studio that builds and ships secure production Telegram bots and Mini Apps.

New articles on Telegram

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