-
Notifications
You must be signed in to change notification settings - Fork 40
Add a Telegram factor option #231
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
Changes from all commits
5c28fc5
bbea65e
4e110ba
9ec3799
982cfdc
99d4ebc
3494792
7601490
2519fe9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| <?php | ||
| $path = "https://api.telegram.org/bot1204730014:AAEMpzxxRPRnQTLZ9eCMNgCAmBd0d9pX8iQ"; | ||
|
|
||
| $update = json_decode(file_get_contents("php://input"), TRUE); | ||
|
|
||
| $chatId = 1292580991; | ||
| $message = $update["message"]["text"]; | ||
| $text = "hello"; | ||
| file_get_contents($path."/sendmessage?chat_id=".$chatId."&text=".$text); | ||
| ?> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,315 @@ | ||
| <?php | ||
| // This file is part of Moodle - http://moodle.org/ | ||
| // | ||
| // Moodle is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // Moodle is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU General Public License | ||
| // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | ||
|
|
||
| /** | ||
| * Email factor class. | ||
| * | ||
| * @package factor_telegram | ||
| * @subpackage tool_mfa | ||
| * @author Jan Dageförde, Laura Troost | ||
| * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
| */ | ||
|
|
||
| namespace factor_telegram; | ||
|
|
||
| defined('MOODLE_INTERNAL') || die(); | ||
|
|
||
| use tool_mfa\local\factor\object_factor_base; | ||
|
|
||
| class factor extends object_factor_base { | ||
| /** | ||
| * User input for the generated code. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function login_form_definition($mform) { | ||
| $mform->addElement('text', 'verificationcode', get_string('verificationcode', 'factor_telegram')); | ||
| $mform->setType("verificationcode", PARAM_ALPHANUM); | ||
| return $mform; | ||
| } | ||
|
|
||
| /** | ||
| * Generate a token that is sent to the user via Telegram. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function login_form_definition_after_data($mform) { | ||
| global $DB, $USER; | ||
|
|
||
| // Get the user's Telegram ID from the tool_mfa configuration. | ||
| $sql = 'SELECT * | ||
| FROM {tool_mfa} | ||
| WHERE userid = ? | ||
| AND factor = \'telegram\' | ||
| AND label LIKE \'telegram:%\''; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use $DB->sql_like() for cross db likes. But is this extra where clause even needed at all? Under what conditions would the label not match this? |
||
| $record = $DB->get_record_sql($sql, array($USER->id)); | ||
| if (empty($record)) { | ||
| throw new \coding_exception('Factor has not been set up for this user!'); | ||
| } | ||
|
|
||
| $telegramuserid = substr($record->label, strlen('telegram:')); | ||
|
|
||
| // Send a random code to the user on Telegram. | ||
| $this->generate_and_telegram_code($telegramuserid); | ||
| return $mform; | ||
| } | ||
|
|
||
| /** | ||
| * Validate the entered code. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function login_form_validation($data) { | ||
| global $USER; | ||
| $return = array(); | ||
|
|
||
| if (!$this->check_verification_code($data['verificationcode'])) { | ||
| $return['verificationcode'] = get_string('error:wrongverification', 'factor_telegram'); | ||
| } | ||
|
|
||
| return $return; | ||
| } | ||
|
|
||
| /** | ||
| * Telegram Factor implementation. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function get_all_user_factors($user) { | ||
| global $DB; | ||
|
|
||
| $records = $DB->get_records('tool_mfa', array( | ||
| 'userid' => $user->id, | ||
| 'factor' => $this->name, // TODO look for prefix | ||
| )); | ||
| return $records; | ||
| } | ||
|
|
||
| public function has_setup() { | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * E-Mail Factor implementation. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function get_state() { | ||
| global $USER; | ||
| $userfactors = $this->get_active_user_factors($USER); | ||
|
|
||
| // If no codes are setup then we must be neutral not unknown. | ||
| if (count($userfactors) == 0) { | ||
| return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; | ||
| } | ||
|
|
||
| return parent::get_state(); | ||
| } | ||
|
|
||
| /** | ||
| * Checks whether user telegram is correctly configured. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private static function is_ready() { | ||
| global $DB, $USER; | ||
|
|
||
| // If this factor is revoked, set to not ready. | ||
| // Looking for prefix is not necessary: A single record with "revoked" is sufficient. | ||
| if ($DB->record_exists('tool_mfa', array('userid' => $USER->id, 'factor' => 'telegram', 'revoked' => 1))) { | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Generates and emails the code for login to the user, stores codes in DB. | ||
| * | ||
| * @return void | ||
| */ | ||
| private function generate_and_telegram_code($telegramuserid) { | ||
| global $DB, $USER, $CFG; | ||
|
|
||
| // Get instance that isnt the parent type that defines the username. | ||
| // This check must exclude the main singleton record, with the label that contains the userid. | ||
| // It must only grab the record with the user agent as the label. | ||
| $sql = 'SELECT * | ||
| FROM {tool_mfa} | ||
| WHERE userid = ? | ||
| AND factor = \'telegram\' | ||
| AND label NOT LIKE \'telegram:%\''; | ||
|
|
||
| $record = $DB->get_record_sql($sql, array($USER->id)); | ||
| $duration = get_config('factor_telegram', 'duration'); | ||
| $newcode = random_int(100000, 999999); | ||
|
|
||
| if (empty($record)) { | ||
| // No code active, generate new code. | ||
| $instanceid = $DB->insert_record('tool_mfa', array( | ||
| 'userid' => $USER->id, | ||
| 'factor' => 'telegram', | ||
| 'secret' => $newcode, | ||
| 'label' => $_SERVER['HTTP_USER_AGENT'], | ||
| 'timecreated' => time(), | ||
| 'createdfromip' => $USER->lastip, | ||
| 'timemodified' => time(), | ||
| 'lastverified' => time(), | ||
| 'revoked' => 0, | ||
| ), true); | ||
| $token = get_config('factor_telegram', 'telegrambottoken'); | ||
| $telegram = new telegram($token); | ||
| $a = new \stdClass(); | ||
| $a->sitename = 'Moodle'; // TODO | ||
| $a->code = $newcode; | ||
| $message = get_string('telegram:message', 'factor_telegram', $a); | ||
| $telegram->send_message($telegramuserid, $message); | ||
|
|
||
| } else if ($record->timecreated + $duration < time()) { | ||
| // Old code found. Keep id, update fields. | ||
| $DB->update_record('tool_mfa', array( | ||
| 'id' => $record->id, | ||
| 'secret' => $newcode, | ||
| 'label' => $_SERVER['HTTP_USER_AGENT'], | ||
| 'timecreated' => time(), | ||
| 'createdfromip' => $USER->lastip, | ||
| 'timemodified' => time(), | ||
| 'lastverified' => time(), | ||
| 'revoked' => 0, | ||
| )); | ||
| $instanceid = $record->id; | ||
| $token = get_config('factor_telegram', 'telegrambottoken'); | ||
| $telegram = new telegram($token); | ||
| $a = new \stdClass(); | ||
| $a->sitename = 'Moodle'; // TODO | ||
| $a->code = $newcode; | ||
| $message = get_string('telegram:message', 'factor_telegram', $a); | ||
| $telegram->send_message($telegramuserid, $message); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Verifies entered code against stored DB record. | ||
| * | ||
| * @return bool | ||
| */ | ||
| private function check_verification_code($enteredcode) { | ||
| global $DB, $USER; | ||
| $duration = get_config('factor_telegram', 'duration'); | ||
|
|
||
| // Get instance that isnt parent email type (label check). | ||
| // This check must exclude the main singleton record, with the label as the email. | ||
| // It must only grab the record with the user agent as the label. | ||
| $sql = 'SELECT * | ||
| FROM {tool_mfa} | ||
| WHERE userid = ? | ||
| AND factor = ? | ||
| AND label NOT LIKE \'telegram:%\''; | ||
| $record = $DB->get_record_sql($sql, array($USER->id, 'telegram')); | ||
|
|
||
| if ($enteredcode == $record->secret) { | ||
| if ($record->timecreated + $duration > time()) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Cleans up email records once MFA passed. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function post_pass_state() { | ||
| global $DB, $USER; | ||
| // Delete all telegram records except base record. | ||
| $selectsql = 'userid = ? | ||
| AND factor = ? | ||
| AND label NOT LIKE \'telegram:%\''; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there is some confusion around the intended data model and the handling of the 'secret'. You should never delete this record nor create it while logging in, just set the secret field and then unset just that one field. There should only ever be one factor record per device / account, and not new records made for each login. I think this explains quite a few of the kinks in my testing and once fixed it should all be pretty smooth
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the main thing which will be affected by the planned refactor, so the post state will only remove the transient secrets and not touch the main mfa table. |
||
| $DB->delete_records_select('tool_mfa', $selectsql, array($USER->id, 'telegram')); | ||
|
|
||
| // Update factor timeverified. | ||
| parent::post_pass_state(); | ||
| } | ||
|
|
||
| /** | ||
| * TOTP Factor implementation. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function setup_factor_form_definition($mform) { | ||
| $mform->addElement('text', 'telegramuserid', get_string('telegram:telegramuserid', 'factor_telegram')); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found this a little confusing:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah as it turned out using my username didn't work at all. I had to go digging but eventually found my user id. I think this should accept a username and then make an API call the grab the userid and then store that so it works if people change their name. Also the dud factor I setup using my username could not be revoked. Nor could the second one I made using my user id: Lastly the 'label' for the dud was somehow my user agent string which is weird, it should show my username / name. |
||
| $mform->setType('telegramuserid', PARAM_ALPHANUM); | ||
|
|
||
| return $mform; | ||
| } | ||
|
|
||
| /** | ||
| * TOTP Factor implementation. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function setup_user_factor($data) { | ||
| global $DB, $USER; | ||
|
|
||
| $sql = 'SELECT * | ||
| FROM {tool_mfa} | ||
| WHERE userid = ? | ||
| AND factor = \'telegram\' | ||
| AND label LIKE \'telegram:%\''; | ||
| $record = $DB->get_record_sql($sql, array($USER->id)); | ||
|
|
||
| if (!empty($data->telegramuserid)) { | ||
| $row = new \stdClass(); | ||
| $row->userid = $USER->id; | ||
| $row->factor = $this->name; | ||
| $row->label = 'telegram:'.$data->telegramuserid; | ||
| $row->timecreated = time(); | ||
| $row->createdfromip = $USER->lastip; | ||
| $row->timemodified = time(); | ||
| $row->lastverified = time(); | ||
| $row->revoked = 0; | ||
|
|
||
| if (empty($record)) { | ||
| $id = $DB->insert_record('tool_mfa', $row); | ||
| } else { | ||
| $id = $row->id = $record->id; | ||
| $DB->update_record('tool_mfa', $row); | ||
| } | ||
|
|
||
| $record = $DB->get_record('tool_mfa', array('id' => $id)); | ||
| $this->create_event_after_factor_setup($USER); | ||
|
|
||
| return $record; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Email factor implementation. | ||
| * | ||
| * {@inheritDoc} | ||
| */ | ||
| public function possible_states($user) { | ||
| // Email can return all states. | ||
| return array( | ||
| \tool_mfa\plugininfo\factor::STATE_FAIL, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't see any code handling an explicit FAIL so I think this is best removed for the MVP |
||
| \tool_mfa\plugininfo\factor::STATE_PASS, | ||
| \tool_mfa\plugininfo\factor::STATE_NEUTRAL, | ||
| \tool_mfa\plugininfo\factor::STATE_UNKNOWN, | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <?php | ||
| // This file is part of Moodle - http://moodle.org/ | ||
| // | ||
| // Moodle is free software: you can redistribute it and/or modify | ||
| // it under the terms of the GNU General Public License as published by | ||
| // the Free Software Foundation, either version 3 of the License, or | ||
| // (at your option) any later version. | ||
| // | ||
| // Moodle is distributed in the hope that it will be useful, | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| // GNU General Public License for more details. | ||
| // | ||
| // You should have received a copy of the GNU General Public License | ||
| // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | ||
| /** | ||
| * Revoke email form. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. typo |
||
| * | ||
| * @package factor_telegram | ||
| * @subpackage tool_mfa | ||
| * @author Jan Dageförde, Laura Troost | ||
| * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | ||
| */ | ||
|
|
||
| namespace factor_telegram\form; | ||
|
|
||
| defined('MOODLE_INTERNAL') || die(); | ||
|
|
||
| require_once($CFG->libdir . "/formslib.php"); | ||
|
|
||
| class telegram extends \moodleform { | ||
|
|
||
| public function definition() { | ||
| $mform = $this->_form; | ||
| $mform->addElement('html', get_string('telegram:accident', 'factor_telegram')); | ||
| $this->add_action_buttons(true, get_string('continue')); | ||
| } | ||
|
|
||
| public function validation($data, $files) { | ||
| $errors = parent::validation($data, $files); | ||
| return $errors; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| <?php | ||
|
|
||
|
|
||
| namespace factor_telegram; | ||
|
|
||
|
|
||
| class telegram { | ||
|
|
||
| private $token; | ||
|
|
||
| public function __construct($token) { | ||
| $this->token = $token; | ||
| } | ||
|
|
||
| public function send_message($userid, $text) { | ||
| $path = "https://api.telegram.org/bot".$this->token; | ||
| file_get_contents($path."/sendmessage?chat_id=".$userid."&text=".$text); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please wrap this in some error handling and use the curl library. I would expect the telegram to be pretty snappy so it could have a very short timeout of say 5-10 seconds and not the default which can hang for some time |
||
| } | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to keep this file but turn it into an admin only 'test connection' in the admin settings page where you set the api