How to implement access control for a Telegram bot

Use a token to restrict who can start a chat with the bot

Author's image
Tamás Sallai
4 mins

Access control for Telegram bots

In the previous article we built a Telegram bot that forwarded messages to chats sent to an SNS topic. We looked into how to integrate CloudWatch Alarms with it, so that when an important event happens in the AWS account, a message is sent.

In this article, we'll look into how to control who can get these messages from the bot.

Telegram bots, and chatbots in general, can be powerful, depending on the code. They can give insight into an account, such as the notification bot, but they might also control resources, such as starting and stopping EC2 instances, or change the configuration of a Lambda function, or either write to a database. Since the bot is just a frontend for code running in the account, the functionality it provides can be nearly anything.

By default, anybody who knows the name of the bot can start a chat with it. If getting notifications is a simple /start command, then there is no access control to it.

In this article, we'll implement a simple access control scheme. In it, the bot requires a start token and refuses to subscribe the sender if the token is missing. To make it easier for legitimate users, we'll also take advantage of Telegram's deep linking feature to make a URL that is easy to share with people.

Generating the start token

First, we need a sufficiently random value that we'll use as the token. In Terraform, the random_id resource is a good candidate to generate it:

resource "random_id" "start_token" {
	byte_length = 16
}

Then the Lambda function needs to know about this token:

resource "aws_lambda_function" "control-lambda" {
	# ...
	environment {
		variables = {
			# ...
			start_token = random_id.start_token.b64_url
		}
	}
}

I use b64_url, as that should be URL-safe and shorter than the hex. With the latter, I experienced problems where the last few characters were cut off from the URL, so using b64_url seems like a safer solution.

Checking the token

Then the Lambda needs to check that the /start message contains the token:

const {start_token} = process.env;
const startPattern = /^\/start (?<token>\S+)$/;
if (text.match(startPattern)) {
	const {token} = text.match(startPattern).groups;
	if (token === start_token) {
		// process start request
	}
}

This is all we need to secure the bot.

Deep linking

Manually typing the /start command and copy-pasting the token is not that user-friendly. Fortunately, Telegram supports deep linking that allows opening a chat with a bot and also adding extra data.

Its structure is https://t.me/<botname>?start=<value>. Opening the link jumps to a chat with the bot and provides a Start button:

Using that button, Telegram sends the token, even if it is not visible:

But the bot gets the value:

The link contains the bot's name. While it could be hardcoded, Telegram provides a getMe method that only needs the bot token and returns its name.

In Terraform, change the setWebhook invocation to also return the username of the bot:

if (event.setWebhook) {
	// set webhook, commands, other initialization

	const me = await sendTelegramCommand("getMe");
	return me.username;
}

Then insert the value to the link:

output "start_link" {
	value = "https://t.me/${jsondecode(data.aws_lambda_invocation.set_webhook.result)}?start=${random_id.start_token.b64_url}"
}

Conclusion

Access control is an important aspect of every system that provides read or read/write access to protected resources. If a chatbot forwards information from an AWS account, or when it allows modifying things in it, you need to think about how you'll protect it from unwanted access.

Using a random token and implementing token-based access control is a simple scheme you can use.

March 29, 2022
In this article