Skip to content

proposition for simple access control and authentication #613

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sharkydog opened this issue Sep 1, 2017 · 3 comments
Open

proposition for simple access control and authentication #613

sharkydog opened this issue Sep 1, 2017 · 3 comments

Comments

@sharkydog
Copy link

sharkydog commented Sep 1, 2017

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'
		]);
	}
}
@noplanman
Copy link
Member

noplanman commented Sep 1, 2017

Wow, this looks pretty cool, I'll have a closer look 👍

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

@sharkydog
Copy link
Author

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
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants