core
core copied to clipboard
proposition for simple access control and authentication
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'
]);
}
}
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
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.
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