Перевірка віджета входу в Telegram на PHP та Yii2: HMAC-SHA-256, auth_date та зв 'язування telegram_id

Віджет входу в Telegram — кнопка в iframe, яка повертає поля профілю та підпис hashрозраховується за допомогою маркера бота. Віджет зручний, але всі поля корисного навантаження (включаючи id, auth_date, first_name) можуть бути підроблені зловмисником. Доводить, що дані надійшли з Telegram, перевіряючи лише підпис на сервері.

У статті ми збираємо робочий шлях перевірки:

  • прийняти віджет корисного навантаження (id, first_name, last_name, userName, photo_url, auth_date, hash);
  • перерахувати HMAC-SHA-256 та порівняти з hash Через hash_equals;
  • вікно обмеження auth_dateщоб вкрадене корисне навантаження не можна було обертати нескінченно;
  • Прив'язати telegram_id користувачеві в Yii2 (створити при першому вході в систему).

Важливо! Віджет є ідентифікаційною заявою, а не попередньо створеною авторизацією. Ви все ще роздаєте свій сеанс самостійно.

Що надсилає віджет

Після успішного входу Telegram додає поля до параметрів запиту зворотного виклику. (data-onauth або JS-callback). Типове корисне навантаження:

id=12345678
first_name=Alex
last_name=Ivanov
username=alex_ivanov
photo_url=https%3A%2F%2Ft.me%2Fi%2Fuserpic%2F...%2F...jpg
auth_date=1716300000
hash=9f2c5b1e8a4f4d0c...e6

id поставляється з рядком, але це завжди числовий ідентифікатор користувача. auth_date — Unix-секунди. hash — HEX від HMAC-SHA-256 (64 символи).

Алгоритм перевірки

Рядок підпису збирається з усіх полів Крім hashпари key=value, відсортовано за ключем, через стрічку , n. Порожні значення відкидаються.

secret_key = SHA256(bot_token)   // 32 байта, raw
computed = HMAC-SHA-256(secret_key, data_check_string) // hex

Ключ HMAC двійковий Токен бота SHA-256 (hash('sha256', $botToken, true)). Поширена помилка в навчальних посібниках — забути прапор TRUE () і отримайте шістнадцятковий рядок замість 32 байтів.

Тут немає мережевих викликів Bot API: все рахується локально.

Перевірка на чистому PHP

<?php
declare(strict_types=1);

final class TelegramLoginVerifier
{
private const CLOCK_SKEW_SECONDS = 300; // 5 минут

public function __construct(
private readonly string $botToken,
private readonly int $now,
) {}

/**
* @param array<string, string> $payload
* @return array{ok: bool, reason?: string, profile?: array{id: int, first_name: string, last_name: ?string, username: ?string, photo_url: ?string}}
*/
public function verify(array $payload): array
{
$hash = $payload['hash'] ?? '';
unset($payload['hash']);

if ($hash === '' || !preg_match('/^[a-f0-9]{64}$/', $hash)) {
return ['ok' => false, 'reason' => 'bad_hash_format'];
}

$authDate = (int) ($payload['auth_date'] ?? 0);
if ($authDate <= 0 || abs($this->now - $authDate) > self::CLOCK_SKEW_SECONDS) {
return ['ok' => false, 'reason' => 'auth_date_expired'];
}

$pairs = [];
foreach ($payload as $k => $v) {
if ($v === '' || $v === null) {
continue;
}
$pairs[] = $k . '=' . $v;
}
sort($pairs, SORT_STRING);
$dataCheckString = implode("\n", $pairs);

$secretKey = hash('sha256', $this->botToken, true);
$computed = hash_hmac('sha256', $dataCheckString, $secretKey);

if (!hash_equals($computed, $hash)) {
return ['ok' => false, 'reason' => 'bad_signature'];
}

return [
'ok' => true,
'profile' => [
'id' => (int) $payload['id'],
'first_name' => (string) $payload['first_name'],
'last_name' => $payload['last_name'] ?? null,
'username' => $payload['username'] ?? null,
'photo_url' => $payload['photo_url'] ?? null,
],
];
}
}

Порівняти підписи за допомогою hash_equals, а не через звичайний ===.

Підключення до Yii2

Токен бота — тільки з params/ENV, не в коді:

// config/params.php
return [
'telegramBotToken' => getenv('TELEGRAM_BOT_TOKEN') ?: '',
];

У моделі користувача вам потрібні такі поля, як telegram_id (Bigint УНІКАЛЬНИЙ НУЛЬ) і last_telegram_login_atКонтролер:

<?php
declare(strict_types=1);

namespace app\controllers;

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

final class TelegramAuthController extends Controller
{
public function actionCallback(): \yii\web\Response
{
$token = (string) Yii::$app->params['telegramBotToken'];
if ($token === '') {
throw new \yii\web\HttpException(500, 'Bot token not configured');
}

$payload = Yii::$app->request->get();
$verifier = new TelegramLoginVerifier($token, time());
$result = $verifier->verify($payload);

if (!$result['ok']) {
Yii::warning('Telegram login rejected: ' . $result['reason'], __METHOD__);
return $this->redirect(['site/login', 'error' => 'telegram_verification_failed']);
}

$profile = $result['profile'];
$user = User::findOne(['telegram_id' => $profile['id']]);

if ($user === null) {
$user = new User([
'telegram_id' => $profile['id'],
'username' => $profile['username'] ?? ('tg_' . $profile['id']),
'first_name' => $profile['first_name'],
'last_name' => $profile['last_name'],
'last_telegram_login_at' => time(),
]);
if (!$user->save(false)) {
throw new \yii\web\HttpException(500, 'Cannot create user');
}
} else {
$user->last_telegram_login_at = time();
$user->save(false, ['last_telegram_login_at']);
}

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

Після зв 'язування telegram_id віджет не повинен вирішувати, який рядок користувача вибрати: ми шукаємо лише за цим ідентифікатором./username можна оновити, id не можна.

Виробничі примітки

  • Часове вікно. 5 хвилин — розумний дефолт. Менше — ламає довгі підтвердження; більше — легше відтворює вкрадене корисне навантаження.
  • Ротація маркера бота. Після зміни маркера старі підписи віджетів перестають передаватися. Немає ніякого «м 'якого» переходу — обертайте токен, тільки якщо ви готові знову ввести його.
  • Віджет входу в домен. Налаштовується на my.telegram.org, а не в BotFather. Один бот може бути використаний як для віджету, так і для API бота; користувачеві не потрібен віджет Start.
  • Выход из ситуации. Досить Yii::$app->user->logout(). Telegram не знає про вашу сесію.
  • Не викликайте API бота «для повторної перевірки». Не потрібна getChat / getProfilePhotos: hash вже є підписом на стороні сервера. Додатковий HTTP лише перетягує маркер у журнали та додає затримку.

Якщо вам потрібен віджет входу та міні-додаток/ діалог з ботом для однієї людини — тримайте telegram_id у внутрішньому записі з першого дня: це ключ зв 'язку.

Потрібен готовий набір «Телеграм-сайту↔» під ключ — пишіть на BotCreator.

Нові статті — у Telegram

Розбираємо, що автоматизувати в бізнесі та як це працює на практиці. Без спаму.