diff --git a/auth/oauth2/classes/api.php b/auth/oauth2/classes/api.php new file mode 100644 index 00000000000..ce02ce39c89 --- /dev/null +++ b/auth/oauth2/classes/api.php @@ -0,0 +1,147 @@ +. + +/** + * Class for loading/storing oauth2 linked logins from the DB. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2; + +use context_user; +use stdClass; +use moodle_exception; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Static list of api methods for auth oauth2 configuration. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * List linked logins + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param int $userid (defaults to $USER->id) + * @return boolean + */ + public static function get_linked_logins($userid = false) { + global $USER; + + if ($userid === false) { + $userid = $USER->id; + } + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + return linked_login::get_records(['userid' => $userid]); + } + + /** + * See if there is a match for this username and issuer in the linked_login table. + * + * @param string $username as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @return stdClass User record if found. + */ + public static function match_username_to_user($username, $issuer) { + $params = [ + 'issuerid' => $issuer->get('id'), + 'username' => $username + ]; + $match = linked_login::get_record($params); + + if ($match) { + $user = get_complete_user_data('id', $match->get('userid')); + + return $user; + } + return false; + } + + /** + * Link a login to this account. + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param array $userinfo as returned from an oauth client. + * @param \core\oauth2\issuer $issuer + * @param int $userid (defaults to $USER->id) + * @return boolean + */ + public static function link_login($userinfo, $issuer, $userid = false) { + global $USER; + + if ($userid === false) { + $userid = $USER->id; + } + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->username = $userinfo['username']; + $record->email = $userinfo['email']; + $record->userid = $userid; + $existing = linked_login::get_record((array)$record); + if ($existing) { + return $existing; + } + $linkedlogin = new linked_login(0, $record); + return $linkedlogin->create(); + } + + /** + * Delete linked login + * + * Requires auth/oauth2:managelinkedlogins capability at the user context. + * + * @param int $linkedloginid + * @return boolean + */ + public static function delete_linked_login($linkedloginid) { + $login = new linked_login($linkedloginid); + $userid = $login->get('userid'); + + if (\core\session\manager::is_loggedinas()) { + throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); + } + + $context = context_user::instance($userid); + require_capability('auth/oauth2:managelinkedlogins', $context); + + $login->delete(); + } +} diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php index 22906567a9d..677ec630b2e 100644 --- a/auth/oauth2/classes/auth.php +++ b/auth/oauth2/classes/auth.php @@ -156,14 +156,8 @@ class auth extends \auth_plugin_base { * @param array $userfields */ public function config_form($config, $err, $userfields) { - echo get_string('plugindescription', 'auth_oauth2'); + include(__DIR__ . "/../config.html"); - // Force all fields updated on login and locked. - - foreach ($userfields as $field) { - set_config('field_updatelocal_' . $field, 'onlogin', 'auth_oauth2'); - set_config('field_lock_' . $field, 'unlockedifempty', 'auth_oauth2'); - } return; } @@ -312,6 +306,14 @@ class auth extends \auth_plugin_base { return true; } + public function process_config($config) { + // Set to defaults if undefined + if (!isset($config->allowlinkedlogins)) { + $config->allowlinkedlogins = false; + } + set_config('allowlinkedlogins', trim($config->allowlinkedlogins), 'auth_oauth2'); + } + /** * Complete the login process after oauth handshake is complete. * @param \core\oauth2\client $client @@ -336,20 +338,33 @@ class auth extends \auth_plugin_base { $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); - if (!empty($userinfo['picture'])) { - $this->set_static_user_picture($userinfo['picture']); - unset($userinfo['picture']); - } + $userwasmapped = false; + if (get_config('auth_oauth2', 'allowlinkedlogins')) { + $mappeduser = api::match_username_to_user($userinfo['username'], $client->get_issuer()); - if (!empty($userinfo['lang'])) { - $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); - if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { - unset($userinfo['lang']); + if ($mappeduser) { + $userinfo = (array) $mappeduser; + $userwasmapped = true; } } + + if (!$userwasmapped) { + if (!empty($userinfo['picture'])) { + $this->set_static_user_picture($userinfo['picture']); + unset($userinfo['picture']); + } + + if (!empty($userinfo['lang'])) { + $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); + if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { + unset($userinfo['lang']); + } + } + } + $this->set_static_user_info($userinfo); - $user = authenticate_user_login($userinfo['username'], ''); + $user = get_complete_user_data('username', $userinfo['username']); if ($user) { complete_user_login($user); diff --git a/auth/oauth2/classes/linked_login.php b/auth/oauth2/classes/linked_login.php new file mode 100644 index 00000000000..93dfd5702cf --- /dev/null +++ b/auth/oauth2/classes/linked_login.php @@ -0,0 +1,63 @@ +. + +/** + * Class for loading/storing issuers from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing issuer from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class linked_login extends persistent { + + /** @const TABLE */ + const TABLE = 'auth_oauth2_linked_login'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'userid' => array( + 'type' => PARAM_INT + ), + 'username' => array( + 'type' => PARAM_RAW + ), + 'email' => array( + 'type' => PARAM_RAW + ) + ); + } + +} diff --git a/auth/oauth2/classes/output/renderer.php b/auth/oauth2/classes/output/renderer.php new file mode 100644 index 00000000000..dbe2048810e --- /dev/null +++ b/auth/oauth2/classes/output/renderer.php @@ -0,0 +1,96 @@ +. + +/** + * Output rendering for the plugin. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace auth_oauth2\output; + +use plugin_renderer_base; +use html_table; +use html_table_cell; +use html_table_row; +use html_writer; +use auth\oauth2\linked_login; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the plugin renderer + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + /** + * This function will render one beautiful table with all the linked_logins. + * + * @param \auth\oauth2\linked_login[] $linkedlogins - list of all linked logins. + * @return string HTML to output. + */ + public function linked_logins_table($linkedlogins) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('issuer', 'auth_oauth2'), + get_string('info', 'auth_oauth2'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($linkedlogins as $linkedlogin) { + // Issuer. + $issuerid = $linkedlogin->get('issuerid'); + $issuer = \core\oauth2\api::get_issuer($issuerid); + $issuercell = new html_table_cell(s($issuer->get('name'))); + + // Issuer. + $username = $linkedlogin->get('username'); + $email = $linkedlogin->get('email'); + $usernamecell = new html_table_cell(s($email) . ', (' . s($username) . ')'); + + $links = ''; + + // Delete. + $deleteparams = ['linkedloginid' => $linkedlogin->get('id'), 'action' => 'delete', 'sesskey' => sesskey()]; + $deleteurl = new moodle_url('/auth/oauth2/linkedlogins.php', $deleteparams); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $issuercell, + $usernamecell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } +} diff --git a/auth/oauth2/config.html b/auth/oauth2/config.html new file mode 100644 index 00000000000..b78655a748d --- /dev/null +++ b/auth/oauth2/config.html @@ -0,0 +1,32 @@ + +
+ +
+allowlinkedlogins)) { + $config->allowlinkedlogins = true; +} +?> + + + + + + + +authtype, $userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); + +?> +
+ + + allowlinkedlogins) { echo 'checked="checked"'; } ?> + > + error_text($err['allowlinkedlogins']); } ?> + + +
diff --git a/auth/oauth2/db/access.php b/auth/oauth2/db/access.php new file mode 100644 index 00000000000..53864726cd9 --- /dev/null +++ b/auth/oauth2/db/access.php @@ -0,0 +1,34 @@ +. + +/** + * Capability definitions for this plugin. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +$capabilities = [ + + 'auth/oauth2:managelinkedlogins' => array( + + 'captype' => 'write', + 'contextlevel' => CONTEXT_USER, + 'archetypes' => array( + 'user' => CAP_ALLOW + ) + ), +]; diff --git a/auth/oauth2/db/install.xml b/auth/oauth2/db/install.xml new file mode 100644 index 00000000000..3e4933822df --- /dev/null +++ b/auth/oauth2/db/install.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/auth/oauth2/db/upgrade.php b/auth/oauth2/db/upgrade.php new file mode 100644 index 00000000000..45ebeb90685 --- /dev/null +++ b/auth/oauth2/db/upgrade.php @@ -0,0 +1,74 @@ +. + +/** + * OAuth2 authentication plugin upgrade code + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * @param int $oldversion the version we are upgrading from + * @return bool result + */ +function xmldb_auth_oauth2_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + // Automatically generated Moodle v3.2.0 release upgrade line. + // Put any upgrade step following this. + + if ($oldversion < 2017030700) { + + // Define table auth_oauth2_linked_login to be created. + $table = new xmldb_table('auth_oauth2_linked_login'); + + // Adding fields to table auth_oauth2_linked_login. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('usermodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('username', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table auth_oauth2_linked_login. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('usermodified_key', XMLDB_KEY_FOREIGN, array('usermodified'), 'user', array('id')); + $table->add_key('userid_key', XMLDB_KEY_FOREIGN, array('userid'), 'user', array('id')); + $table->add_key('issuerid_key', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); + $table->add_key('uniq_key', XMLDB_KEY_UNIQUE, array('userid', 'issuerid', 'username')); + + // Adding indexes to table auth_oauth2_linked_login. + $table->add_index('search_index', XMLDB_INDEX_NOTUNIQUE, array('issuerid', 'username')); + + // Conditionally launch create table for auth_oauth2_linked_login. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Oauth2 savepoint reached. + upgrade_plugin_savepoint(true, 2017030700, 'auth', 'oauth2'); + } + + return true; +} diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php index ce34fa5aae2..74ee233d0c4 100644 --- a/auth/oauth2/lang/en/auth_oauth2.php +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -27,3 +27,13 @@ $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['notloggedin'] = 'The login attempt failed.'; $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the moodle login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['pluginname'] = 'OAuth 2'; +$string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; +$string['linkedlogins'] = 'Linked logins'; +$string['linkedloginshelp'] = 'Help with linked logins.'; +$string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; +$string['issuer'] = 'OAuth 2 Service'; +$string['info'] = 'External account'; +$string['createnewlinkedlogin'] = 'Link a new account ({$a})'; +$string['allowlinkedlogins'] = 'Allow linked logins'; +$string['allowlinkedloginsdesc'] = 'Linked logins allow users to link their Moodle account to another external account which they can use to login with.'; +$string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; diff --git a/auth/oauth2/lib.php b/auth/oauth2/lib.php new file mode 100644 index 00000000000..17040112bd3 --- /dev/null +++ b/auth/oauth2/lib.php @@ -0,0 +1,19 @@ +parent->find('useraccount', navigation_node::TYPE_CONTAINER); + $thingnode = $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); + } + } + } +} + diff --git a/auth/oauth2/linkedlogins.php b/auth/oauth2/linkedlogins.php new file mode 100644 index 00000000000..a5ffbb08151 --- /dev/null +++ b/auth/oauth2/linkedlogins.php @@ -0,0 +1,103 @@ +. + +/** + * OAuth 2 Linked login configuration page. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/auth/oauth2/linkedlogins.php'); +$PAGE->set_context(context_user::instance($USER->id)); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('linkedlogins', 'auth_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +if (!get_config('auth_oauth2', 'allowlinkedlogins')) { + throw new moodle_exception('Linked logins are disabled.'); +} + +$action = optional_param('action', '', PARAM_ALPHAEXT); +if ($action == 'new') { + require_sesskey(); + $issuerid = required_param('issuerid', PARAM_INT); + $issuer = \core\oauth2\api::get_issuer($issuerid); + + + // We do a login dance with this issuer. + $addparams = ['action' => 'new', 'issuerid' => $issuerid, 'sesskey' => sesskey()]; + $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); + $client = \core\oauth2\api::get_user_oauth_client($issuer, $addurl); + + if (optional_param('logout', false, PARAM_BOOL)) { + $client->log_out(); + } + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + + $userinfo = $client->get_userinfo(); + + if (!empty($userinfo)) { + \auth_oauth2\api::link_login($userinfo, $issuer); + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } else { + redirect($PAGE->url, get_string('notloggedin', 'auth_oauth2'), null, \core\output\notification::NOTIFY_ERROR); + } +} else if ($action == 'delete') { + require_sesskey(); + $linkedloginid = required_param('linkedloginid', PARAM_INT); + + auth_oauth2\api::delete_linked_login($linkedloginid); + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); +} + +$renderer = $PAGE->get_renderer('auth_oauth2'); + +$linkedloginid = optional_param('id', '', PARAM_RAW); +$linkedlogin = null; + +echo $OUTPUT->header(); +echo $OUTPUT->heading(get_string('linkedlogins', 'auth_oauth2')); +echo $OUTPUT->doc_link('Linked_Logins', get_string('linkedloginshelp', 'auth_oauth2')); +$linkedlogins = auth_oauth2\api::get_linked_logins(); + +echo $renderer->linked_logins_table($linkedlogins); + +$issuers = \core\oauth2\api::get_all_issuers(); + +foreach ($issuers as $issuer) { + if (!$issuer->is_authentication_supported()) { + continue; + } + + $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true]; + $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); + echo $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get('name')))); +} +echo $OUTPUT->footer(); + + diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php index 11f8fe144dc..7b2703b2298 100644 --- a/auth/oauth2/version.php +++ b/auth/oauth2/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2017030700; // The current plugin version (Date: YYYYMMDDXX) $plugin->requires = 2016112900; // Requires this Moodle version $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics)