Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions factor/telegram/Telegram.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
$path = "https://api.telegram.org/bot1204730014:AAEMpzxxRPRnQTLZ9eCMNgCAmBd0d9pX8iQ";

Copy link
Copy Markdown
Contributor

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


$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);
?>
315 changes: 315 additions & 0 deletions factor/telegram/classes/factor.php
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:%\'';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:%\'';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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'));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this a little confusing:

  1. the form asks for an ID which I usually assume is a number. I could not find that number anywhere so I used myusername which appeared to work

  2. there was no validation that I own this username. It should send a code to this account and you must validate / verify that code before the telegram record should be created. See the TOTP form for an example of this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:

image

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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,
);
}
}
43 changes: 43 additions & 0 deletions factor/telegram/classes/form/telegram.php
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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;
}
}
19 changes: 19 additions & 0 deletions factor/telegram/classes/telegram.php
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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

}
}
Loading