~/wiki / telegram-boty / telegram-auth-site-2026

Telegram-authorization on the site: how it works – Login Widget, OIDC, bot-flow and Mini App

Main chat

A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.

$ cd section/ $ join vibe dev
Telegram-authorization on the site: how it works – Login Widget, OIDC, bot-flow and Mini App - обложка

*Updated: June 2026. Includes a new OIDC flow through oauth.telegram.org, which was introduced in 2025. *


You added a button to the site “Login through Telegram”, copied the script with the widget – and think that’s ready. But if you have not written a backend that checks the HMAC signature, you have just created a hole: anyone can transfer an arbitrary user_id and log in under another account.

Authorization via Telegram seems simple on the outside. Inside it has several modes, non-trivial cryptography and a number of architectural solutions that affect security and UX. In this article - a complete analysis: how each method works, how to choose the right one, how to correctly check the signature and store the session.


Four ways to log in via Telegram

In front of the details is a map of options. Telegram doesn’t use OAuth 2.0 in its classic form — it “loves reinventing bicycles,” as the developers in RuNet accurately note. So instead of one standard flow, we have four different mechanisms:

Способ Когда появился Для чего подходит Сложность
Login Widget Февраль 2018 Сайты с бэкендом, классический «Войти через Telegram» Средняя
OIDC через oauth.telegram.org 2025 Современные приложения, совместимость со стандартом Средняя
Бот с одноразовой ссылкой Всегда работал Россия (виджет блокируется), корпоративные сценарии Низкая
Mini App initData 2022+ Приложения внутри Telegram Средняя

There is no "best" - there is a suitable scenario.


Login Widget: classics and their problems

How it works

Login Widget is a JavaScript script that Telegram provided in 2018. You paste it on the page, the user sees the button “Login through Telegram”, clicks – and the following happens:

  1. The browser opens a pop-up or redirect to Telegram servers.
  2. If the user is the first time, Telegram asks for a phone number and sends a confirmation code to the app.
  3. If the user is already authorized in the web version of Telegram, you do not need to enter anything.
  4. Telegram forms an object with user data and a cryptographic signature and transmits it to your site – either through callback_url (redirect) or through a JavaScript colback.

The data that comes in:

json
{
  "id": 123456789,
  "first_name": "Иван",
  "last_name": "Иванов",
  "username": "ivan_ivanov",
  "photo_url": "https://t.me/i/userpic/...",
  "auth_date": 1749300000,
  "hash": "abc123def456..."
}

Embedding the widget

html
<!-- Режим редиректа: данные придут на callback_url как GET-параметры -->
<script
  async
  src="https://telegram.org/js/telegram-widget.js?22"
  data-telegram-login="YourBotUsername"
  data-size="large"
  data-auth-url="https://your-site.ru/auth/telegram/callback"
  data-request-access="write">
</script>

<!-- Режим колбэка: данные придут в JavaScript-функцию -->
<script
  async
  src="https://telegram.org/js/telegram-widget.js?22"
  data-telegram-login="YourBotUsername"
  data-size="large"
  data-onauth="onTelegramAuth(user)"
  data-request-access="write">
</script>
<script>
function onTelegramAuth(user) {
  // НЕ доверяйте этим данным без проверки на бэкенде!
  fetch('/auth/telegram', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(user)
  });
}
</script>

Checking the signature on the backend: this is a must

The data that came from the widget cannot be considered trusted without verification. Telegram signs them HMAC-SHA256 – the key is built from a bot token. Verification algorithm:

  1. Take all fields from the object except hash.
  2. Sort by alphabet, connect via \n: auth_date=...\nfirst_name=...\nid=....
  3. Calculate secret_key = HMAC-SHA256("WebAppData", bot_token) -- no, stop. Login Widget is different from secret_key = SHA256(bot_token).
  4. Calculate HMAC-SHA256(data_check_string, secret_key).
  5. Compare the result with the hash field. Must match.
  6. Check auth_date - data should not be older than 24 hours.

