· 7 min read
Securing Telegram Bots for One-Time Verification Links
A practical guide to building Telegram bots that issue one-time verification links without leaking tokens, replaying callbacks, or trusting unbounded chat state. Learn how to design a bot flow that stays safe even when messages are forwarded, chats are reused, or users retry the flow.
Telegram bots are often used for support, onboarding, alerts, and automation. A newer and increasingly useful pattern is using a bot as a one-time verification broker: the bot messages a user, the user taps a link or button, and your backend uses that confirmation to unlock an account, sign a device, or authorize a sensitive action.
This flow is simple on the surface, but it creates a very specific security problem: you are no longer just handling chat messages. You are now handling short-lived secrets, callback authenticity, replay prevention, and identity binding.
This tutorial shows how to build that flow safely.
The use case
Imagine a SaaS product that needs to confirm a user before:
- approving an admin action,
- linking a device,
- authorizing a password reset,
- or confirming an email change.
Instead of sending a code by email, you send a Telegram message from your bot:
- The user receives a message with a button.
- The button opens a verification action.
- Your backend validates the action.
- The bot marks the request as confirmed.
This works well because Telegram gives you a fast, familiar user channel. But the security model is easy to get wrong.
Threat model first
Before writing code, define what you must protect.
For one-time verification links, the main risks are:
- Token leakage: a verification token appears in logs, browser history, forwarded messages, or analytics.
- Replay: a captured token or callback is reused after the first approval.
- Cross-user confusion: one user can approve another user’s request.
- Chat spoofing: your backend trusts a message without verifying it came from your bot.
- Race conditions: two requests finalize the same action.
- State loss: your bot forgets what it is verifying and falls back to insecure defaults.
The right design prevents those by binding every action to a server-side record and by using Telegram only as a delivery and interaction channel.
Prefer opaque server-side challenges
Do not embed meaningful data directly in a Telegram button or deep link.
Bad pattern:
https://example.com/verify?user_id=123&action=reset&approved=true
Even if you sign this, you are exposing structure and inviting replay or parameter tampering.
Better pattern:
- Generate a random opaque token on your server.
- Store it with the intended user, action, expiry, and status.
- Put only the opaque token in the link or callback payload.
- Look up the token server-side and enforce policy there.
A good token should be:
- high entropy,
- single-use,
- time-limited,
- and useless on its own without server state.
Use two separate identifiers
For safer designs, keep these values separate:
- request ID: identifies the verification request in your database.
- secret token: proves possession of the link or message.
Why split them?
If someone guesses or learns a request ID, they still cannot approve it without the secret token. If someone obtains a secret token, it only works for one request and only until it expires.
A practical record might look like this:
{
"request_id": "vrq_7f0d...",
"user_id": 456789,
"action": "device_link",
"telegram_chat_id": 99887766,
"token_hash": "...",
"status": "pending",
"expires_at": "2026-08-27T12:45:00Z",
"used_at": null
}
Store the token as a hash if possible. If your database leaks, raw tokens should not be immediately reusable.
Bind the request to the Telegram identity you expect
Telegram bots know the sender ID in updates, but that does not mean every update is trustworthy. You must verify that the update matches the request you created.
When starting a verification request, save:
- Telegram
user_id - Telegram
chat_id - the action being authorized
- the expiry time
- the current status
When a callback arrives, check:
- Does the token exist?
- Is it unexpired?
- Is it still pending?
- Does the Telegram sender match the expected user?
- Does the chat match the expected chat if your workflow depends on chat context?
- Is this request eligible for this action right now?
Do not rely on the chat alone. Users can move between devices, forward content, or interact from a different context than you expected.
Use inline buttons for confirmation, not free-text commands
For security-sensitive flows, inline buttons are usually better than manual commands.
Why?
- They reduce ambiguity.
- They let you embed a server-generated callback reference.
- They are easier to validate atomically.
- They reduce accidental approval through copied text.
A good confirmation message might say:
Approve device link for MacBook Pro?
This request expires in 10 minutes.
Buttons:
- Approve
- Deny
Each button sends a callback that contains only a short opaque reference.
Make callback handling idempotent
Telegram clients may retry interactions, users may tap twice, and your own backend may process the same update more than once.
Your callback handler must be idempotent.
That means:
- approving a pending request once changes state to approved,
- any later approval attempt sees the request is already finalized,
- duplicate updates do not create duplicate side effects.
Use a database transaction or compare-and-swap logic:
begin transaction
request = load_for_update(token)
if request.status != "pending":
return already_processed
if request.expired():
request.status = "expired"
commit
return expired
request.status = "approved"
request.used_at = now()
commit
This is the difference between a secure workflow and a workflow that can be double-approved under load.
Never trust forwarded messages
A Telegram user can forward messages, including bot messages, into another chat.
If your bot uses message content as proof of identity, forwarding becomes a problem.
Avoid patterns like:
- “If the user can quote the code, they must own the request.”
- “If the message text matches, approve it.”
Instead, treat forwarded content as untrusted. Only accept:
- server-generated tokens,
- callback queries from the bot’s own inline buttons,
- or a link that resolves to a server-side lookup with fresh validation.
Keep tokens out of logs
Verification tokens are secrets. They should not appear in:
- application logs,
- reverse-proxy logs,
- analytics,
- error tracking,
- or client-side traces.
This is easy to miss because developers often log entire updates during debugging.
Safer logging strategy:
- log request IDs, not raw tokens,
- mask callback data,
- redact query parameters,
- and disable verbose update dumps in production.
If you must debug a production issue, sample logs carefully and redact secrets before they are persisted.
Add expiry and a short replay window
One-time verification should really mean one-time and short-lived.
Recommended defaults:
- expire in 5–15 minutes,
- invalidate immediately after use,
- reject any request after expiry,
- and store
used_atto block replays.
Short expiry matters because Telegram interactions can be delayed. If a user comes back later, issuing a new challenge is safer than keeping the old one alive.
Handle login or account linking with extra care
If your bot verifies a Telegram user for account linking, you must prove both sides of the relationship:
- the Telegram account belongs to the user currently in the bot,
- and the external account being linked is the one intended.
A safe pattern is:
- User initiates linking in your app.
- Your backend creates a pending link request.
- The bot sends a one-time approval button to the Telegram account already associated with that user.
- The callback only finalizes the exact pending request.
Do not let Telegram alone become the sole account recovery factor unless you have strong anti-abuse controls.
Rate-limit request creation and callback attempts
Bot security is not just about token validation. You also need abuse controls.
Rate-limit:
- how many verification requests a user can create,
- how many failed callback attempts a token can receive,
- and how often a chat can trigger sensitive actions.
Useful controls:
- per-user limits,
- per-IP limits for web-based redirects,
- per-request failure counters,
- and temporary lockouts after unusual activity.
This helps prevent brute force attempts against short codes or excessive spam to a user’s Telegram account.
Use separate paths for initiation and approval
A clean design separates the request creation step from the approval step.
Initiation
Created by your backend or app when the user starts a sensitive action.
- generates request record,
- stores expected Telegram identity,
- sends bot message with inline button,
- marks request as pending.
Approval
Triggered by the Telegram callback.
- verifies token and sender,
- checks expiry and state,
- finalizes the request,
- emits an internal event,
- sends confirmation back to the user.
This separation prevents your bot from doing too much in one handler and keeps the trust boundary clear.
Example flow
Here is a secure end-to-end pattern.
1. Create a request
Your backend creates a pending verification request:
request_id = random_id()
secret = random_secret()
store(
request_id=request_id,
token_hash=hash(secret),
user_id=telegram_user_id,
action="device_link",
status="pending",
expires_at=now()+10m
)
2. Send a Telegram message
The bot sends:
Device link request
Approve linking this device?
Expires in 10 minutes.
The inline button callback contains only the request_id and maybe a short server-generated reference, never the raw secret.
3. User taps Approve
Telegram sends a callback query to your bot.
4. Backend validates everything
Your handler:
- verifies the callback came from the expected Telegram user,
- loads the pending request,
- checks expiry,
- checks status,
- verifies the secret reference,
- finalizes once.
5. Final state
Return a new message like:
Approved. Your device is now linked.
And on the backend, mark the request as used so it cannot be replayed.
Test the attacks, not just the happy path
A good bot security review includes negative testing.
Try these cases:
- reuse the same callback twice,
- tap Approve after expiry,
- send the callback from another Telegram account,
- forward the message to another chat and interact there,
- submit malformed callback data,
- simulate duplicate update delivery,
- and inspect logs for secret leakage.
If any of those succeed, your flow is not ready.
Minimal checklist
Before shipping a one-time verification bot flow, confirm that:
- all secrets are opaque and server-generated,
- tokens expire quickly,
- every request is single-use,
- callback handling is idempotent,
- sender identity is checked,
- forwarded messages are ignored,
- logs redact secrets,
- and rate limiting is in place.
Final thought
Telegram bots are great for fast user confirmation, but security depends on treating Telegram as an interaction layer, not the source of truth.
If you bind each verification request to server-side state, keep secrets opaque, and make approval handlers idempotent, you can safely use Telegram for a modern one-time verification workflow without turning your bot into a replay or spoofing target.