ЩО МИ МОЖЕМО СТВОРИТИ
Потік двосторонньої автентифікації:
1. Програма React Mini: initData від @twa-dev/sdk і додає його до кожного запиту API як користувацький заголовок. 2. PHP-сервер перевіряє цей заголовок за допомогою HMAC-SHA-256 (маркер бота як ключ), відхиляє всі підроблені або застарілі, а потім видає короткочасний JWT, прив 'язаний до перевіреного Ідентифікатор Telegram. 3. Захищені кінцеві точки перевіряють JWT і відхиляють запити, коли Ідентифікатор Telegram претензія не збігається з тим, що очікує сервер для цього ресурсу.
Ми не стверджувати, що це запобігає конкретній атаці, яка контролює як клієнта, так і мережу. Що він робить, так це піднімає планку, щоб «контролювати шлях підпису Telegram або викрасти ваш токен бота», що є реалістичною моделлю загрози для віджета.
---
1. Модель загрози, коротко
Три речі, від яких вам дійсно потрібно захистити себе у віджеті, який викликає ваш API:
- Відтворення вкраденого initData: зупинитися на Дата авторизації закінчення терміну дії та невеликий одноразовий сеанс. - Ковка Ідентифікатор Telegram: зупиніться, перерахувавши HMAC на сервері за допомогою маркера бота. Клієнт ніколи не вибирає ідентифікатор користувача; це робить підпис. - Повторне використання JWT, виданого користувачеві A проти ресурсу користувача B: зупиніться, пов 'язавши JWT з Ідентифікатор Telegram і протестувати його на кожній захищеній кінцевій точці, а не лише під час входу.
Ось і весь контрольний список. Щось нижче відповідає одному з них.
---
2. Сторона реакції: посудину initData при кожному зверненні
Встановіть SDK і HTTP-клієнт. Ми використовуємо приносити безпосередньо, щоб навчальний посібник не залежав від транспорту, але той самий шаблон працює з перехоплювачами Axios.
npm i @twa-dev/sdk
Мінімальна клієнтська обгортка:
// src/api/client.js
import WebApp from '@twa-dev/sdk';
const API_BASE = import.meta.env.VITE_API_BASE;
export async function api(path, options = {}) {
const initData = WebApp.initData; // raw query string, including hash
const initDataUnsafe = WebApp.initDataUnsafe; // parsed object, for client-side hints only
const headers = {
'Content-Type': 'application/json',
'X-Tg-Init-Data': initData,
...(options.headers || {}),
};
const res = await fetch(`${API_BASE}${path}`, {
...options,
headers,
body: options.body ? JSON.stringify(options.body) : undefined,
});
if (!res.ok) {
const text = await res.text();
throw new Error(`API ${res.status}: ${text}`);
}
return res.json();
}
Кнопка входу
// src/Login.jsx
import WebApp from '@twa-dev/sdk';
import { api } from './api/client';
export function Login() {
async function signIn() {
WebApp.readyToString();
const { token, user } = await api('/auth/telegram', { method: 'POST' });
localStorage.setItem('tg_jwt', token);
console.log('Signed in as', user.telegram_id);
}
return <button onClick={signIn}>Sign in with Telegram</button>;
}
Необхідно пам 'ятати про декілька моментів:
- WebApp.initData це необроблений рядок, який вводить Telegram. Надіслати що, а не ресеріалізовану версію. Сервер повинен точно проаналізувати його. - WebApp.initDataUnsafe для підказок інтерфейсу користувача (відображення імені користувача, кнопка приховування для не адміністраторів). Ніколи довіряти йому на сервері. Сервер перераховує всі initData і маркер бота.
---
3. Сторона PHP: Перевірка initData та м 'яти JWT
Ми розділили це на три невеликі частини: верифікатор, асистент JWT та маршрут.
Верифікатор здійснює офіційну верифікацію Telegram:
1. Парсинг initData XXX значення ключа пари, розділені &, зберігаючи значення, декодовані URL-адресою. 2. Створіть data_check_string шляхом об 'єднання кожної пари Крім хеші, відсортований за ключем, поєднаний з , n3. Розрахунок HMAC-SHA-256(key=BOT_TOKEN, msg="WebAppData" + data_check_string). 4. Порівняйте з хеші за допомогою hash_equals. 5. Відхилити, якщо Дата авторизації старші за ~5 хвилин (або незалежно від тривалості вашого сеансу).
// src/Telegram/InitDataVerifier.php
namespace App\Telegram;
final class InitDataVerifier
{
public function __construct(private readonly string $botToken) {}
/** @return array<string,string> */
public function verify(string $initData, int $maxAgeSeconds = 300): array
{
$pairs = [];
foreach (explode('&', $initData) as $pair) {
if ($pair === '' || !str_contains($pair, '=')) continue;
[$k, $v] = explode('=', $pair, 2);
$pairs[urldecode($k)] = urldecode($v);
}
if (!isset($pairs['hash'], $pairs['auth_date'], $pairs['user'])) {
throw new \DomainException('initData missing required fields');
}
$receivedHash = $pairs['hash'];
unset($pairs['hash']);
ksort($pairs);
$dataCheckString = implode("\n", array_map(
fn($k, $v) => "$k=$v",
array_keys($pairs),
array_values($pairs)
));
$secret = hash_hmac('sha256', $this->botToken, 'WebAppData', true);
$computed = hash_hmac('sha256', $dataCheckString, $secret);
if (!hash_equals($computed, $receivedHash)) {
throw new \DomainException('initData signature mismatch');
}
if (time() - (int)$pairs['auth_date'] > $maxAgeSeconds) {
throw new \DomainException('initData expired');
}
return $pairs;
}
}
Помічник JWT за допомогою firebase/php-jwt. Тримайте секрет подалі від сховища; прочитайте його з навколишнього середовища.
// src/Auth/JwtIssuer.php
namespace App\Auth;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
final class JwtIssuer
{
public function __construct(
private readonly string $secret,
private readonly string $algo = 'HS256',
private readonly int $ttlSeconds = 3600,
) {}
public function issueForTelegramUser(int $telegramId, array $extra = []): string
{
$now = time();
return JWT::encode([
'sub' => (string)$telegramId,
'tg' => $telegramId,
'iat' => $now,
'nbf' => $now,
'exp' => $now + $this->ttlSeconds,
] + $extra, $this->secret, $this->algo);
}
/** @return object decoded claims */
public function decode(string $token): object
{
return JWT::decode($token, new Key($this->secret, $this->algo));
}
}
Маршрут. Зверніть увагу на три речі: ми читаємо Bot Token і JWT secret від середовища, ми декодуємо JSON користувача. і потягніть id тільки після проходження перевірки HMAC, і ми чеканимо JWT, чий sub перевірено Ідентифікатор Telegram.
// public/index.php (or your router)
use App\Telegram\InitDataVerifier;
use App\Auth\JwtIssuer;
$botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
$jwtSecret = getenv('APP_JWT_SECRET') ?: '';
if ($botToken === '' || $jwtSecret === '') {
http_response_code(500); exit('server misconfigured');
}
$verifier = new InitDataVerifier($botToken);
$issuer = new JwtIssuer($jwtSecret);
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
if ($path === '/auth/telegram' && $method === 'POST') {
$initData = $_SERVER['HTTP_X_TG_INIT_DATA'] ?? '';
if ($initData === '') { http_response_code(400); exit('missing initData'); }
try {
$claims = $verifier->verify($initData);
} catch (\Throwable $e) {
http_response_code(401); exit('invalid initData');
}
$user = json_decode($claims['user'], true);
if (!is_array($user) || !isset($user['id'])) {
http_response_code(400); exit('user field malformed');
}
$token = $issuer->issueForTelegramUser((int)$user['id']);
header('Content-Type: application/json');
echo json_encode([
'token' => $token,
'user' => ['telegram_id' => (int)$user['id'], 'name' => $user['first_name'] ?? ''],
]);
return true;
}
Це весь процес входу. Клієнт зберігає його, сервер не довіряє нічому, що він не підписав.
---
4. Захищені кінцеві точки: зв 'яжіть JWT з Ідентифікатор Telegram
JWT самостійно недостатньо. Якщо ваш маршрут обробляє ресурс, який належить конкретному користувачеві Telegram (замовлення, підписка, чернетка), обробник повинен порівняти ФОРМУЛА ВИНАХОДУ проти власника ресурсу, перш ніж що-небудь робити.
// src/Auth/require_telegram_user.php
function requireTelegramUser(JwtIssuer $issuer, int $expectedTelegramId): object
{
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!preg_match('/^Bearer (.+)$/', $auth, $m)) {
http_response_code(401); exit('missing bearer');
}
try {
$claims = $issuer->decode($m[1]);
} catch (\Throwable $e) {
http_response_code(401); exit('invalid token');
}
if ((int)($claims->tg ?? 0) !== $expectedTelegramId) {
http_response_code(403); exit('telegram id mismatch');
}
return $claims;
}
І маршрут, який його використовує:
if ($path === '/orders' && $method === 'GET') {
$telegramId = (int)($_GET['telegram_id'] ?? 0);
requireTelegramUser($issuer, $telegramId);
$orders = $db->fetchAll('orders', ['telegram_id' => $telegramId]);
header('Content-Type: application/json');
echo json_encode($orders);
return true;
}
JWT для користувача A буде відхилено користувачем B Ідентифікатор Telegram, навіть якщо обидва підписані одним і тим же секретом. Тобто "зобов 'язання", на яке посилається заголовок.
---
5. Виробничі примітки (необов 'язково, але варто)
Вони не потрібні для роботи потоку, але вони представляють різницю між демонстрацією та тим, що ви можете надіслати.
- Зберігання токенів на клієнті. localStorage підходить для віджету, оскільки джерелом є WebView Telegram, а не ворожа сторінка. Якщо ви обслуговуєте той самий пакет із загальнодоступного домену, віддайте перевагу файлам cookie HttpOnly, встановленим /auth/telegram. - Вікно відтворення. 5 хвилин - це значення за замовчуванням вище. Зменште його до 60 секунд, якщо ви карбуєте одноразові монети; подовжте його, тільки якщо у вас є суворий потік оновлення, пов 'язаний з пристроєм. - Оновити Видати другу кінцеву точку /auth/refresh який займає initData знову, повторно перевіряє його і повертає свіжий JWT. Зробіть не прийняти запит на оновлення без свіжого initData підпис. Подробиці в журналі Журнал подій Ідентифікатор Telegram, Дата авторизаціїі обрізаний хеш. Ніколи не реєструйте RAW initData рядок запиту — містить дані користувача. - Годинник перекошений. Порівняти з Дата авторизації використання часу сервера, а не претензій клієнтів. час Нормально. Вебхук проти міні-застосунку автентичн. Ця стаття про міні-додаток initData. Потік віджетів входу використовує інше корисне навантаження. (id, first_name, Дата авторизації, хеші, ...) та інші data_check_stringНе змішуйте їх.
---
6. Що перевіряти
Коротке ручне тестування перед відвантаженням:
1. Відкрийте мини-приложение у Telegram. /auth/telegram повертає 200 і JWT. 2. Відтворити те саме X-Tg-Init-Data заголовок з Коло Через 6 хвилин зачекайте 401 "initData expired". 3. Змінити один байт з initData і повторіть його. Очікуйте 401 "недійсні initData". 4. Надіслати дійсний JWT для користувача A, але запит /orders?telegram_id=B. Очікуйте 403.
Якщо всі чотири передані, ваш шлях автентифікації виконує те, що повинен.
---
Якщо ви запускаєте міні-додаток і хочете команду, яка вже відвантажила такий потік, BotCreator - це студія, яка створює ботів для Telegram та міні-додатків від початку до кінця, і на неї варто подивитися. Для більш глибокого занурення в поверхню API бота (вебхуки, обмеження швидкості, корисні навантаження зворотного виклику) їх посилання на API бота Telegram є корисною закладкою.