diff --git a/factor/telegram/classes/factor.php b/factor/telegram/classes/factor.php new file mode 100644 index 00000000..4a492fc8 --- /dev/null +++ b/factor/telegram/classes/factor.php @@ -0,0 +1,356 @@ +. + +/** + * Telegram 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 moodle_url; +use tool_mfa\local\factor\object_factor_base; + +class factor extends object_factor_base { + /** + * Login form: Definition. + * + * {@inheritDoc} + */ + public function login_form_definition($mform) { + $mform->addElement(new \tool_mfa\local\form\verification_field()); + $mform->setType('verificationcode', PARAM_ALPHANUM); + return $mform; + } + + /** + * Login form: Send a secret to the user and add a corresponding note to the form. + * + * {@inheritDoc} + */ + public function login_form_definition_after_data($mform) { + $recipient = $this->generate_and_send_code(); + $mform->addElement('html', \html_writer::tag('p', get_string('telegramsent', 'factor_telegram', $recipient) . '
')); + // Disable the form check prompt. + $mform->disable_form_change_checker(); + return $mform; + } + + /** + * Login form: Validate secret. + * + * {@inheritDoc} + */ + public function login_form_validation($data) { + $return = array(); + + if (!$this->check_verification_code($data['verificationcode'])) { + $return['verificationcode'] = get_string('wrongcode', 'factor_telegram'); + } + + return $return; + } + + /** + * Gets the string for setup button on preferences page. + */ + public function get_setup_string() { + return get_string('setupfactor', 'factor_telegram'); + } + + /** + * Requires a bot token to be *actually* enabled. + * + * {@inheritDoc} + */ + public function is_enabled() { + if (empty(get_config('factor_telegram', 'telegrambottoken'))) { + return false; + } else { + return parent::is_enabled(); + } + } + + /** + * Requires user input to verify. + * + * {@inheritDoc} + */ + public function has_input() { + return true; + } + + /** + * Requires user setup. + * + * {@inheritDoc} + */ + public function has_setup() { + return true; + } + + /** + * If there is already a factor setup, don't allow multiple (for now). + * + * {@inheritDoc} + */ + public function show_setup_buttons() { + global $DB, $USER; + $sql = 'SELECT * + FROM {tool_mfa} + WHERE userid = ? + AND factor = ? + AND secret = ? + AND revoked = 0'; + + $record = $DB->get_record_sql($sql, [$USER->id, $this->name, '']); + return !empty($record) ? false : true; + } + + /** + * A factor can be revoked by a user. + * + * {@inheritDoc} + */ + public function has_revoke() { + return true; + } + + /** + * Verifies entered code against stored DB record. + * + * @return bool + */ + private function check_verification_code($enteredcode) { + $state = $this->secretmanager->validate_secret($enteredcode); + if ($state === \tool_mfa\local\secret_manager::VALID) { + return true; + } + return false; + } + + /** + * The Telegram factor can assume one of these states. + * + * {@inheritDoc} + */ + public function possible_states($user) { + return array( + \tool_mfa\plugininfo\factor::STATE_PASS, + \tool_mfa\plugininfo\factor::STATE_NEUTRAL, + \tool_mfa\plugininfo\factor::STATE_UNKNOWN, + ); + } + + /** + * Get all Telegram IDs of a user. + * + * {@inheritDoc} + */ + public function get_all_user_factors($user) { + global $DB; + + $sql = 'SELECT * + FROM {tool_mfa} + WHERE userid = ? + AND factor = ? + AND label IS NOT NULL + AND revoked = 0'; + + return $DB->get_records_sql($sql, [$user->id, $this->name]); + } + + /** + * User factor: Form definition. + * + * {@inheritDoc} + */ + public function setup_factor_form_definition($mform) { + global $SESSION, $OUTPUT; + + $mform->addElement('html', $OUTPUT->heading(get_string('setupfactor', 'factor_telegram'), 2)); + + // The field $SESSION->tool_mfa_telegram_id temporarily stores an ID that the user has entered, but not yet verified. + if (empty($SESSION->tool_mfa_telegram_id)) { + $mform->addElement('hidden', 'verificationcode', 0); + $mform->setType("verificationcode", PARAM_ALPHANUM); + + // Field that specifies the user's Telegram ID. + $mform->addElement('text', 'telegramid', get_string('addtelegramid', 'factor_telegram')); + $mform->setType('telegramid', PARAM_TEXT); + $botname = get_config('factor_telegram', 'telegrambotname'); + if (strpos($botname, '@') === 0) { + $botname = substr($botname, 1); + } + $mform->addElement('html', \html_writer::tag('p', get_string('telegramhelp', 'factor_telegram', $botname))); + } + } + + /** + * User factor: Form definition after data. + * + * {@inheritDoc} + */ + public function setup_factor_form_definition_after_data($mform) { + global $SESSION; + + // Do nothing before a Telegram ID has been entered. + if (empty($SESSION->tool_mfa_telegram_id)) { + return $mform; + } + + // Once there is a supplied Telegram ID, send a verification code to set up the factor. + $mform->addElement(new \tool_mfa\local\form\verification_field()); + $mform->setType('verificationcode', PARAM_ALPHANUM); + + $duration = get_config('factor_telegram', 'duration'); + $code = $this->secretmanager->create_secret($duration, true); + if (!empty($code)) { + $this->send_verification_code($code, $SESSION->tool_mfa_telegram_id); + } + + // Tell users it was sent. + $mform->addElement('html', \html_writer::tag('p', + get_string('telegramsent', 'factor_telegram', $SESSION->tool_mfa_telegram_id) . '
')); + + // Disable the form check prompt. + $mform->disable_form_change_checker(); + } + + /** + * User factor: Form validation. + * + * {@inheritDoc} + */ + public function setup_factor_form_validation($data) { + global $SESSION; + + // No validation on the initial ID (i.e., after step 1). + if (empty($SESSION->tool_mfa_telegram_id)) { + return []; + } + + // Validate the trial secret. + $errors = []; + $result = $this->secretmanager->validate_secret($data['verificationcode']); + if ($result !== $this->secretmanager::VALID) { + $errors['verificationcode'] = get_string('wrongcode', 'factor_telegram'); + } + + return $errors; + } + + /** + * User factor: Form submission. + * Either stores the Telegram ID in a temporary session variable (awaiting manual verification), + * or in the database (after verification succeeded). + * + * {@inheritDoc} + */ + public function setup_user_factor($data) { + global $DB, $SESSION, $USER; + + // Initial submission of ID: Store in session; redirect for step 2 (verification). + if (empty($SESSION->tool_mfa_telegram_id)) { + $SESSION->tool_mfa_telegram_id = $data->telegramid; + + $addurl = new \moodle_url('/admin/tool/mfa/action.php', [ + 'action' => 'setup', + 'factor' => 'telegram', + ]); + redirect($addurl); + } + + // Step 2 (verification succeeded): Store permanently. + + // If the user somehow gets here through form resubmission. + // We dont want two phones active. + if ($DB->record_exists('tool_mfa', ['userid' => $USER->id, 'factor' => $this->name, 'revoked' => 0])) { + return null; + } + + $row = new \stdClass(); + $row->userid = $USER->id; + $row->factor = $this->name; + $row->secret = ''; + $row->label = $SESSION->tool_mfa_telegram_id; + $row->timecreated = time(); + $row->createdfromip = $USER->lastip; + $row->timemodified = time(); + $row->lastverified = time(); + $row->revoked = 0; + + $id = $DB->insert_record('tool_mfa', $row); + $record = $DB->get_record('tool_mfa', array('id' => $id)); + $this->create_event_after_factor_setup($USER); + + // Remove ID from temporary session variable. + unset($SESSION->tool_mfa_telegram_id); + + return $record; + } + + /** + * Generates and sends the code for login to the user, stores codes in DB. + * + * @return int the instance ID being used. + */ + private function generate_and_send_code() { + global $DB, $USER; + + $duration = get_config('factor_telegram', 'duration'); + $secret = $this->secretmanager->create_secret($duration, false); + $instance = $DB->get_record('tool_mfa', ['factor' => $this->name, 'userid' => $USER->id, 'revoked' => 0]); + + // There is a new code that needs to be sent. + if (!empty($secret)) { + // Grab the singleton SMS record. + $this->send_verification_code($secret, $instance->label); + } + return $instance->label; + } + + /** + * This function sends a code to the user via Telegram. + * + * @param int $secret the secret to send. + * @param string $telegramid Recipient user id. + * @return void + */ + private function send_verification_code($secret, $telegramid) { + global $CFG, $SITE; + + // Here we should get the information, then construct the message. + $url = new moodle_url($CFG->wwwroot); + $content = [ + 'fullname' => $SITE->fullname, + 'shortname' => $SITE->shortname, + 'supportname' => $CFG->supportname, + 'url' => $url->get_host(), + 'code' => $secret]; + $message = get_string('telegramstring', 'factor_telegram', $content); + + $bottoken = get_config('factor_telegram', 'telegrambottoken'); + $client = new \factor_telegram\telegram_client($bottoken); + $client->send_message($telegramid, $message); + } +} diff --git a/factor/telegram/classes/privacy/provider.php b/factor/telegram/classes/privacy/provider.php new file mode 100644 index 00000000..0c1d7604 --- /dev/null +++ b/factor/telegram/classes/privacy/provider.php @@ -0,0 +1,47 @@ +. +/** + * Privacy provider. + * + * @package factor_telegram + * @author Jan Dageförde, Laura Troost + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace factor_telegram\privacy; + +defined('MOODLE_INTERNAL') || die; + +use core_privacy\local\metadata\null_provider; +use core_privacy\local\legacy_polyfill; + +/** + * Class provider + * @package factor_telegram\privacy + */ +class provider implements null_provider { + use legacy_polyfill; + + /** + * Get the language string identifier with the component's language + * file to explain why this plugin stores no data. + * + * @return string + */ + public static function _get_reason() { + return 'privacy:metadata'; + } +} diff --git a/factor/telegram/classes/telegram_client.php b/factor/telegram/classes/telegram_client.php new file mode 100644 index 00000000..14954823 --- /dev/null +++ b/factor/telegram/classes/telegram_client.php @@ -0,0 +1,51 @@ +. + +/** + * Telegram API client. + * + * @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(); + +/** + * Telegram API client. + * @package factor_telegram + */ +class telegram_client { + + private $token; + + public function __construct($token) { + $this->token = $token; + } + + public function send_message($userid, $text) { + $params = [ + 'chat_id' => $userid, + 'text' => $text, + ]; + + $httpclient = new \curl(); + $httpclient->get("https://api.telegram.org/bot" . $this->token . "/sendmessage", $params); + } +} \ No newline at end of file diff --git a/factor/telegram/lang/en/factor_telegram.php b/factor/telegram/lang/en/factor_telegram.php new file mode 100644 index 00000000..f4d60786 --- /dev/null +++ b/factor/telegram/lang/en/factor_telegram.php @@ -0,0 +1,45 @@ +. + +/** + * Language strings. + * + * @package factor_telegram + * @author Jan Dageförde, Laura Troost + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Telegram One-Time Code'; +$string['action:revoke'] = 'Revoke Telegram ID'; +$string['addtelegramid'] = 'Enter Telegram username or ID'; +$string['info'] = '

Setup your Telegram ID so that one-time security codes can be sent to you.

'; +$string['loginsubmit'] = 'Verify code'; +$string['loginskip'] = "I didn't receive a code"; +$string['setupfactor'] = 'Setup Telegram ID'; +$string['settings:duration'] = 'Validity duration'; +$string['settings:duration_help'] = 'The period of time that the code is valid.'; +$string['settings:telegrambottoken'] = 'Token of your Telegram bot'; +$string['settings:telegrambottoken_help'] = 'Register a new Telegram bot as described in the Telegram documentation and enter its token here.'; +$string['settings:telegrambotname'] = 'Username of your Telegram bot'; +$string['settings:telegrambotname_help'] = 'Enter the username of your bot (e. g., @Moodlebot), as entered during the registration. This name will be displayed to users so that they can set up their own factor.'; +$string['telegramhelp'] = 'First, send a message containing the text "/start" to @{$a}. Afterwards(!), enter your Telegram user name or, alternatively, your Telegram user ID.'; +$string['telegramsent'] = 'A Telegram message containing your verification code was sent to you ({$a}).'; +$string['telegramstring'] = '{$a->code} is your {$a->fullname} one-time security code. + +@{$a->url} #{$a->code}'; +$string['summarycondition'] = 'Using a one-time security code sent via Telegram'; +$string['privacy:metadata'] = 'The Telegram factor plugin does not store any personal data'; +$string['wrongcode'] = 'Invalid security code.'; diff --git a/factor/telegram/settings.php b/factor/telegram/settings.php new file mode 100644 index 00000000..a34a4330 --- /dev/null +++ b/factor/telegram/settings.php @@ -0,0 +1,46 @@ +. + +/** + * Settings + * + * @package factor_telegram + * @author Jan Dageförde, Laura Troost + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); +global $CFG, $OUTPUT; + +$settings->add(new admin_setting_configcheckbox('factor_telegram/enabled', + new lang_string('settings:enablefactor', 'tool_mfa'), + new lang_string('settings:enablefactor_help', 'tool_mfa'), 0)); + +$settings->add(new admin_setting_configtext('factor_telegram/weight', + new lang_string('settings:weight', 'tool_mfa'), + new lang_string('settings:weight_help', 'tool_mfa'), 100, PARAM_INT)); + +$settings->add(new admin_setting_configduration('factor_telegram/duration', + get_string('settings:duration', 'tool_mfa'), + get_string('settings:duration_help', 'tool_mfa'), 30 * MINSECS, MINSECS)); + +$settings->add(new admin_setting_configpasswordunmask('factor_telegram/telegrambottoken', + new lang_string('settings:telegrambottoken', 'factor_telegram'), + new lang_string('settings:telegrambottoken_help', 'factor_telegram'), '', PARAM_TEXT)); + +$settings->add(new admin_setting_configtext('factor_telegram/telegrambotname', + new lang_string('settings:telegrambotname', 'factor_telegram'), + new lang_string('settings:telegrambotname_help', 'factor_telegram'), '', PARAM_TEXT)); \ No newline at end of file diff --git a/factor/telegram/version.php b/factor/telegram/version.php new file mode 100644 index 00000000..5cd377ad --- /dev/null +++ b/factor/telegram/version.php @@ -0,0 +1,33 @@ +. + +/** + * Plugin version and other meta-data are defined here. + * + * @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 + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2020110100; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2017051500.00; // Support back to 3.3 - Totara 12. Patches required. +$plugin->component = 'factor_telegram'; +$plugin->release = 'v0.1'; +$plugin->maturity = MATURITY_STABLE; +$plugin->dependencies = array('tool_mfa' => 2019102400); diff --git a/renderer.php b/renderer.php index a26998e3..54f13ad1 100644 --- a/renderer.php +++ b/renderer.php @@ -224,7 +224,7 @@ public function not_enough_factors() { $return = $this->output->notification($notification, 'notifyerror'); // Logout button. - $url = new \moodle_url('\admin\tool\mfa\auth.php', ['logout' => 1]); + $url = new \moodle_url('/admin/tool/mfa/auth.php', ['logout' => 1]); $btn = new \single_button($url, get_string('logout'), 'post', true); $return .= $this->render($btn);