· 8 min read
Securing Telegram Mini Apps: Verifying Init Data, Hardening Auth, and Avoiding Trust Bugs
Telegram Mini Apps are convenient, but convenience can hide dangerous trust assumptions. This guide focuses on the most important security control: treating everything from the client as untrusted and verifying Telegram-signed init data correctly on the server.
Telegram Mini Apps are built to feel native inside chats, but that convenience creates a security trap: developers often trust fields coming from the client without proving they came from Telegram. In practice, the most important security boundary in a Mini App is not the WebView itself, but your server’s ability to verify that a request actually originated from a real Telegram session.
This guide focuses on one security concern that causes the most damage in production: forging user identity by trusting client-provided data. If you build Mini Apps, you should treat the browser, the JavaScript bridge, and any payload received from the front end as attacker-controlled until the backend validates it.
The core rule: never trust user.id from the browser
A common mistake looks like this:
- the Mini App reads
window.Telegram.WebApp.initDataUnsafe.user.id - the client sends that
user.idto your API - the server uses it as the authenticated user identifier
This is insecure because initDataUnsafe is explicitly unsafe: it is convenient for UI rendering, but it is not a security proof. An attacker can modify requests, replay values, or fake the entire payload if your backend accepts it without verification.
The correct model is:
- Telegram opens the Mini App and provides signed init data.
- The client forwards the raw
initDatastring to your backend. - Your backend verifies the signature using your bot token.
- Only after verification do you trust the embedded user identity and metadata.
What must be verified
Telegram Mini Apps include an initData string that contains key-value pairs such as:
userchat_instanceauth_datequery_idhash
The security control is the hash. Your server must reconstruct the signing payload exactly as specified by Telegram and compare the computed HMAC to the supplied hash.
If this comparison fails, the request must be rejected.
Important properties to enforce:
- the hash comparison must be constant-time
auth_datemust be recent enough to prevent replay- all parsing must preserve the exact canonical form used in verification
- the bot token must never be exposed to the client
Server-side verification flow
A robust verification flow should look like this:
- Receive
initDataas a raw string from the client. - Parse it into key-value pairs.
- Remove the
hashparameter from the set used to compute the check string. - Sort the remaining parameters lexicographically by key.
- Join them as
key=valuelines separated by newline characters. - Compute the secret key from the bot token.
- Compute the HMAC over the check string.
- Compare the resulting digest with the provided
hashusing constant-time comparison. - Check
auth_dateagainst an acceptable freshness window. - If valid, extract the Telegram user and create or continue the session.
If any step fails, do not partially accept the request.
Example backend implementation pattern
Below is a simplified pattern in pseudocode. Adapt the cryptographic primitives to your stack.
function verifyInitData(initData, botToken):
params = parseQueryString(initData)
receivedHash = params["hash"]
remove params["hash"]
checkString = join(
sortByKey(params)
.map(key => key + "=" + params[key]),
"\n"
)
secretKey = HMAC_SHA256(key="WebAppData", message=botToken)
computedHash = HMAC_SHA256(key=secretKey, message=checkString)
if not constantTimeEqual(hex(computedHash), receivedHash):
reject("invalid signature")
authDate = int(params["auth_date"])
if now() - authDate > 86400:
reject("stale init data")
return params
There are two implementation details that often go wrong:
1. The signing key derivation is not optional
Do not reuse the bot token directly as the HMAC key unless your framework or Telegram’s current specification explicitly says so for the verification step you are implementing. Follow the exact derivation rule for Mini App init data verification in the documentation you are targeting.
2. String canonicalization must be exact
Verification fails if you:
- sort incorrectly
- decode and re-encode values inconsistently
- normalize characters unexpectedly
- drop empty values
- alter newline handling
The backend should validate the raw incoming string, not a JSON object reconstructed by the client.
Use initDataUnsafe only for display
The Telegram JavaScript bridge exposes both safe and unsafe forms of init data for a reason.
initDataUnsafeis fine for immediate UI presentation, such as showing the user’s display name or avatar before the server round-trip completes.- It is not acceptable as proof of identity.
A safe pattern is:
- use
initDataUnsafefor optimistic UI rendering - send
initDatato the backend on app startup - wait for the server to validate before enabling sensitive actions
Sensitive actions include:
- creating orders
- changing account settings
- transferring points or credits
- reading private data
- issuing privileged API calls
Replay attacks: signature validation is not enough
A valid signature proves the payload came from Telegram, but it does not by itself prove freshness. An attacker who captures a valid initData string may attempt to replay it.
To reduce replay risk:
- enforce a maximum age for
auth_date - create short-lived application sessions after verification
- bind your own session token to the verified Telegram user
- rotate or invalidate server sessions on logout
- log repeated verifications from suspicious IPs or devices
A practical freshness window is typically measured in minutes or hours depending on UX. If your app requires long-lived sessions, do not keep reusing the original init data as the sole proof of identity. Use it once, then mint your own session credential.
Protect the login boundary, not just the API
Many Telegram Mini App vulnerabilities appear at the first authenticated endpoint. Developers verify the payload once, then assume every later request is safe because it came from the same browser.
That is only safe if you issue and validate your own session token after the initial Telegram verification.
Recommended architecture:
- Telegram
initDatais a bootstrap credential - your backend verifies it and creates a session
- subsequent API requests use your session mechanism
- the Telegram payload is not re-parsed on every request unless needed for re-authentication
This reduces complexity and prevents accidental trust in front-end state.
Common mistakes that lead to account takeover
Accepting user.id from the client
This is the most obvious flaw and still one of the most common. If the client sends a user ID and the server accepts it without validating Telegram’s signature, identity is forgeable.
Verifying only part of the payload
Checking auth_date but not the hash, or checking the hash but not freshness, leaves room for abuse.
Using unsafe parsing logic
Query-string parsing bugs are frequent. Some frameworks coerce repeated parameters, transform + and %20 differently, or sort parameters implicitly. Use a parser that preserves raw values.
Logging full init data
If you log the entire init data string in plaintext, you may be storing reusable authentication material. Logs are often copied to third-party systems and retained longer than you expect.
Trusting frontend role flags
If your client says is_admin=true, that means nothing unless the server derived that role from a trusted source.
Reusing the init payload as an API credential
The init payload is meant for startup verification, not as a permanent bearer token.
What to store after verification
After the backend verifies the payload, store only the minimum useful identity data:
- Telegram user ID
- display name if needed
- username if present
- avatar file reference if needed
- auth timestamp
Do not persist the raw hash or the full init data unless you have a very specific auditing requirement and a secure storage design.
If you need to reproduce a login event later, store a hashed or redacted audit record rather than the full credential.
Session design recommendations
A secure Mini App usually needs two layers:
Layer 1: Telegram proof
Use the signed init data once to prove the user is who they claim to be.
Layer 2: Application session
Issue your own session token or server-side session after verification.
Good properties of the app session:
- short or moderate lifetime
- revocable server-side
- bound to the verified Telegram user
- optionally tied to device or WebView metadata
Do not rely on the Telegram payload as your long-term auth mechanism.
Defense in depth for privileged actions
Even with correct init data verification, high-risk operations deserve extra checks.
For example:
- require re-authentication for payouts or destructive actions
- verify permissions on the server for every sensitive endpoint
- check business rules independently of the client
- rate-limit transaction creation and mutation endpoints
- audit actions by verified Telegram user ID
Security should not depend on the frontend’s visibility into permissions.
Testing your verification code
A good Mini App security review should include negative tests, not just happy-path tests.
Test cases to include:
- modified
user.id - altered
auth_date - changed parameter ordering
- missing
hash - stale payloads
- repeated payload replay
- malformed query strings
- Unicode and escaping edge cases
- mismatched constant-time compare behavior
If possible, generate a known-good payload in a test fixture and compare your implementation against it in multiple languages.
Operational hardening
Beyond signature validation, protect the surrounding system:
- serve the app and API over HTTPS only
- set strict CORS rules for your frontend origin
- avoid exposing admin endpoints to the Mini App context unless necessary
- secure bot token storage with environment secrets or a secret manager
- rotate bot tokens if there is any suspicion of leakage
- monitor anomalous login rates and user-agent patterns
A bot token leak is serious because it can undermine any server-side verification that depends on it.
A practical security checklist
Before shipping a Telegram Mini App, confirm that:
- the backend verifies raw
initData - the signature comparison is constant-time
auth_datefreshness is enforced- the bot token never reaches the browser
initDataUnsafeis used only for non-security UI- sensitive actions require a validated session
- replay protection is in place
- logs do not store secrets in full
- every privileged endpoint performs server-side authorization
If any item is missing, the app is likely trusting the client too much.
Final takeaway
The most important security principle for Telegram Mini Apps is simple: the browser is not an identity provider. Telegram gives you signed startup data so your backend can prove the user’s identity, but that proof only exists if you validate it correctly and then move to your own session model.
If you implement init data verification carefully, keep the client untrusted, and treat the Telegram payload as a bootstrap credential rather than a session token, you eliminate the most common and most damaging Mini App auth bugs.