Securing Webhook Endpoints with Secret Tokens
Welcome to the next lesson in our journey to build and deploy Telegram bots. In our last session, you successfully transitioned your bot from long polling to a webhook-based architecture, using Bun.serve and ngrok to receive updates in real-time. However, we ended on a crucial cliffhanger: your webhook URL is currently public and unprotected. Anyone who finds it can send fake updates, potentially causing your bot to malfunction or incur unwanted costs.
This lesson directly addresses that vulnerability. You will learn how to secure your webhook endpoint using a secret token, the standard mechanism recommended by Telegram. We'll explore why this is critical, how the token-based header authentication works, and then implement it within your grammY application.
By the end of this lesson, you will have a robust and secure webhook that only accepts legitimate updates from Telegram, making your bot ready for its final deployment to the cloud.
The Critical Need for Webhook Security
Before we dive into the implementation, it's essential to understand the risks of an unsecured webhook. An open endpoint is a target. Malicious actors could bombard your bot with requests, and if your bot interacts with paid APIs (like an AI model), this could lead to significant and unexpected costs. It could also be used to inject bad data or trigger unintended actions.
The following video, while not specific to Telegram, provides an excellent overview of the general risks and common methods for securing webhooks.
n8n Webhook Security: Learn This Before It’s Too Late
This video from the Nate Herk channel clearly explains the financial and data privacy risks of unsecured webhooks.
Watch the introductory segment from the beginning to understand the potential consequences. Then, watch the explanation of Header Auth, as this is the exact type of mechanism that Telegram uses.
As the video explains, one of the most common and effective security methods is header-based authentication. This is precisely the approach Telegram has built into its Bot API.
The Secret Token Authentication Flow
The mechanism is a straightforward challenge-response system based on a shared secret:
- You Generate a Secret: You create a long, random, and unpredictable string. This is your
secret_token. - You Tell Telegram Your Secret: When you call the
setWebhookAPI method, you include yoursecret_tokenas a parameter. - Telegram Sends a Custom Header: From that point on, every single update that Telegram POSTs to your webhook URL will include a special HTTP header:
X-Telegram-Bot-Api-Secret-Token. The value of this header will be your secret token. - Your Server Verifies the Header: Your web server's first job upon receiving a request is to inspect the headers. It must check for the existence of
X-Telegram-Bot-Api-Secret-Tokenand verify that its value matches the secret you configured.- If the header is present and the value matches, the request is legitimate. Your server processes the update.
- If the header is missing or the value is incorrect, the request is from an unauthorized source. Your server must immediately reject it, typically by returning an HTTP
401 Unauthorizedstatus code, without passing the update to your bot logic.
This flow ensures that only Telegram's servers, which know the secret you provided, can trigger your bot's logic.
The grammY documentation provides a clear definition of the options involved, including the secretToken.
1.38.0 • npm-grammy • tessl • Registry • Tessl
The grammY documentation details the options for configuring webhooks.
Please review the Webhook Options interface. Pay close attention to the secretToken property and its description; it explicitly states that this is used to validate the X-Telegram-Bot-Api-Secret-Token header.
Implementing the Secret Token in Your Bot
Now, let's secure your bot. The process involves updating both your server code and your webhook management script.
Step 1: Generate and Store Your Secret
First, we need a strong, random secret. You can use Bun to generate a UUID, which is perfectly suitable for this purpose.
bun -e "console.log(crypto.randomUUID())"
This will output a unique string like a1b2c3d4-e5f6-7890-a1b2-c3d4e5f67890. Copy this value.
Next, open your .env file and add this new secret. This keeps your credentials separate from your code.
.env
TELEGRAM_BOT_TOKEN="your_bot_token_here"
# Add your new secret token
TELEGRAM_WEBHOOK_SECRET="your_generated_uuid_here"
Step 2: Update Your Bot Server
Now, let's configure your Bun server to use this secret. grammY makes this incredibly simple. The webhookCallback function we are already using accepts an options object where we can provide the secret token. It will then handle the header verification for you automatically.
Modify your src/index.ts file to load the secret and pass it to the callback.
src/index.ts
import { webhookCallback } from "grammy";
import { bot } from "./bot";
const secret = process.env.TELEGRAM_WEBHOOK_SECRET;
if (!secret) throw new Error("TELEGRAM_WEBHOOK_SECRET is not set");
// Create a webhook callback that validates the secret token
const handleUpdate = webhookCallback(bot, "std/http", {
secretToken: secret,
});
Bun.serve({
// The fetch handler now uses the secured callback
fetch: async (req) => {
try {
const url = new URL(req.url);
if (url.pathname === "/") {
// Use the handleUpdate function that includes the secret token check
return await handleUpdate(req);
}
return new Response("Not Found", { status: 404 });
} catch (err) {
console.error(err);
return new Response("Internal Server Error", { status: 500 });
}
},
port: 3000,
error() {
return new Response("Internal Server Error", { status: 500 });
},
});
console.log("Bot server listening on port 3000...");
Notice that we've passed a secretToken option to webhookCallback. Under the hood, grammY will now perform the header check on every incoming request before handing it off to your bot's middleware.
Step 3: Update the Webhook Management Script
Finally, we need to tell Telegram what our secret is. We'll modify the src/webhook.ts script to include the secret_token parameter when it calls bot.api.setWebhook.
src/webhook.ts
import { bot } from "./bot";
import "dotenv/config"; // Ensure .env is loaded
const command = process.argv[2];
async function setupWebhook() {
const url = process.argv[3];
if (!url) {
console.error("Please provide a URL for the webhook.");
process.exit(1);
}
// Load the secret from the environment
const secret = process.env.TELEGRAM_WEBHOOK_SECRET;
if (!secret) {
console.error("TELEGRAM_WEBHOOK_SECRET is not set in .env file.");
process.exit(1);
}
try {
// Pass the secret_token when setting the webhook
await bot.api.setWebhook(url, {
secret_token: secret,
});
console.log("Webhook set successfully to:", url);
await getWebhookInfo();
} catch (error) {
console.error("Failed to set webhook:", error);
}
}
// ... (deleteWebhook and getWebhookInfo functions remain the same)
async function deleteWebhook() {
//...
}
async function getWebhookInfo() {
//...
}
// ... (switch statement remains the same)
switch (command) {
//...
}
We've added logic to load the TELEGRAM_WEBHOOK_SECRET and pass it inside an options object to setWebhook. The parameter name here is secret_token as specified by the Telegram Bot API.
Testing the Secure Endpoint
Let's verify that everything is working as expected.
-
Terminal 1: Start Your Bot Server
bun startYour secured server is now running.
-
Terminal 2: Start ngrok
ngrok http 3000Copy the public HTTPS URL from
ngrok. -
Terminal 3: Register the Secured Webhook
Use your updated script to set the webhook. It will now also register your secret.bun src/webhook.ts set <YOUR_NGROK_URL>Check the output of the
getWebhookInfocall. It should now showhas_custom_certificate: false(or true depending on ngrok), but importantly, it confirms the webhook is set.
Now, send a message to your bot in Telegram. It should reply just as before. The flow is working.
But is it secure? Let's prove it. In Terminal 3, use curl to send a request to your webhook URL without the secret header.
# Replace <YOUR_NGROK_URL> with the actual URL
curl -X POST -H "Content-Type: application/json" \
-d '{"update_id":999,"message":{"text":"fake message"}}' \
<YOUR_NGROK_URL>
You will receive a 401 Unauthorized response. Your server rejected the request! Check the ngrok terminal (Terminal 2); you will see a 401 status code for that request. grammY's webhookCallback has done its job.
Now, let's try again, this time with the correct header.
# Replace <YOUR_NGROK_URL> and <YOUR_SECRET>
curl -X POST -H "Content-Type: application/json" \
-H "X-Telegram-Bot-Api-Secret-Token: <YOUR_SECRET>" \
-d '{"update_id":999,"message":{"text":"fake message"}}' \
<YOUR_NGROK_URL>
This time, the request will succeed with a 200 OK status, and your bot will process it. (Note: Since this is a fake update object, your bot might not reply in Telegram, but the server will accept the request, which is what we want to test).
Conclusion
In this lesson, you have implemented a critical security measure for your webhook-based bot. By using a secret token, you ensure that your server only processes legitimate updates sent from Telegram, protecting it from unauthorized access and potential abuse.
Key Takeaways:
- An unsecured webhook is a significant security risk.
- Telegram's standard security mechanism involves a secret token passed via the
X-Telegram-Bot-Api-Secret-TokenHTTP header. - The implementation in
grammYis straightforward:- Store a secret in your
.envfile. - Pass the secret to
webhookCallbackvia thesecretTokenoption in your server code. - Pass the secret to
bot.api.setWebhookvia thesecret_tokenoption when registering the webhook.
- Store a secret in your
grammYautomatically handles the validation, rejecting requests that lack a valid secret token.
Your bot is now not only functional but also secure. With this final piece in place, you are ready for the last major step in our course: deploying your bot to a live production environment. In the next lesson, we will leave ngrok behind and learn how to deploy your bot to a cloud platform, making it permanently available on the internet.
Can't find a good explanation? Sign up and we'll make it for you
Sign up