The Telegram Login Widget adds a “Log in with Telegram” button to your site without a third-party OAuth provider or a password. The user confirms in the Telegram client; your HTTPS endpoint receives a payload with id, auth_date, and a hash signature. This guide covers embedding the widget, verifying HMAC on the server, binding telegram_id to your user, and a short alternative via a bot deep link.
Prerequisites
- A bot from @BotFather and its token (server-side secret only).
- Your site domain linked to the bot with BotFather’s
/setdomain. - A public HTTPS callback URL — Telegram rejects localhost and plain HTTP.
Without the domain in BotFather the button will not work. Never expose the bot token in the browser; it is only for verifying hash.
1. Embed the Login Widget
Add Telegram’s script on the login page. data-telegram-login is the bot username without @. data-auth-url must be the absolute HTTPS URL of your handler (redirect after auth).
<!-- /login page -->
<script async
src="https://telegram.org/js/telegram-widget.js?22"
data-telegram-login="YourBotUsername"
data-size="large"
data-auth-url="https://example.com/auth/telegram"
data-request-access="write">
</script>
After confirmation Telegram redirects to your URL with query parameters: id, first_name, optional username, photo_url, auth_date, and hash.
Important:data-auth-urlmust be a public HTTPS URL. Debug tunnels such as ngrok are fine; barehttp://localhostis not.
A deeper walkthrough of signature checks in PHP/Yii2 is in Verify Telegram Login Widget in PHP and Yii2.
2. Server-side HMAC and auth_date
Anything from the browser can be forged. Trust only a server check:
- Collect every field except
hashaskey=valuepairs. - Sort by key and join with
\n. - Secret key: binary
SHA256(bot_token). - Compute
hash_hmac('sha256', …)and compare tohashwithhash_equals. - Reject the payload if
auth_dateis older than your window (often 24 hours; shorter is safer).
<?php
function verifyTelegramLogin(array $data, string $botToken, int $maxAge = 86400): bool
{
$checkHash = (string)($data['hash'] ?? '');
if ($checkHash === '') {
return false;
}
$pairs = [];
foreach ($data as $key => $value) {
if ($key === 'hash' || !is_scalar($value)) {
continue;
}
$pairs[] = $key . '=' . $value;
}
sort($pairs);
$dataCheckString = implode("\n", $pairs);
$secretKey = hash('sha256', $botToken, true);
$calc = hash_hmac('sha256', $dataCheckString, $secretKey);
if (!hash_equals($calc, $checkHash)) {
return false;
}
$authDate = (int)($data['auth_date'] ?? 0);
if ($authDate < time() - $maxAge) {
return false;
}
return isset($data['id']) && (int)$data['id'] > 0;
}
// Handler sketch
$data = $_GET; // or $_POST, matching your callback
if (!verifyTelegramLogin($data, getenv('TELEGRAM_BOT_TOKEN'))) {
http_response_code(403);
exit('Invalid Telegram login');
}
$telegramId = (int)$data['id'];
// next: find or create the user and open a session
Do not log the bot token or full signed payloads. Treat bad signatures as a generic “login failed” so you do not leak timing or format hints.
3. Bind telegram_id to your user
After a successful verify:
- Look up the user by
telegram_user_id(or an oauth-link table). - If found — log them in.
- If a site session already exists — attach Telegram to that account (and ensure the id is not owned by someone else).
- Otherwise create the account / oauth row and store
idas the stable identifier.
The Login Widget gives you the user id but does not guarantee the user has pressed Start on the bot. If you need to message them in private chat, wait for that dialog or use the deep-link flow below.
For a Yii2-oriented verify-and-bind walkthrough, see verify Telegram Login Widget (PHP/Yii2).
4. Alternative: deep-link start=token
When the widget domain is unavailable (subdomain, staging without /setdomain) or you need chat_id immediately, use a one-time token in a bot deep link.
- The site creates a short token, stores it as
pendingwith a TTL (for example 15 minutes). - The user opens
https://t.me/YourBotUsername?start=login_TOKEN. - The bot’s
/starthandler reads the payload, confirms the token, and storestelegram_user_id/chat_id. - The browser polls or hits
/auth/telegram/complete?token=…and receives a session.
<?php
// site: create a login session
$token = bin2hex(random_bytes(10)); // ~20 hex chars
// persist: token, status=pending, expires_at=now+900
$deepLink = 'https://t.me/YourBotUsername?start=login_' . $token;
// bot: handle message.text like "/start login_TOKEN"
if (preg_match('#^/start\s+login_([a-f0-9]+)$#', $text, $m)) {
$token = $m[1];
// find pending token, save from.id and chat.id, status=confirmed
// send the user a link to https://example.com/auth/telegram/complete?token=...
}
// site: complete
// if token is confirmed and not expired — login / bind telegram_id, status=consumed
Tokens must be single-use: mark them consumed after login. Put only an opaque record id in start, never a site session secret.
Which option to pick
- Login Widget — familiar button on the site, little bot UX, requires
/setdomainand a public HTTPS callback. - Deep-link — works without the widget domain, opens the bot chat immediately, needs a bit more token plumbing.
Both paths end the same way: the server trusts a verified telegram_id and opens the user session itself.
Need a ready site-and-bot login setup for your domain? See botservice.biz or send a short brief for an estimate.