Node.js:

javascript
const crypto = require('crypto');

function verifyTelegramAuth(data, botToken) {
  const { hash, ...rest } = data;

  // Формируем строку для проверки
  const dataCheckString = Object.keys(rest)
    .sort()
    .map(key => `${key}=${rest[key]}`)
    .join('\n');

  // Ключ для Login Widget: SHA256 от токена бота
  const secretKey = crypto
    .createHash('sha256')
    .update(botToken)
    .digest();

  // Вычисляем HMAC
  const computedHash = crypto
    .createHmac('sha256', secretKey)
    .update(dataCheckString)
    .digest('hex');

  // Проверяем подпись
  if (computedHash !== hash) {
    throw new Error('Недействительная подпись Telegram');
  }

  // Проверяем свежесть: не старше 24 часов
  const authDate = parseInt(rest.auth_date, 10);
  const now = Math.floor(Date.now() / 1000);
  if (now - authDate > 86400) {
    throw new Error('Данные авторизации устарели');
  }

  return true;
}

// Использование в Express
app.post('/auth/telegram', async (req, res) => {
  try {
    verifyTelegramAuth(req.body, process.env.BOT_TOKEN);

    // Подпись верна — ищем или создаём пользователя
    const user = await upsertUser({
      telegramId: req.body.id,
      firstName: req.body.first_name,
      lastName: req.body.last_name,
      username: req.body.username,
    });

    // Выдаём сессию или JWT
    req.session.userId = user.id;
    res.json({ ok: true });

  } catch (err) {
    res.status(401).json({ error: err.message });
  }
});

Python:

python
import hashlib
import hmac
import time

def verify_telegram_auth(data: dict, bot_token: str) -> bool:
    received_hash = data.get('hash')
    if not received_hash:
        raise ValueError('Отсутствует hash')

    # Строим строку для проверки
    data_check = {k: v for k, v in data.items() if k != 'hash'}
    data_check_string = '\n'.join(
        f'{k}={v}' for k, v in sorted(data_check.items())
    )

    # SHA256 от токена бота как ключ
    secret_key = hashlib.sha256(bot_token.encode()).digest()

    # HMAC-SHA256
    computed_hash = hmac.new(
        secret_key,
        data_check_string.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(computed_hash, received_hash):
        raise ValueError('Недействительная подпись')

    # Проверка времени
    auth_date = int(data.get('auth_date', 0))
    if time.time() - auth_date > 86400:
        raise ValueError('Данные авторизации устарели')

    return True

The widget problem in Russia

In Russia, Telegram Login Widget does not work, so authorization through Telegram is easier to implement using a bot: when sending a command, a bot calls a webhook, transferring a user name, in response, a one-time link is sent to enter. The bot needs to be run on a foreign hosting, since api.telegram.org should be available from it, the user can be anywhere.

This is not a bug – this is a consequence of the blocking of Telegram in 2018-2020. The widget accesses Telegram servers directly from the browser, and even if the lock is removed at the provider level, DNS and CDN can cause problems. Bot flow is not dependent on the availability of widget scripts.


OIDC via oauth.telegram.org: The New Standard Path

In 2025, Telegram released a full-fledged OpenID Connect Flow through oauth.telegram.org. This is an important update: Telegram authentication is now compatible with the standard that most frameworks and libraries support.

The official documentation now describes a full OpenID Connect via oauth.telegram.org: with JWKS, JWT and claims. PoC can log the user through the new OIDC, holds a cookie session for HTML pages and gives a couple of access + refresh tokens to the JSON API.

How OIDC Flow Works

Standard Authorization Code Flow:

plaintext
User → Your website → oauth.telegram.org/auth?client id=
Telegram confirms identity
Redirect to your callback with code
Your backend exchanges code for tokens through POST /token
Get JWT with user data
Check the signature of JWT through JWKS
python
# Example configuration for Python-social-auth / authlib
TELEGRAM OIDC CONFIG =
'issuer': 'https://oauth.telegram.org','
'authorization endpoint': 'https://oauth.telegram.org/auth','
'token endpoint': 'https://oauth.telegram.org/token','
'jwks uri': 'https://oauth.telegram.org/.well-known/jwks.json','
}

