How to log in to the site using the Telegram bot?

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-url must be a public HTTPS URL. Debug tunnels such as ngrok are fine; bare http://localhost is 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:

  1. Collect every field except hash as key=value pairs.
  2. Sort by key and join with \n.
  3. Secret key: binary SHA256(bot_token).
  4. Compute hash_hmac('sha256', …) and compare to hash with hash_equals.
  5. Reject the payload if auth_date is 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 id as 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.

  1. The site creates a short token, stores it as pending with a TTL (for example 15 minutes).
  2. The user opens https://t.me/YourBotUsername?start=login_TOKEN.
  3. The bot’s /start handler reads the payload, confirms the token, and stores telegram_user_id / chat_id.
  4. 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 /setdomain and 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.

New articles on Telegram

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