core icon indicating copy to clipboard operation
core copied to clipboard

proposition for simple access control and authentication

Open sharkydog opened this issue 6 years ago • 3 comments

Required Information

  • Operating system: Debian testing
  • PHP version: 5.6, 7.1
  • PHP Telegram Bot version: 0.48
  • Using MySQL database: yes
  • MySQL version: mariadb 10.1
  • Update Method: Webhook
  • Self-signed certificate: no
  • RAW update (if available):

Expected behaviour

Bot doesn't reply for users not in access list for given commands. Bot asks for authentication code for given commands, provided to the user by other means.

Actual behaviour

Open to the world for anyone to send commands to your bot, not even via telegram (if he/she knows your hook address), even with your own telegram id (if he/she knows that too).

Steps to reproduce

See bellow

Extra details

Currently I do this by hacking around the bot api, using google authenticator to get time based codes. My wish is something like this (better) to be included into the main code.

class ShdTelegram extends Longman\TelegramBot\Telegram {
	const ACL_ALLOW = 1;
	const ACL_DENY = 0;
	// deny_list -> allow_list -> default_user -> default_cmd -> default_all
	protected $acl_deny_list = [];    // array(command=>array(from.id))
	protected $acl_allow_list = [];   // array(command=>array(from.id))
	protected $acl_default_user = []; // array(from.id=>[self::ACL_ALLOW|self::ACL_DENY])
	protected $acl_default_cmd = [];  // array(command=>[self::ACL_ALLOW|self::ACL_DENY])
	protected $acl_default_all = self::ACL_ALLOW; // [self::ACL_ALLOW|self::ACL_DENY]
	
	public function getAclAllow($command, $from_id) {
		if(!empty($this->acl_deny_list[$command]) && in_array($from_id, $this->acl_deny_list[$command])) return false;
		if(!empty($this->acl_allow_list[$command]) && in_array($from_id, $this->acl_allow_list[$command])) return true;
		if(isset($this->acl_default_user[$from_id])) return $this->acl_default_user[$from_id]==self::ACL_ALLOW;
		if(isset($this->acl_default_cmd[$command])) return $this->acl_default_cmd[$command]==self::ACL_ALLOW;
		return $this->acl_default_all==self::ACL_ALLOW;
	}
	
	public function setAclAllowAll($allow) {
		$this->acl_default_all = (bool)$allow ? self::ACL_ALLOW : self::ACL_DENY;
	}
	public function setAclCmd(array $acl) {
		$this->acl_default_cmd = $acl;
	}
	public function setAclUser(array $acl) {
		$this->acl_default_user = $acl;
	}
	public function setAclAllow(array $acl) {
		$this->acl_allow_list = $acl;
	}
	public function setAclDeny(array $acl) {
		$this->acl_deny_list = $acl;
	}
}

class ShdTelegramConversation extends Longman\TelegramBot\Conversation {
	public function update() {
		$this->notes['update'] = microtime(true);
		return parent::update();
	}
	public function refresh() {
		$this->load();
		return $this;
	}
	public function getUpdateMicrotime() {
		return !empty($this->notes['update']) ? $this->notes['update'] : null;
	}
}

trait ShdTelegramCommand {
	abstract public function executeCmd();
	
	final public function execute() {
		$cmd = strtolower($this->getName());
		$msg = $this->getMessage();
		$from = $msg->getFrom()->getId();
		$chat = $msg->getChat()->getId();
		
		if(!$this->telegram->getAclAllow($cmd,$from)) return Longman\TelegramBot\Request::emptyResponse();
		
		$res = $this->executeCmd();
		return $res;
	}
}

trait ShdTelegramCommandAuth {
	abstract public function executeAuthenticated();
	
	public function preExecute() {
		$this->need_mysql = true;
		$this->private_only = true;
		return parent::preExecute();
	}
	
	public function executeCmd() {
		$cmd = strtolower($this->getName());
		$msg = $this->getMessage();
		$from = $msg->getFrom()->getId();
		$chat = $msg->getChat()->getId();
		
		$conv = new ShdTelegramConversation($from,$chat,$cmd);
		if(empty($conv->notes['auth']['passed'])) {
			if(empty($conv->notes['auth']['msg'])) {
				$conv->notes['auth']['msg'] = $this->update->message;
				$conv->update();
				
				return Longman\TelegramBot\Request::sendMessage([
					'chat_id' => $chat,
					'text' => 'Enter code'
				]);
			} else {
				if(!ShdGoogleAuth::verify(trim($msg->getText(true)))) {
					$conv->stop();
					return Longman\TelegramBot\Request::sendMessage([
						'chat_id' => $chat,
						'text' => 'Wrong code'
					]);
				}
				
				$conv->notes['auth']['passed'] = true;
				$conv->update();
				$this->update->message = $conv->notes['auth']['msg'];
			}
		}
		
		$updateTime = $conv->refresh()->getUpdateMicrotime();
		$res = $this->executeAuthenticated();
		if($updateTime == $conv->refresh()->getUpdateMicrotime()) $conv->stop();
		
		return $res;
	}
}

abstract class ShdTelegramUserCommand extends Longman\TelegramBot\Commands\UserCommand {
	use ShdTelegramCommand;
}

abstract class ShdTelegramSystemCommand extends Longman\TelegramBot\Commands\SystemCommand {
	use ShdTelegramCommand;
}

For authentication I use a wrapper for google authenticator: ShdGoogleAuth::verify($code)

Then setting access control

$telegram->setAclCmd([
	'ping' => ShdTelegram::ACL_DENY,
	'wake' => ShdTelegram::ACL_DENY
]);
$telegram->setAclUser([
	123456789 => ShdTelegram::ACL_ALLOW
]);

commands go like this

namespace Longman\TelegramBot\Commands\UserCommands;

class WakeCommand extends \ShdTelegramUserCommand {
	protected $name = 'wake';

	use \ShdTelegramCommandAuth;
	
	public function executeAuthenticated() {
		$msg = $this->getMessage();
		$from = $msg->getFrom()->getId();
		$chat = $msg->getChat()->getId();
		$text = trim($this->getMessage()->getText(true));
		
		// do stuff here
		
		return \Longman\TelegramBot\Request::sendMessage([
			'chat_id' => $chat,
			'text' => 'Woken'
		]);
	}
}

Commands must use \ShdTelegramConversation to open conversations, otherwise the microtime will not be updated and conversation will be closed right after execution.

or like this (without authentication)

namespace Longman\TelegramBot\Commands\UserCommands;

class PingCommand extends \ShdTelegramUserCommand {
	protected $name = 'ping';
	
	public function executeCmd() {
		return \Longman\TelegramBot\Request::sendMessage([
			'chat_id' => $this->getMessage()->getChat()->getId(),
			'text' => 'pong'
		]);
	}
}

sharkydog avatar Sep 01 '17 07:09 sharkydog

Wow, this looks pretty cool, I'll have a closer look :+1:

Open to the world for anyone to send commands to your bot

Just to let you know, there is a wiki page on hardening your bot here: Securing & Hardening your Telegram Bot

noplanman avatar Sep 01 '17 08:09 noplanman

The url token is even suggested in telegram bot api FAQ and it is just a way to make your address hard to guess, kind of solves one problem. This however adds a way to limit access and authenticate, available to all commands if they wish to use it. It could be better integrated into the code and natively available to commands.

sharkydog avatar Sep 01 '17 08:09 sharkydog

About the access list, I made a simpler and more structured variant in my fork, take a look https://github.com/sharkydog/php-telegram-bot/tree/acl

sharkydog avatar Oct 06 '17 14:10 sharkydog