#Scope: openid profile

A cookie session is required so that HTML pages “remember” the user between requests. JWT pair – for JSON API (mobile/SPA).

The classic rule: Do not mix approaches unnecessarily. For server-side rendering – httpOnly cookie with session ID. For SPA and mobile applications – JWT access token (short TTL, 15-60 minutes) + refresh token (long TTL, httpOnly cookie or secure storage).

javascript
// Выдача сессии после успешной авторизации (Express + express-session)
app.get('/auth/telegram/callback', async (req, res) => {
  const { code } = req.query;

  // Обмениваем code на токены
  const tokens = await exchangeCodeForTokens(code);

  // Проверяем JWT через JWKS
  const userInfo = await verifyJWT(tokens.id_token);

  // Создаём или обновляем пользователя
  const user = await upsertUser({
    telegramId: userInfo.sub,
    firstName: userInfo.given_name,
    username: userInfo.preferred_username,
  });

  // Сохраняем в сессии
  req.session.userId = user.id;
  req.session.save(() => {
    res.redirect('/dashboard');
  });
});

When to choose OIDC

OIDC via oauth.telegram.org is the best choice if:

  • you are building a new app from scratch and want a standard flow
  • your framework or library supports OIDC (Next-Auth, Passport.js, Spring Security, etc.)
  • compatibility with other authorization providers in the same system

The widget remains a reasonable choice for simple scenarios and already running projects.


This method does not depend on the widget, does not require OIDC and works even where telegram.org is not available from the browser.

Principle

plaintext
User on the site → clicks “Login via Telegram”
The site generates a one-time token (UUID), saves in Redis
Shows the "Open Bot" button with the link t.me/YourBot?start=<token >>
User opens bot, sends /start <token >>
The bot makes a request to the site API: “Token X belongs to the user telegram id Y”
The site links the token with telegram id, creates a session
Authorization page polls API every 2 seconds, sees session – redirect

Implementation

Token generation and button display (Node.js/Express):

javascript
const { v4: uuidv4 } = require('uuid');
const redis = require('./redis'); // ваш Redis-клиент

app.get('/auth/telegram/init', async (req, res) => {
  const token = uuidv4();
  
  // Сохраняем токен на 5 минут, пока пользователь не авторизовался
  await redis.setex(`tg_auth:${token}`, 300, JSON.stringify({ status: 'pending' }));

  res.json({
    token,
    botUrl: `https://t.me/${process.env.BOT_USERNAME}?start=${token}`,
  });
});

// Опрос статуса авторизации (фронтенд спрашивает каждые 2 сек)
app.get('/auth/telegram/status/:token', async (req, res) => {
  const data = await redis.get(`tg_auth:${req.params.token}`);
  if (!data) return res.json({ status: 'expired' });
  res.json(JSON.parse(data));
});

Node-telegram-bot-api handler:

javascript
const TelegramBot = require('node-telegram-bot-api');
const bot = new TelegramBot(process.env.BOT_TOKEN, { polling: true });

bot.onText(/\/start (.+)/, async (msg, match) => {
  const token = match[1];
  const telegramId = msg.from.id;

  // Проверяем, что токен существует и ещё pending
  const data = await redis.get(`tg_auth:${token}`);
  if (!data || JSON.parse(data).status !== 'pending') {
    bot.sendMessage(telegramId, '❌ Ссылка недействительна или устарела.');
    return;
  }

  // Создаём или находим пользователя
  const user = await upsertUser({
    telegramId,
    firstName: msg.from.first_name,
    lastName: msg.from.last_name,
    username: msg.from.username,
  });

  // Привязываем токен к пользователю
  await redis.setex(
    `tg_auth:${token}`,
    60, // Ещё 60 секунд, чтобы сайт успел прочитать
    JSON.stringify({ status: 'authorized', userId: user.id })
  );

  bot.sendMessage(telegramId, '✅ Вы успешно вошли на сайт!');
});

Frontend: Status survey:

javascript
async function initTelegramAuth() {
  // Получаем токен и ссылку на бота
  const { token, botUrl } = await fetch('/auth/telegram/init').then(r => r.json());

  // Показываем кнопку
  document.getElementById('tg-link').href = botUrl;
  document.getElementById('tg-link').style.display = 'block';

  // Опрашиваем статус каждые 2 секунды
  const interval = setInterval(async () => {
    const status = await fetch(`/auth/telegram/status/${token}`).then(r => r.json());

    if (status.status === 'authorized') {
      clearInterval(interval);
      // Финализируем сессию и редиректим
      await fetch('/auth/telegram/finalize', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ token }),
      });
      window.location.href = '/dashboard';
    }

    if (status.status === 'expired') {
      clearInterval(interval);
      alert('Ссылка устарела. Попробуйте снова.');
    }
  }, 2000);
}

Mini App and initData: Authorization within Telegram

If your product is the Telegram Mini App, you have a different authorization mechanism. Telegram, when opening the Mini App, transmits to window.Telegram.WebApp.initData a string with user data and a cryptographic signature.

What is initData

initData is a URL-encoded string that Telegram throws into the Mini App when it opens. It contains fields: auth_date, user (JSON with id, name, avatar), hash (HMAC signature from Telegram), and several other service ones. The main rule of security: never trust initDataUnsafe.user directly - this is a field the client can fake in DevTools. You can only trust that you have passed the HMAC test on the backend.

InitData verification algorithm (different from Login Widget!)

For the Mini App, the key is built differently: HMAC-SHA256("WebAppData", bot_token) is not SHA256, but HMAC with the literal string "WebAppData".

javascript
const crypto = require('crypto');

function verifyInitData(initDataString, botToken) {
  const params = new URLSearchParams(initDataString);
  const hash = params.get('hash');
  params.delete('hash');

  // Сортируем параметры и формируем строку
  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([k, v]) => `${k}=${v}`)
    .join('\n');

  // Ключ: HMAC-SHA256("WebAppData", bot_token) — НЕ SHA256!
  const secretKey = crypto
    .createHmac('sha256', 'WebAppData')
    .update(botToken)
    .digest();

  const computedHash = crypto
    .createHmac('sha256', secretKey)
    .update(dataCheckString)
    .digest('hex');

  if (computedHash !== hash) {
    throw new Error('Недействительная подпись initData');
  }

  // Проверка времени: обычно допускают 1 час, не 24
  const authDate = parseInt(params.get('auth_date'), 10);
  if (Date.now() / 1000 - authDate > 3600) {
    throw new Error('initData устарела');
  }

  // Возвращаем данные пользователя
  return JSON.parse(params.get('user'));
}

Flow authorization in Mini App

javascript
// Фронтенд: отправляем initData на бэкенд при старте
const tg = window.Telegram.WebApp;
tg.ready();

async function authenticate() {
  const response = await fetch('/auth/miniapp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ initData: tg.initData }),
  });
  const { token } = await response.json();
  // Сохраняем JWT для последующих запросов
  localStorage.setItem('auth_token', token);
}
javascript
// Бэкенд
app.post('/auth/miniapp', async (req, res) => {
  const { initData } = req.body;

  try {
    const user = verifyInitData(initData, process.env.BOT_TOKEN);

    const dbUser = await upsertUser({ telegramId: user.id, ...user });

    // Выдаём JWT (короткий TTL — initData можно переиспользовать)
    const token = jwt.sign({ userId: dbUser.id }, process.env.JWT_SECRET, {
      expiresIn: '7d',
    });

    res.json({ token });
  } catch (err) {
    res.status(401).json({ error: err.message });
  }
});

If you do not check the signature, the attacker can fake the user. The lack of HTTPS is critical: Telegram requires HTTPS, but some have self-signed certificates. The entire front of the Mini App works in the Telegram browser, which means XSS can be critical. If there are open endpoints that give data without authentication, it is a leak.


Sessions and Security: What is important not to miss

javascript
Correct configuration of express-session
app.use(session)({)
secret: process.env.SESSION SECRET, // long random string
resave: false,
saveUninitialized: false,
cookie:
httpOnly: true, // JS does not have access to cookies
secure: true // HTTPS only
sameSite: 'lax', // CSRF protection
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
}
store: redisStore // Stored in Redis, not in memory
);

JWT: Do not store critical data in localStorage

javascript
LocalStorage Vulnerable to XSS
localStorage.setItem('token', jwt);

// Better for the web: httpOnly cookie
res.cookie('access token', jwt, {)
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 15 * 60 * 1000, // 15 minutes
};

For the Mini App, localStorage is acceptable, since the environment is more controlled, but the context of working in the Telegram browser still requires caution with XSS.

Major security errors

Ошибка Последствие Решение
Нет проверки hash на бэкенде Любой может войти под чужим ID Обязательная HMAC-верификация
Нет проверки auth_date Перехваченные данные работают вечно Проверяем TTL: 24ч для виджета, 1ч для initData
Сравнение строк через === Timing attack — можно угадать хеш по времени ответа Используйте crypto.timingSafeEqual
Хранить bot_token в коде Утечка токена = полный контроль бота Только через переменные среды
HTTP вместо HTTPS Перехват сессионной куки Только HTTPS, secure: true на cookie
Доверять initDataUnsafe.user XSS или DevTools позволяют подделать Только initData с HMAC-проверкой на сервере
javascript
// Правильное сравнение хешей (защита от timing attack)
const timingSafeEqual = (a, b) => {
  const bufA = Buffer.from(a, 'hex');
  const bufB = Buffer.from(b, 'hex');
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);
};

Website and bot communication: one user in two systems

If you have a website and a Telegram bot at the same time, the communication key is telegram_id. It is unique, does not change, does not depend on the username (username user can change).

sql
- User table diagram
Create table users (
id SERIAL PRIMARY KEY,
telegram id BIGINT UNIQUE NOT NULL - the main key from Telegram
username VARCHAR(64) - may change
first name VARCHAR(128)
created at TIMESTAMPTZ DEFAULT NOW(),
- Your business fields.
plan Varchar(32) DEFAULT 'free',
email VARCHAR(255)
);

Then both the site and the bot work with one user.id from your database linked via telegram_id. The user can configure something in the bot - changes are immediately visible on the site, and vice versa.


Comparison of methods: what to choose

Критерий Login Widget OIDC Бот-флоу Mini App initData
Работает в России Нестабильно Да Да Да (внутри Telegram)
Сложность реализации Средняя Средняя Низкая Средняя
Стандарт Нет OpenID Connect Нет Нет
Нужен бэкенд Обязательно Обязательно Обязательно Обязательно
Нужен бот Да (для настройки) Да Да (активно) Да (активно)
UX Хороший Хороший Чуть сложнее Лучший в экосистеме
Подходит для SPA Да Да Да Только внутри TG
Подходит для SSR Да Да Да Только внутри TG
Безопасность HMAC-SHA256 JWT/JWKS Одноразовый токен HMAC-SHA256 (иначе)

Scenario recommendations

Public site, audience not only in Russia → Login Widget or OIDC. The widget is easier and faster to implement. OIDC – if you already have OAuth infrastructure.

Site with audience mainly from Russia → Bot-floo with a one-time link. It does not depend on the availability of Telegram scripts.

**Telegram Mini App ** InitData only. Other methods are irrelevant within the ecosystem.

