· 4 min read

Telegram Mini Apps: The Common Mistake That Breaks Auth, and How to Fix It

A lot of Telegram Mini App bugs come from one place: developers trust the client too early. This tutorial shows how to correctly validate the init data, avoid spoofed users, and structure your Mini App auth flow so it works reliably in production.

Telegram Mini Apps are a great way to bring app-like experiences into Telegram chats. But many developers hit the same production bug: they treat data from the WebView as if it were already trusted.

That mistake usually shows up in one of these ways:

  • reading user.id from the frontend and using it to identify the user on the server
  • accepting requests from the Mini App without verifying Telegram’s signature
  • storing session state before validating the launch data
  • assuming the data is secure because it came from Telegram

The core lesson is simple: the Mini App client is not the source of truth. Your backend must verify the launch payload before it trusts any user identity or business action.

Why this mistake happens

Telegram Mini Apps run inside a WebView. The frontend receives launch parameters such as user info, chat context, and other metadata. Because the app is embedded inside Telegram, it feels trustworthy.

That feeling is dangerous.

The client can be modified, scripts can be replayed, and values can be copied from one environment to another. If your backend accepts a user ID or role from the browser without checking Telegram’s signed data, you’ve created an easy impersonation path.

What you should trust

In a proper Mini App auth flow, trust only this sequence:

  1. Telegram launches your Mini App.
  2. Your frontend receives initData.
  3. The frontend sends initData to your backend.
  4. Your backend verifies the signature using your bot token.
  5. Only after verification do you create or update a session.

Everything else is untrusted until step 4 succeeds.

The wrong way

Here is the pattern that causes trouble:

// Frontend
const userId = window.Telegram.WebApp.initDataUnsafe.user.id;
await fetch('/api/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ userId })
});

And on the server:

app.post('/api/login', async (req, res) => {
  const { userId } = req.body;
  const user = await db.users.findByTelegramId(userId);
  // trusts the client-supplied id
  res.json({ ok: true, user });
});

This is broken because the browser can send any userId. Even if Telegram populated initDataUnsafe in a real session, your backend did not verify that the value came from Telegram.

The right way: verify initData

Telegram provides signed launch data in initData. That string must be validated on the server before you use any embedded user information.

The exact verification logic depends on your stack, but the flow is consistent:

  • parse the query string values from initData
  • exclude the hash field from the data set
  • build the data check string in sorted order
  • derive the secret key from your bot token
  • compute the HMAC
  • compare it with Telegram’s hash

If the hashes do not match, reject the request.

Example backend verification in Node.js

A minimal verification helper looks like this:

import crypto from 'crypto';

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

  const dataCheckString = [...params.entries()]
    .sort(([a], [b]) => a.localeCompare(b))
    .map(([key, value]) => `${key}=${value}`)
    .join('\n');

  const secretKey = crypto
    .createHash('sha256')
    .update(botToken)
    .digest();

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

  return hash === calculatedHash;
}

Then use it in your login endpoint:

app.post('/api/login', async (req, res) => {
  const { initData } = req.body;

  if (!verifyTelegramInitData(initData, process.env.BOT_TOKEN)) {
    return res.status(401).json({ error: 'Invalid Telegram data' });
  }

  const params = new URLSearchParams(initData);
  const userJson = params.get('user');
  const user = JSON.parse(userJson);

  const telegramId = user.id;
  const session = await createSessionForTelegramUser(telegramId);

  res.json({ ok: true, session });
});

A second common mistake: using initDataUnsafe for authorization

Telegram’s frontend API often exposes both raw and parsed values. The parsed object is convenient, but it is still frontend data.

Use initDataUnsafe only for UI hints:

  • showing the current user’s name
  • pre-filling a profile screen
  • choosing a language or theme

Do not use it for:

  • determining who is logged in
  • checking subscription status
  • assigning admin access
  • approving payments
  • deciding whether a user owns a resource

Those decisions belong on the server, after verification.

Add replay protection

Signature verification proves the request was issued by Telegram, but it does not automatically solve replay concerns in your app logic.

For sensitive flows, also check:

  • auth_date is recent enough
  • the same initData is not reused for one-time actions
  • the session is bound to the verified Telegram user

A practical pattern is to issue your own application session after Telegram verification and then rely on that session for later requests.

A robust Mini App auth setup usually looks like this:

  1. Frontend loads inside Telegram.
  2. Frontend immediately sends initData to the backend.
  3. Backend verifies the signature and timestamp.
  4. Backend creates an app session or JWT tied to the Telegram user ID.
  5. Frontend uses that session for subsequent API calls.
  6. Backend re-checks permissions on every sensitive action.

This keeps Telegram-specific logic at the edge and your app logic separate.

Debugging tip: test with invalid data on purpose

If your verification is working, bad requests should fail cleanly.

Try these tests during development:

  • remove the hash field
  • change one character in the user payload
  • replay an old initData string
  • send a request without Telegram launch data

Your server should reject all of them.

If they still pass, your auth layer is not secure enough.

What to remember

The most common Telegram Mini Apps mistake is trusting the client because it looks like it came from Telegram. It did not come from Telegram in a form you can trust until your backend verifies the signature.

If you remember only one rule, make it this:

Never authorize a Telegram Mini App user based on frontend data alone.

Verify initData, create your own server-side session, and keep all sensitive permissions on the backend. That one change prevents a large class of auth bugs and security issues in Mini Apps.

← All posts