User-Specific Bot Access Control
Welcome back. In our last session, you built your first piece of custom middleware—a logger that passively observes every update, giving you valuable insight into your bot's operations. This was a crucial first step in mastering grammY's middleware architecture.
Today, we will build on that foundation by creating an active middleware. Instead of just observing, this middleware will act as a gatekeeper, controlling who can interact with your bot. You will implement an authentication layer to restrict bot access to a predefined list of users. This is a common requirement for private bots, administrative tools, or bots with tiered access levels.
This concept may feel familiar. In front-end development, you often implement route guards or higher-order components that check a user's authentication or authorization status before rendering a page or component. The principle here is identical: we will write a function that intercepts a request (an update), checks a condition (is the user authorized?), and then either allows it to proceed or halts execution.
The Logic of an Authentication Middleware
The purpose of our authentication middleware is to sit early in the processing pipeline and make a decision: is the sender of this message on our list of approved users?
The logic follows these steps:
- Identify the user: For any given update, the user who sent it can be identified by a unique ID. grammY makes this available through the
Contextobject atctx.from.id. - Define the "allow list": We need a list of authorized user IDs to check against. For this lesson, we'll start with a simple, hardcoded array of numbers. In a production bot, you might load this from a database or, more likely, from environment variables using the
Bun.envobject we explored in Module 1. - Implement the decision:
- If the user's ID is in our allow list, we want processing to continue as normal. We achieve this by calling
await next(). - If the user's ID is not on the list, we stop the middleware chain. We'll send a polite "access denied" message and then simply
return, ensuring no further handlers are executed for this update.
- If the user's ID is in our allow list, we want processing to continue as normal. We achieve this by calling
Let's sketch out what this looks like in code.
import { Bot, Context, NextFunction } from "grammy";
// 1. Define your allow list of user IDs.
// Replace these with actual Telegram user IDs.
const ALLOWED_USER_IDS = [123456789, 987654321];
// 2. Create the middleware function.
async function authMiddleware(ctx: Context, next: NextFunction): Promise<void> {
// Check if the user is in the allow list.
// We also check if `ctx.from` is defined, as some updates (like from channels) might not have a user.
if (ctx.from && ALLOWED_USER_IDS.includes(ctx.from.id)) {
// User is authorized, so we call the next middleware.
await next();
} else {
// User is not authorized.
console.log(`Unauthorized access attempt by user ${ctx.from?.id}.`);
await ctx.reply("Sorry, you are not authorized to use this bot.");
// We stop processing here and do not call `next()`.
}
}
// Assume 'bot' is already initialized
const bot = new Bot("");
// 3. Register the middleware.
// It should be placed before any handlers you want to protect.
bot.use(authMiddleware);
// This command handler is now protected by the authMiddleware.
bot.command("start", (ctx) => {
ctx.reply("Welcome, authorized user!");
});
// ...
This structure is a direct application of the middleware principles you've learned. The authMiddleware intercepts every single update. By placing it before your command and message handlers, you create a security checkpoint that all traffic must pass through.
Enriching Context vs. Blocking Access
The pattern of checking the user ID in middleware is very common, but it isn't just for blocking access. You can also use it to enrich the context object with user-specific information for downstream handlers to use.
The official grammY documentation provides an excellent example of this pattern. It shows how to create a middleware that checks if a user is the "bot developer" and then attaches a boolean flag isDeveloper to the context object. While their goal is to add information rather than block access, the core mechanism of inspecting ctx.from.id is identical. This technique is useful for enabling special commands or debug modes for specific users without locking everyone else out entirely.
This guide from the grammY docs demonstrates how to add custom properties to the context object using middleware. This is a powerful pattern that complements what we are doing.
Read the section Via Middleware. Pay close attention to how they: Define a list of special users (BOT_DEVELOPER). Create a middleware that checks ctx.from?.id. Add a new config property to ctx. Extend the Context type with MyContext to provide TypeScript with information about the new property, ensuring type safety.
While our authMiddleware blocks unauthorized users, you could combine the patterns. For example, your auth middleware could add a ctx.userRole = "admin" property for authorized users before calling next(). Downstream handlers could then use this property to further tailor their behavior.
A More Declarative Approach: bot.filter()
For the specific task of routing updates based on a simple condition, grammY provides a more concise and declarative helper: bot.filter(). Instead of writing a full middleware function with an if/else block, you can use filter() to create a new, filtered instance of your bot.
The bot.filter() method takes a predicate function—a function that receives the context ctx and returns a boolean. If the function returns true, the update is passed along to any handlers attached to the filtered bot instance. If it returns false, the update is silently dropped by that instance.
This allows for a very clean separation of concerns.
Here's how you could refactor our authentication logic using bot.filter():
import { Bot } from "grammy";
const bot = new Bot("");
const ALLOWED_USER_IDS = [123456789, 987654321];
// This bot instance will only process updates from authorized users.
const adminBot = bot.filter((ctx) => {
return ctx.from !== undefined && ALLOWED_USER_IDS.includes(ctx.from.id);
});
// Handlers attached to `adminBot` are automatically protected.
adminBot.command("start", (ctx) => ctx.reply("Welcome, authorized user!"));
adminBot.command("admin", (ctx) => ctx.reply("Here are the admin tools..."));
// What about unauthorized users? Updates from them are ignored by `adminBot`.
// We can add a "catch-all" handler on the main `bot` object to handle them.
// This handler will only be reached if the `adminBot` filter returns false.
bot.on("message", (ctx) => {
ctx.reply("Sorry, you are not authorized to use this bot.");
});
bot.start();
In this example, grammY's middleware order is key.
- An update arrives.
grammYfirst tries to match it against handlers onadminBot.- The
filter()predicate runs. If it's true,adminBot's command handlers run, and processing for this update typically ends. - If the
filter()predicate is false,grammYcontinues down the middleware stack on the originalbotobject. It then hits ourbot.on("message", ...)catch-all handler, which sends the "access denied" message.
The filter() approach is often cleaner for simple access control, while a full middleware function gives you more power to log attempts, modify the context, or perform other complex side effects.
Conclusion
In this lesson, you've leveled up from a passive observer to an active controller of your bot's middleware flow. You now have the tools to secure your bot and ensure that only authorized users can access its functionality.
Key Takeaways:
- You can implement authentication by creating a middleware that checks the sender's ID (
ctx.from.id) against an allow list. - Stopping the middleware chain is as simple as
returning from the function without callingawait next(). - Besides blocking access, middleware can also be used to enrich the
Contextobject with custom data, like user roles, for downstream handlers. bot.filter()offers a more declarative and concise alternative for creating branches in your logic that only handle certain updates, which is perfect for simple access control.
You've now seen how to create individual middleware for logging and authentication. In a real-world bot, you might have many such handlers for different features. Our next lesson will address how to keep your code organized as it grows. We will explore grammY's Composer class to organize related handlers into reusable, modular units.
Can't find a good explanation? Sign up and we'll make it for you
Sign up