*Corporate Tool or Closed Community → Bot Flow. You can add a check that the user is only logged in if telegram_id is in the current list.


Wibcoding and Telegram-authorization: where AI helps, where not

Authorization is where Claude Code or Codex can write 80% of the code for you. But the remaining 20% requires you to understand: the cryptographic algorithm must be checked manually, temporary checks cannot be omitted, the storage of secrets cannot be delegated to “write sometime”.

A good prompt for Claude Code:

plaintext
Implement authorization via Telegram Login Widget on Express.js.
Requirements:
Check HMAC-SHA256 with bot token from .env
Check auth date (not older than 24 hours)
- Comparison of hashes through crypto.timingSafeEqual
- After successful verification, session via express-session + Redis
Upsert of the user in PostgreSQL by telegram id
Endpoint GET /auth/telegram/callback for redirect widget mode
Endpoint POST /auth/telegram for JavaScript colback
Do not use any npm packets to verify Telegram – only built-in crypto.

Bad prompt:

plaintext
Make authorization through Telegram

The result of the second is often code without signature verification, with hash in the comparison bar via ===, and no time check. This is a working but insecure code.


Implementation checklist

plaintext
Preparation:
Created bot via @BotFather, received token
● In BotFather, the site domain (setdomain) is specified - for Login Widget
BOT TOKEN is stored in environment variables, not code
● The site works on HTTPS.

Backend:
HMAC signature verification (SHA256 for widget, HMAC for initData)
● Checked auth date (not older than 24h for widget, 1h for initData)
● Comparison of hashes using timingSafeEqual, not via ===
Implemented upsert by telegram id (not by username!)
Session or JWT is issued only after successful verification
Cookies: httpOnly, secure, sameSite

Sessions:
Sessions are stored in Redis or PostgreSQL, not in Node.js memory
Installed TTL for sessions
Exit implemented (session deletion/JWT disability)

Safety:
● No trust initDataUnsafe.user without HMAC verification
● All API endpoints require authentication
● Attempts of unsuccessful authorization are logged

Testing:
Verified authorization in real Telegram (not in the test client)
● Script with outdated data (auth date > 24h)
● Screenplay with the substitution of hash

Outcome

Authorization through Telegram is a powerful tool: the user does not create a new password, does not pass unnecessary steps, you get a verified identity. But it only works properly if the backend checks the signature.

**Top rule: Telegram data is only a hint. They become trusted after you have verified the HMAC on the server with your bot token.

The choice of the mechanism is determined by the scenario: Login Widget is a classic, OIDC is a standard of the future, bot-flow is reliability for the Russian-speaking audience, initData - if you are already inside the Telegram ecosystem.


FAQ

Do you need a separate bot for Login Widget? ** Yes, but it is only used as an identity provider. The user is not obliged to write a bot – it is enough that the bot exists and the domain of your site is specified in BotFather.

**What is better than a username for storage in a database? ** Username can be changed at any time. telegram_id is a constant numeric identifier that never changes. Always use it as your primary key.

**Is it possible to implement authorization without Redis? ** For bot-floo, yes, you can store disposable tokens in PostgreSQL with the expires_at field. For sessions, Redis or PostgreSQL with session id index is recommended. Storing sessions in Node.js memory does not reboot.

How do I call off the session? ** With Redis-storage, delete the session key. With JWT, you need a blacklist (the same Redis with a list of recalled jti). Without blacklist, JWT cannot be recalled before the TTL expires – so use short TTL (15–60 minutes) + refresh token for critical scenarios.

**Telegram Mini App or a regular website - which is better for logging in via Telegram? ** Different audiences. Mini App is for users who are already in Telegram and want to stay in the ecosystem. A regular site with Login Widget is for a wider audience. Many projects implement both entrances.


*The data is current as of June 2026. Telegram continues to develop the ecosystem – check the official documentation of core.telegram.org and the updates to oauth.telegram.org. *

$ cd ../ ← back to Telegram bots