diff --git a/admin/oauth2callback.php b/admin/oauth2callback.php index 08bb01d78e3..709e28ce99b 100644 --- a/admin/oauth2callback.php +++ b/admin/oauth2callback.php @@ -30,6 +30,17 @@ require_once(__DIR__ . '/../config.php'); +$error = optional_param('error', '', PARAM_RAW); +if ($error) { + $message = optional_param('error_description', '', PARAM_RAW); + if ($message) { + print_error($message); + } else { + print_error($error); + } + die(); +} + // The authorization code generated by the authorization server. $code = required_param('code', PARAM_RAW); // The state parameter we've given (used in moodle as a redirect url). diff --git a/admin/tool/oauth2/classes/form/issuer.php b/admin/tool/oauth2/classes/form/issuer.php new file mode 100644 index 00000000000..ce03af63e4a --- /dev/null +++ b/admin/tool/oauth2/classes/form/issuer.php @@ -0,0 +1,107 @@ +. + +/** + * This file contains the form add/update oauth2 issuer. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_oauth2\form; +defined('MOODLE_INTERNAL') || die(); + +use stdClass; +use core\form\persistent; + +/** + * Issuer form. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class issuer extends persistent { + + protected static $persistentclass = 'core\\oauth2\\issuer'; + + protected static $fieldstoremove = array('submitbutton', 'action'); + + /** + * Define the form - called by parent constructor + */ + public function definition() { + global $PAGE; + + $mform = $this->_form; + $provider = $this->get_persistent(); + + $mform->addElement('header', 'generalhdr', get_string('general')); + + // Name. + $mform->addElement('text', 'name', get_string('issuername', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('name', null, 'required', null, 'client'); + $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('name', 'issuername', 'tool_oauth2'); + + // Client ID. + $mform->addElement('text', 'clientid', get_string('issuerclientid', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('clientid', null, 'required', null, 'client'); + $mform->addRule('clientid', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('clientid', 'issuerclientid', 'tool_oauth2'); + + // Client Secret. + $mform->addElement('text', 'clientsecret', get_string('issuerclientsecret', 'tool_oauth2'), 'maxlength="255"'); + $mform->addRule('clientsecret', null, 'required', null, 'client'); + $mform->addRule('clientsecret', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); + $mform->addHelpButton('clientsecret', 'issuerclientsecret', 'tool_oauth2'); + + // Base Url. + $mform->addElement('text', 'baseurl', get_string('issuerbaseurl', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('baseurl', null, 'required', null, 'client'); + $mform->addRule('baseurl', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('baseurl', 'issuerbaseurl', 'tool_oauth2'); + + // Offline access type + $options = $provider->get_behaviour_list(); + $mform->addElement('select', 'behaviour', get_string('issuerbehaviour', 'tool_oauth2'), $options); + $mform->addHelpButton('behaviour', 'issuerbehaviour', 'tool_oauth2'); + + // Image. + $mform->addElement('text', 'image', get_string('issuerimage', 'tool_oauth2'), 'maxlength="1024"'); + $mform->addRule('image', get_string('maximumchars', '', 1024), 'maxlength', 1024, 'client'); + $mform->addHelpButton('image', 'issuername', 'tool_oauth2'); + + // Show on login page. + $mform->addElement('checkbox', 'showonloginpage', get_string('issuershowonloginpage', 'tool_oauth2')); + $mform->addHelpButton('showonloginpage', 'issuershowonloginpage', 'tool_oauth2'); + + + $mform->addElement('hidden', 'sortorder'); + $mform->setType('sortorder', PARAM_INT); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_RAW); + + $mform->addElement('hidden', 'id', $provider->get('id')); + $mform->setType('id', PARAM_INT); + + $this->add_action_buttons(true, get_string('savechanges', 'tool_oauth2')); + } + +} + diff --git a/admin/tool/oauth2/classes/output/renderer.php b/admin/tool/oauth2/classes/output/renderer.php new file mode 100644 index 00000000000..f26ca777f91 --- /dev/null +++ b/admin/tool/oauth2/classes/output/renderer.php @@ -0,0 +1,167 @@ +. + +/** + * Output rendering for the plugin. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace tool_oauth2\output; + +use plugin_renderer_base; +use html_table; +use html_table_cell; +use html_table_row; +use html_writer; +use core\oauth2\issuer; +use core\oauth2\api; +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 issuers. + * + * @param \core\oauth2\issuer[] $issuers - list of all issuers. + * @return string HTML to output. + */ + public function issuers_table($issuers) { + global $CFG, $OUTPUT; + + $table = new html_table(); + $table->head = [ + get_string('name'), + get_string('configuredstatus', 'tool_oauth2'), + get_string('loginissuer', 'tool_oauth2'), + get_string('discoverystatus', 'tool_oauth2'), + get_string('systemauthstatus', 'tool_oauth2'), + get_string('edit'), + ]; + $table->attributes['class'] = 'admintable generaltable'; + $data = []; + + $index = 0; + + foreach ($issuers as $issuer) { + // We need to handle the first and last ones specially. + $first = false; + if ($index == 0) { + $first = true; + } + $last = false; + if ($index == count($issuers) - 1) { + $last = true; + } + + // Name. + $name = $issuer->get('name'); + $image = $issuer->get('image'); + if ($image) { + $name = ' ' . $name; + } + $namecell = new html_table_cell($name); + $namecell->header = true; + + // Configured. + if (!empty($issuer->get('clientid')) && !empty($issuer->get('clientsecret'))) { + $configured = $OUTPUT->pix_icon('yes', get_string('configured', 'tool_oauth2'), 'tool_oauth2'); + } else { + $configured = $OUTPUT->pix_icon('no', get_string('notconfigured', 'tool_oauth2'), 'tool_oauth2'); + } + $configuredstatuscell = new html_table_cell($configured); + + // Login issuer. + if (!empty($issuer->get('showonloginpage'))) { + $loginissuer = $OUTPUT->pix_icon('yes', get_string('loginissuer', 'tool_oauth2'), 'tool_oauth2'); + } else { + $loginissuer = $OUTPUT->pix_icon('no', get_string('notloginissuer', 'tool_oauth2'), 'tool_oauth2'); + } + $loginissuerstatuscell = new html_table_cell($loginissuer); + + // Discovered. + if (!empty($issuer->get('scopessupported'))) { + $discovered = $OUTPUT->pix_icon('yes', get_string('discovered', 'tool_oauth2'), 'tool_oauth2'); + } else { + $discovered = $OUTPUT->pix_icon('no', get_string('notdiscovered', 'tool_oauth2'), 'tool_oauth2'); + } + $discoverystatuscell = new html_table_cell($discovered); + + // Connected. + if ($issuer->is_system_account_connected()) { + $systemauth = $OUTPUT->pix_icon('yes', get_string('systemaccountconnected', 'tool_oauth2'), 'tool_oauth2'); + } else { + $systemauth = $OUTPUT->pix_icon('no', get_string('systemaccountnotconnected', 'tool_oauth2'), 'tool_oauth2'); + } + + if ($issuer->is_system_account_setup_supported()) { + $params = ['id' => $issuer->get('id'), 'action' => 'auth']; + $authurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $icon = $OUTPUT->pix_icon('auth', get_string('connectsystemaccount', 'tool_oauth2'), 'tool_oauth2'); + $authlink = html_writer::link($authurl, $icon); + $systemauth .= ' ' . $authlink; + } + + $systemauthstatuscell = new html_table_cell($systemauth); + + // Action links. + $links = ''; + $editurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'edit']); + $editlink = html_writer::link($editurl, $OUTPUT->pix_icon('t/edit', get_string('edit'))); + + $links .= ' ' . $editlink; + $deleteurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['id' => $issuer->get('id'), 'action' => 'delete']); + $deletelink = html_writer::link($deleteurl, $OUTPUT->pix_icon('t/delete', get_string('delete'))); + $links .= ' ' . $deletelink; + if (!$last) { + $params = ['id' => $issuer->get('id'), 'action' => 'movedown', 'sesskey' => sesskey()]; + $movedownurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $movedownlink = html_writer::link($movedownurl, $OUTPUT->pix_icon('t/down', get_string('movedown'))); + $links .= ' ' . $movedownlink; + } + if (!$first) { + $params = ['id' => $issuer->get('id'), 'action' => 'moveup', 'sesskey' => sesskey()]; + $moveupurl = new moodle_url('/admin/tool/oauth2/issuers.php', $params); + $moveuplink = html_writer::link($moveupurl, $OUTPUT->pix_icon('t/up', get_string('moveup'))); + $links .= ' ' . $moveuplink; + } + + $editcell = new html_table_cell($links); + + $row = new html_table_row([ + $namecell, + $configuredstatuscell, + $loginissuerstatuscell, + $discoverystatuscell, + $systemauthstatuscell, + $editcell, + ]); + + $data[] = $row; + $index++; + } + $table->data = $data; + return html_writer::table($table); + } +} diff --git a/admin/tool/oauth2/issuers.php b/admin/tool/oauth2/issuers.php new file mode 100644 index 00000000000..d80f91d1e2a --- /dev/null +++ b/admin/tool/oauth2/issuers.php @@ -0,0 +1,142 @@ +. + +/** + * OAuth 2 Configuration page. + * + * @package tool_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('/admin/tool/oauth2/issuers.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('pluginname', 'tool_oauth2'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$renderer = $PAGE->get_renderer('tool_oauth2'); + +$action = optional_param('action', '', PARAM_ALPHAEXT); +$idpid = optional_param('id', '', PARAM_RAW); +$issuer = null; +$mform = null; + +if ($idpid) { + $issuer = \core\oauth2\api::get_issuer($idpid); + if (!$issuer) { + print_error('invaliddata'); + } +} + +if ($action == 'edit') { + if ($issuer) { + $PAGE->navbar->add(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + } else { + $PAGE->navbar->add(get_string('createnewissuer', 'tool_oauth2')); + } + + $mform = new \tool_oauth2\form\issuer(null, ['persistent' => $issuer]); +} + +if ($mform && $mform->is_cancelled()) { + redirect(new moodle_url('/admin/tool/oauth2/issuers.php')); +} else if ($action == 'edit') { + + if ($data = $mform->get_data()) { + + try { + if (!empty($data->id)) { + core\oauth2\api::update_issuer($data); + } else { + core\oauth2\api::create_issuer($data); + } + redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); + } catch (Exception $e) { + redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); + } + } else { + echo $OUTPUT->header(); + if ($issuer) { + echo $OUTPUT->heading(get_string('editissuer', 'tool_oauth2', $issuer->get('name'))); + } else { + echo $OUTPUT->heading(get_string('createnewissuer', 'tool_oauth2')); + } + $mform->display(); + echo $OUTPUT->footer(); + } + +} else if ($action == 'delete') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = ['action' => 'delete', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); + echo $OUTPUT->header(); + echo $OUTPUT->confirm(get_string('deleteconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + core\oauth2\api::delete_issuer($idpid); + redirect($PAGE->url, get_string('issuerdeleted', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } + +} else if ($action == 'auth') { + + if (!optional_param('confirm', false, PARAM_BOOL)) { + $continueparams = ['action' => 'auth', 'id' => $idpid, 'sesskey' => sesskey(), 'confirm' => true]; + $continueurl = new moodle_url('/admin/tool/oauth2/issuers.php', $continueparams); + $cancelurl = new moodle_url('/admin/tool/oauth2/issuers.php'); + echo $OUTPUT->header(); + echo $OUTPUT->confirm(get_string('authconfirm', 'tool_oauth2', $issuer->get('name')), $continueurl, $cancelurl); + echo $OUTPUT->footer(); + } else { + require_sesskey(); + $params = ['sesskey' => sesskey(), 'id' => $idpid, 'action' => 'auth', 'confirm' => true, 'response' => true]; + if (core\oauth2\api::connect_system_account($issuer, new moodle_url('/admin/tool/oauth2/issuers.php', $params))) { + redirect($PAGE->url, get_string('authconnected', 'tool_oauth2'), null, \core\output\notification::NOTIFY_SUCCESS); + } else { + redirect($PAGE->url, get_string('authnotconnected', 'tool_oauth2'), null, \core\output\notification::NOTIFY_ERROR); + } + } +} else if ($action == 'moveup') { + require_sesskey(); + core\oauth2\api::move_up_issuer($idpid); + redirect($PAGE->url); + +} else if ($action == 'movedown') { + require_sesskey(); + core\oauth2\api::move_down_issuer($idpid); + redirect($PAGE->url); + +} else { + echo $OUTPUT->header(); + $issuers = core\oauth2\api::get_all_issuers(); + echo $renderer->issuers_table($issuers); + + $addurl = new moodle_url('/admin/tool/oauth2/issuers.php', ['action' => 'edit']); + echo $renderer->single_button($addurl, get_string('createnewissuer', 'tool_oauth2')); + echo $OUTPUT->footer(); +} diff --git a/admin/tool/oauth2/lang/en/tool_oauth2.php b/admin/tool/oauth2/lang/en/tool_oauth2.php new file mode 100644 index 00000000000..73bce13bed4 --- /dev/null +++ b/admin/tool/oauth2/lang/en/tool_oauth2.php @@ -0,0 +1,63 @@ +. + +/** + * Strings for component 'tool_oauth2', language 'en' + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Open ID Connect configuration'; +$string['editissuer'] = 'Edit identity issuer: {$a}'; +$string['issuername'] = 'Name'; +$string['issuername_help'] = 'Name of the identity issuer. May be displayed on login page.'; +$string['issuerimage'] = 'Logo URL'; +$string['issuerimage_help'] = 'An image url used to show a logo for this issuer. May be displayed on login page.'; +$string['issuerclientid'] = 'Client Id'; +$string['issuerclientid_help'] = 'The OAuth client ID for this issuer.'; +$string['issuerclientsecret'] = 'Client Secret'; +$string['issuerclientsecret_help'] = 'The OAuth client secret for this issuer.'; +$string['issuerbaseurl'] = 'Service base url'; +$string['issuerbaseurl_help'] = 'Base url used to access the service.'; +$string['issuershowonloginpage'] = 'Show on login page.'; +$string['issuershowonloginpage_help'] = 'If the OpenID Connect Authentication plugin is enabled, this login issuer will be listed on the login page to allow users to login with accounts from this issuer.'; +$string['issuerbehaviour'] = 'Behaviour'; +$string['issuerbehaviour_help'] = 'Choose from one of the supported behaviours. + +* OAuth 2.0 - OAuth 2.0 API with no authentication +* OpenID Connect - Standards based OAuth 2.0 API with Authentication +* Microsoft OAuth 2.0 - Non-standard OAuth 2.0 combined with Microsoft Graph API'; +$string['savechanges'] = 'Save changes'; +$string['configuredstatus'] = 'Configured'; +$string['discoverystatus'] = 'Discovery'; +$string['systemauthstatus'] = 'System account connected'; +$string['configured'] = 'Configured'; +$string['notconfigured'] = 'Not configured'; +$string['discovered'] = 'Service discovery successful'; +$string['notdiscovered'] = 'Service discovery not successful'; +$string['loginissuer'] = 'Allow login'; +$string['notloginissuer'] = 'Do not allow login'; +$string['systemaccountconnected'] = 'System account connected'; +$string['systemaccountnotconnected'] = 'System account not connected'; +$string['createnewissuer'] = 'Create new identity issuer'; +$string['deleteconfirm'] = 'Are you sure you want to delete the identity issuer "{$a}"? Any plugins relying on this issuer will stop working.'; +$string['issuerdeleted'] = 'Identity issuer deleted'; +$string['connectsystemaccount'] = 'Connect to a system account'; +$string['authconfirm'] = 'This action will grant permanent API access to Moodle for the authenticated account. This is intended to be used as a system account for managing files owned by Moodle.'; +$string['authconnected'] = 'The system account is now connected for offline access'; +$string['authnotconnected'] = 'The system account was not connected for offline access'; diff --git a/admin/tool/oauth2/pix/auth.svg b/admin/tool/oauth2/pix/auth.svg new file mode 100644 index 00000000000..409824ad998 --- /dev/null +++ b/admin/tool/oauth2/pix/auth.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/pix/no.svg b/admin/tool/oauth2/pix/no.svg new file mode 100644 index 00000000000..0185d868b95 --- /dev/null +++ b/admin/tool/oauth2/pix/no.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/pix/yes.svg b/admin/tool/oauth2/pix/yes.svg new file mode 100644 index 00000000000..714d4c73c79 --- /dev/null +++ b/admin/tool/oauth2/pix/yes.svg @@ -0,0 +1,3 @@ + +]> \ No newline at end of file diff --git a/admin/tool/oauth2/settings.php b/admin/tool/oauth2/settings.php new file mode 100644 index 00000000000..950a02137d1 --- /dev/null +++ b/admin/tool/oauth2/settings.php @@ -0,0 +1,29 @@ +. + +/** + * Oauth2 system configuration. + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { + $ADMIN->add('server', new admin_externalpage('oauth2', new lang_string('pluginname','tool_oauth2'), "$CFG->wwwroot/$CFG->admin/tool/oauth2/issuers.php")); +} diff --git a/admin/tool/oauth2/tests/behat/manage_tasks.feature b/admin/tool/oauth2/tests/behat/manage_tasks.feature new file mode 100644 index 00000000000..73fc27dd61b --- /dev/null +++ b/admin/tool/oauth2/tests/behat/manage_tasks.feature @@ -0,0 +1,53 @@ +@tool @tool_task @javascript +Feature: Manage scheduled tasks + In order to configure scheduled tasks + As an admin + I need to be able to disable, enable, edit and reset to default scheduled tasks + + Background: + Given I log in as "admin" + And I navigate to "Scheduled tasks" node in "Site administration > Server" + + Scenario: Disable scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | disabled | 1 | + And I press "Save changes" + Then I should see "Changes saved" + And I should see "Task disabled" in the "Log table cleanup" "table_row" + + Scenario: Enable scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | disabled | 0 | + And I press "Save changes" + Then I should see "Changes saved" + And I should not see "Task disabled" in the "Log table cleanup" "table_row" + + Scenario: Edit scheduled task + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | minute | */5 | + | hour | 1 | + | day | 2 | + | month | 3 | + | dayofweek | 4 | + And I press "Save changes" + Then I should see "Changes saved" + And the following should exist in the "admintable" table: + | Component | Minute | Hour | Day | Day of week | Month | + | Standard log | */5 | 1 | 2 | 4 | 3 | + + Scenario: Reset scheduled task to default + When I click on "Edit task schedule: Log table cleanup" "link" in the "Log table cleanup" "table_row" + Then I should see "Edit task schedule: Log table cleanup" + And I set the following fields to these values: + | resettodefaults | 1 | + And I press "Save changes" + Then I should see "Changes saved" + And the following should not exist in the "admintable" table: + | Name | Component | Minute | Hour | Day | Day of week | Month | + | Log table cleanup | Standard log | */5 | 1 | 2 | 4 | 3 | \ No newline at end of file diff --git a/admin/tool/oauth2/tests/form_test.php b/admin/tool/oauth2/tests/form_test.php new file mode 100644 index 00000000000..a9b8aa12bf9 --- /dev/null +++ b/admin/tool/oauth2/tests/form_test.php @@ -0,0 +1,275 @@ +. + +/** + * File containing tests for the mform class. + * + * @package tool_task + * @copyright 2014 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +/** + * Mform test class. + * + * @package tool_task + * @copyright 2014 onwards Ankit Agarwal + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or late + */ +class tool_task_form_testcase extends advanced_testcase { + + /** + * Test validations for minute field. + */ + public function test_validate_fields_minute() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '*/65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20,30,45'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25,75'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('minute', '25-75'); + $this->assertFalse($valid); + } + + /** + * Test validations for minute hour. + */ + public function test_validate_fields_hour() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '*/65'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20,30,45'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25,75'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('hour', '25-75'); + $this->assertFalse($valid); + } + + /** + * Test validations for day field. + */ + public function test_validate_fields_day() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/65'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2,20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20,30,25'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65,20,30'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '2-20'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '20-30'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '65-20'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('day', '25-35'); + $this->assertFalse($valid); + } + + /** + * Test validations for month field. + */ + public function test_validate_fields_month() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '10'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,11'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2,10,12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65,2,13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '2-12'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '3-6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '65-2'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('month', '25-26'); + $this->assertFalse($valid); + } + + /** + * Test validations for dayofweek field. + */ + public function test_validate_fields_dayofweek() { + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '0'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '7'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '20'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/1'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '*/35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1,2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2,6,3'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65,2,13'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '25,35'); + $this->assertFalse($valid); + + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '1-2'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '2-6'); + $this->assertTrue($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '65-2'); + $this->assertFalse($valid); + $valid = \tool_task_edit_scheduled_task_form::validate_fields('dayofweek', '3-7'); + $this->assertFalse($valid); + } +} + diff --git a/admin/tool/oauth2/version.php b/admin/tool/oauth2/version.php new file mode 100644 index 00000000000..8d18262c360 --- /dev/null +++ b/admin/tool/oauth2/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package tool_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016120500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2016112900; // Requires this Moodle version +$plugin->component = 'tool_oauth2'; // Full name of the plugin (used for diagnostics) + diff --git a/auth/classes/output/login.php b/auth/classes/output/login.php index a58dfe64433..bebfed86915 100644 --- a/auth/classes/output/login.php +++ b/auth/classes/output/login.php @@ -121,10 +121,17 @@ class login implements renderable, templatable { } public function export_for_template(renderer_base $output) { - global $CFG; + global $CFG, $OUTPUT; $identityproviders = array_map(function($idp) use ($output) { - $idp['icon'] = $idp['icon']->export_for_template($output); + global $OUTPUT; + + if (!empty($idp['icon'])) { + $idp['iconurl'] = $OUTPUT->pix_url($idp['icon']->key, $idp['icon']->component); + } else if ($idp['iconurl'] instanceof moodle_url) { + $idp['iconurl'] = $idp['iconurl']->out(false); + } + unset($idp['icon']); if ($idp['url'] instanceof moodle_url) { $idp['url'] = $idp['url']->out(false); } diff --git a/auth/oauth2/auth.php b/auth/oauth2/auth.php new file mode 100644 index 00000000000..0d5ecd7f79a --- /dev/null +++ b/auth/oauth2/auth.php @@ -0,0 +1,40 @@ +. + +/** + * Open ID authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/authlib.php'); + +/** + * Plugin for oauth2 authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ +class auth_plugin_oauth2 extends \auth_oauth2\auth { + +} + + diff --git a/auth/oauth2/classes/auth.php b/auth/oauth2/classes/auth.php new file mode 100644 index 00000000000..4bf2cc9fb68 --- /dev/null +++ b/auth/oauth2/classes/auth.php @@ -0,0 +1,350 @@ +. + +/** + * Anobody can login with any password. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +namespace auth_oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use pix_icon; +use moodle_url; +use core_text; +use stdClass; +use core\oauth2\issuer; +use core\oauth2\client; + +require_once($CFG->libdir.'/authlib.php'); + +/** + * Plugin for oauth2 authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ +class auth extends \auth_plugin_base { + + /** + * @var stdClass $userinfo The set of user info returned from the oauth handshake + */ + private static $userinfo; + + /** + * @var stdClass $userpicture The url to a picture. + */ + private static $userpicture; + + /** + * Constructor. + */ + public function __construct() { + $this->authtype = 'oauth2'; + $this->config = get_config('auth_oauth2'); + } + + /** + * Returns true if the username and password work or don't exist and false + * if the user exists and the password is wrong. + * + * @param string $username The username + * @param string $password The password + * @return bool Authentication success or failure. + */ + public function user_login($username, $password) { + $cached = $this->get_static_user_info(); + $verifyusername = $cached['username']; + if ($verifyusername == $username) { + return true; + } + } + + /** + * We don't want to allow users setting an internal password. + * + * @return bool + */ + public function prevent_local_passwords() { + return true; + } + + /** + * Returns true if this authentication plugin is 'internal'. + * + * @return bool + */ + public function is_internal() { + return false; + } + + /** + * Indicates if moodle should automatically update internal user + * records with data from external sources using the information + * from auth_plugin_base::get_userinfo(). + * + * @return bool true means automatically copy data from ext to user table + */ + public function is_synchronised_with_external() { + return true; + } + + /** + * Returns true if this authentication plugin can change the user's + * password. + * + * @return bool + */ + public function can_change_password() { + return false; + } + + /** + * Returns the URL for changing the user's pw, or empty if the default can + * be used. + * + * @return moodle_url + */ + public function change_password_url() { + return null; + } + + /** + * Returns true if plugin allows resetting of internal password. + * + * @return bool + */ + public function can_reset_password() { + return false; + } + + /** + * Returns true if plugin can be manually set. + * + * @return bool + */ + public function can_be_manually_set() { + return true; + } + + /** + * Prints a form for configuring this authentication plugin. + * + * This function is called from admin/auth.php, and outputs a full page with + * a form for configuring this plugin. + * + * @param stdClass $config + * @param string $err + * @param array userfields + */ + public function config_form($config, $err, $userfields) { + echo get_string('plugindescription', 'auth_oauth2'); + + // 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; + } + + /** + * Return the userinfo from the oauth handshake. Will only be valid + * for the logged in user. + */ + public function get_userinfo($username) { + $cached = $this->get_static_user_info(); + if (!empty($cached) && $cached['username'] == $username) { + return $cached; + } + return false; + } + + private function is_ready_for_login_page($issuer) { + return !empty($issuer->get('clientid')) && + !empty($issuer->get('clientsecret')) && + $issuer->is_authentication_supported() && + !empty($issuer->get('showonloginpage')); + } + + /** + * Return a list of identity providers to display on the login page. + */ + public function loginpage_idp_list($wantsurl) { + $providers = \core\oauth2\api::get_all_issuers(); + $result = []; + if (empty($wantsurl)) { + $wantsurl = '/'; + } + foreach ($providers as $idp) { + if ($this->is_ready_for_login_page($idp)) { + $params = ['id' => $idp->get('id'), 'wantsurl' => $wantsurl, 'sesskey' => sesskey()]; + $url = new moodle_url('/auth/oauth2/login.php', $params); + $icon = $idp->get('image'); + $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get('name')]; + } + } + return $result; + } + + /** + * Statically cache the user info from the oauth handshake + * @param stdClass $userinfo + */ + private function set_static_user_info($userinfo) { + self::$userinfo = $userinfo; + } + + /** + * Get the static cached user info + * @return stdClass + */ + private function get_static_user_info() { + return self::$userinfo; + } + + /** + * Statically cache the user picture from the oauth handshake + * @param string $userpicture + */ + private function set_static_user_picture($userpicture) { + self::$userpicture = $userpicture; + } + + /** + * Get the static cached user picture + * @return string + */ + private function get_static_user_picture() { + return self::$userpicture; + } + + /** + * If this user has no picture - but we got one from oauth - set it. + * @return boolean True if the image was updated. + */ + private function update_picture($user) { + global $CFG, $DB, $USER; + + require_once($CFG->libdir . '/filelib.php'); + require_once($CFG->libdir . '/gdlib.php'); + + $fs = get_file_storage(); + $userid = $user->id; + if (!empty($user->picture)) { + return false; + } + $picture = $this->get_static_user_picture(); + if (empty($picture)) { + return false; + } + + $context = \context_user::instance($userid, MUST_EXIST); + $fs->delete_area_files($context->id, 'user', 'newicon'); + + $filerecord = array( + 'contextid' => $context->id, + 'component' => 'user', + 'filearea' => 'newicon', + 'itemid' => 0, + 'filepath' => '/', + 'filename' => 'image' + ); + + try { + $fs->create_file_from_string($filerecord, $picture); + } catch (\file_exception $e) { + return get_string($e->errorcode, $e->module, $e->a); + } + + $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); + + // There should only be one. + $iconfile = reset($iconfile); + + // Something went wrong while creating temp file - remove the uploaded file. + if (!$iconfile = $iconfile->copy_content_to_temp()) { + $fs->delete_area_files($context->id, 'user', 'newicon'); + return false; + } + + // Copy file to temporary location and the send it for processing icon. + $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); + // Delete temporary file. + @unlink($iconfile); + // Remove uploaded file. + $fs->delete_area_files($context->id, 'user', 'newicon'); + // Set the user's picture. + $updateuser = new stdClass(); + $updateuser->id = $userid; + $updateuser->picture = $newpicture; + $USER->picture = $newpicture; + user_update_user($updateuser); + return true; + } + + /** + * Complete the login process after oauth handshake is complete. + * @param \core\oauth2\client $client + * @param string $redirecturl + * @return none Either redirects or throws an exception + */ + public function complete_login(client $client, $redirecturl) { + global $CFG, $SESSION; + + $userinfo = $client->get_userinfo(); + + if (!$userinfo) { + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } + + $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); + + 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'], ''); + + if ($user) { + complete_user_login($user); + $this->update_picture($user); + redirect($redirecturl); + } + $errormsg = get_string('notloggedin', 'auth_oauth2'); + $SESSION->loginerrormsg = $errormsg; + redirect(new moodle_url($CFG->httpswwwroot . '/login/index.php')); + } +} + + diff --git a/auth/oauth2/lang/en/auth_oauth2.php b/auth/oauth2/lang/en/auth_oauth2.php new file mode 100644 index 00000000000..d221f328364 --- /dev/null +++ b/auth/oauth2/lang/en/auth_oauth2.php @@ -0,0 +1,29 @@ +. + +/** + * Strings for component 'auth_oauth2', language 'en'. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; +$string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; +$string['pluginname'] = 'OAuth 2'; +$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['notloggedin'] = 'The login attempt failed.'; diff --git a/auth/oauth2/login.php b/auth/oauth2/login.php new file mode 100644 index 00000000000..835a39b9ba7 --- /dev/null +++ b/auth/oauth2/login.php @@ -0,0 +1,45 @@ +. + +/** + * Open ID authentication. + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU Public License + */ + +require_once('../../config.php'); + +$issuerid = required_param('id', PARAM_INT); +$wantsurl = new moodle_url(optional_param('wantsurl', '/', PARAM_URL)); + +require_sesskey(); + +$issuer = new \core\oauth2\issuer($issuerid); + +$returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid]; +$returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams); + +$client = \core\oauth2\api::get_user_oauth_client($issuer, $returnurl); + +if ($client) { + $auth = new \auth_oauth2\auth(); + $auth->complete_login($client, $wantsurl); +} else { + throw new moodle_exception('Could not get an OAuth client.'); +} + diff --git a/auth/oauth2/version.php b/auth/oauth2/version.php new file mode 100644 index 00000000000..11f8fe144dc --- /dev/null +++ b/auth/oauth2/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version information + * + * @package auth_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2016120500; // 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) diff --git a/lib/classes/oauth2/api.php b/lib/classes/oauth2/api.php new file mode 100644 index 00000000000..00230d1620e --- /dev/null +++ b/lib/classes/oauth2/api.php @@ -0,0 +1,352 @@ +. + +/** + * Class for loading/storing oauth2 endpoints from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +require_once($CFG->libdir . '/filelib.php'); + +use context_system; +use curl; +use stdClass; +use moodle_exception; +use moodle_url; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Static list of api methods for system oauth2 configuration. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class api { + + /** + * Called from install.php and upgrade.php - install the default list of issuers + * @return int The number of issuers installed. + */ + public static function install_default_issuers() { + // Setup default list of identity issuers. + $record = (object) [ + 'name' => 'Google', + 'image' => 'https://accounts.google.com/favicon.ico', + 'behaviour' => issuer::BEHAVIOUR_OPENID_CONNECT, + 'baseurl' => 'http://accounts.google.com/', + 'clientid' => '', + 'clientsecret' => '', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $record); + $issuer->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'discovery_endpoint', + 'url' => 'https://accounts.google.com/.well-known/openid-configuration' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'name' => 'Microsoft', + 'image' => 'https://www.microsoft.com/favicon.ico', + 'behaviour' => issuer::BEHAVIOUR_MICROSOFT, + 'baseurl' => 'http://login.microsoftonline.com/common/oauth2/v2.0/', + 'clientid' => '', + 'clientsecret' => '', + 'showonloginpage' => true + ]; + + $issuer = new issuer(0, $record); + $issuer->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'authorization_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'token_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + + $record = (object) [ + 'issuerid' => $issuer->get('id'), + 'name' => 'end_session_endpoint', + 'url' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + ]; + $endpoint = new endpoint(0, $record); + $endpoint->create(); + return issuer::count_records(); + } + + public static function get_all_issuers() { + return issuer::get_records([], 'sortorder'); + } + + public static function get_issuer($id) { + return new issuer($id); + } + + public static function get_system_account(issuer $issuer) { + return system_account::get_record(['issuerid' => $issuer->get('id')]); + } + + public static function get_system_oauth_client(issuer $issuer) { + } + + public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '') { + $client = \core\oauth2\client::create($issuer, $currenturl, $additionalscopes); + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + return $client; + } + + public static function get_endpoints(issuer $issuer) { + require_capability('moodle/site:config', context_system::instance()); + + return endpoint::get_records(['issuerid' => $issuer->get('id')]); + } + + protected static function guess_image($issuer) { + if (empty($issuer->get('image'))) { + $baseurl = parse_url($issuer->get('discoveryurl')); + $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico'; + $issuer->set('image', $imageurl); + $issuer->update(); + } + } + + /** + * If the behaviour supports discovery for this issuer, try and determine the list of valid endpoints. + * + * @param issuer $issuer + * @return int The number of discovered services. + */ + protected static function discover_endpoints($issuer) { + $curl = new curl(); + + if ($issuer->get('behaviour') != issuer::BEHAVIOUR_OPENID_CONNECT) { + return 0; + } + + $url = $issuer->get_endpoint_url('discovery'); + if (!$url) { + $url = $issuer->get('url') . '/.well-known/openid-configuration'; + } + + if (!$json = $curl->get($issuer->get_endpoint_url('discovery'))) { + $msg = 'Could not discover end points for identity issuer' . $issuer->get('name'); + throw new moodle_exception($msg); + } + + if ($msg = $curl->error) { + throw new moodle_exception('Could not discover service endpoints: ' . $msg); + } + + $info = json_decode($json); + if (empty($info)) { + $msg = 'Could not discover end points for identity issuer' . $issuer->get('name'); + throw new moodle_exception($msg); + } + + foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) { + if ($endpoint->get('name') != 'discovery_endpoint') { + $endpoint->delete(); + } + } + + foreach ($info as $key => $value) { + if (substr_compare($key, '_endpoint', - strlen('_endpoint')) === 0) { + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->name = $key; + $record->url = $value; + + $endpoint = new endpoint(0, $record); + $endpoint->create(); + } + + if ($key == 'scopes_supported') { + $issuer->set('scopessupported', implode(' ', $value)); + $issuer->update(); + } + } + + return endpoint::count_records(['issuerid' => $issuer->get('id')]); + } + + public static function update_issuer($data) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer(0, $data); + + // Will throw exceptions on validation failures. + $issuer->update(); + + // Perform service discovery. + self::discover_endpoints($issuer); + self::guess_image($issuer); + } + + public static function create_issuer($data) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer(0, $data); + + // Will throw exceptions on validation failures. + $issuer->create(); + + // Perform service discovery. + self::discover_endpoints($issuer); + self::guess_image($issuer); + } + + /** + * Reorder this identity issuer. + * + * Requires moodle/site:config capability at the system context. + * + * @param int $id The id of the identity issuer to move. + * @return boolean + */ + public static function move_up_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $current = new issuer($id); + + $sortorder = $current->get('sortorder'); + if ($sortorder == 0) { + return false; + } + + $sortorder = $sortorder - 1; + $current->set('sortorder', $sortorder); + + $filters = array('sortorder' => $sortorder); + $children = issuer::get_records($filters, 'id'); + foreach ($children as $needtoswap) { + $needtoswap->set('sortorder', $sortorder + 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + public static function move_down_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $current = new issuer($id); + + $max = issuer::count_records(); + if ($max > 0) { + $max--; + } + + $sortorder = $current->get('sortorder'); + if ($sortorder >= $max) { + return false; + } + $sortorder = $sortorder + 1; + $current->set('sortorder', $sortorder); + + $filters = array('sortorder' => $sortorder); + $children = issuer::get_records($filters); + foreach ($children as $needtoswap) { + $needtoswap->set('sortorder', $sortorder - 1); + $needtoswap->update(); + } + + // OK - all set. + $result = $current->update(); + + return $result; + } + + public static function delete_issuer($id) { + require_capability('moodle/site:config', context_system::instance()); + $issuer = new issuer($id); + + $systemaccount = self::get_system_account($issuer); + if ($systemaccount) { + $systemaccount->delete(); + } + $endpoints = self::get_endpoints($issuer); + if ($endpoints) { + foreach ($endpoints as $endpoint) { + $endpoint->delete(); + } + } + + // Will throw exceptions on validation failures. + $issuer->delete(); + } + + public static function connect_system_account($issuer, $returnurl) { + require_capability('moodle/site:config', context_system::instance()); + + // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access. + $scopesrequired = 'openid email profile'; + + // Allow callbacks to inject non-standard scopes to the auth request. + + $client = client::create($issuer, $returnurl, $scopesrequired, true); + + if (!optional_param('response', false, PARAM_BOOL)) { + $client->log_out(); + } + + if (!$client->is_logged_in()) { + redirect($client->get_login_url()); + } + + $refreshtoken = $client->get_refresh_token(); + if (!$refreshtoken) { + return false; + } + + $systemaccount = self::get_system_account($issuer); + if ($systemaccount) { + $systemaccount->delete(); + } + $record = new stdClass(); + $record->issuerid = $issuer->get('id'); + $record->refreshtoken = $refreshtoken; + $record->grantedscopes = $scopesrequired; + + $systemaccount = new system_account(0, $record); + + $systemaccount->create(); + + $client->log_out(); + return true; + } +} diff --git a/lib/classes/oauth2/client.php b/lib/classes/oauth2/client.php new file mode 100644 index 00000000000..7410e64fad5 --- /dev/null +++ b/lib/classes/oauth2/client.php @@ -0,0 +1,117 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/oauthlib.php'); +require_once($CFG->libdir . '/filelib.php'); + +use moodle_url; +use curl; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class client extends \oauth2_client { + + /** @var \core\oauth2\issuer $issuer */ + private $issuer; + + /** @var bool $system */ + protected $system = false; + + /** + * Constructor. + * + * @param issuer $issuer + * @param moodle_url $returnurl + */ + public function __construct(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system) { + $this->issuer = $issuer; + $this->system = $system; + $scopes = $this->get_login_scopes(); + $additionalscopes = explode(' ', $scopesrequired); + + foreach ($additionalscopes as $scope) { + if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { + $scopes .= ' ' . $scope; + } + } + parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); + } + + public static function create(issuer $issuer, moodle_url $returnurl, $scopesrequired, $system = false) { + if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { + return new client_openid_connect($issuer, $returnurl, $scopesrequired, $system); + } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_OAUTH2) { + return new client_oauth2($issuer, $returnurl, $scopesrequired, $system); + } else if ($issuer->get('behaviour') == issuer::BEHAVIOUR_MICROSOFT) { + return new client_microsoft($issuer, $returnurl, $scopesrequired, $system); + } + } + + /** + * Returns the auth url for OAuth 2.0 request + * @return string the auth url + */ + protected function auth_url() { + return $this->issuer->get_endpoint_url('authorization'); + } + + public function get_issuer() { + return $this->issuer; + } + + public function get_additional_login_parameters() { + if ($this->issuer->get('behaviour') == issuer::BEHAVIOUR_OPENID_CONNECT) { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + return []; + } + + protected function get_login_scopes() { + return 'openid profile email'; + } + + /** + * Returns the token url for OAuth 2.0 request + * @return string the auth url + */ + protected function token_url() { + return $this->issuer->get_endpoint_url('token'); + } + + protected function get_tokenname() { + $name = static::class; + if ($this->system) { + $name .= '-system'; + } + return $name; + } + +} diff --git a/lib/classes/oauth2/client_microsoft.php b/lib/classes/oauth2/client_microsoft.php new file mode 100644 index 00000000000..3d0bcd617e7 --- /dev/null +++ b/lib/classes/oauth2/client_microsoft.php @@ -0,0 +1,54 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * We have to call directly to the graph APIs because the Microsoft Open ID Connect API is + * lame. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_microsoft extends client { + + public function get_additional_login_parameters() { + return ['prompt' => 'consent']; + } + + public function get_login_scopes() { + return 'openid profile email user.read'; + } + + public function get_userinfo() { + $me = $client->get('https://graph.microsoft.com/v1.0/me/'); + var_dump($me); + + + } + +} diff --git a/lib/classes/oauth2/client_oauth2.php b/lib/classes/oauth2/client_oauth2.php new file mode 100644 index 00000000000..96fb804abd6 --- /dev/null +++ b/lib/classes/oauth2/client_oauth2.php @@ -0,0 +1,43 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_oauth2 extends client { + + public function get_additional_login_parameters() { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + + +} diff --git a/lib/classes/oauth2/client_openid_connect.php b/lib/classes/oauth2/client_openid_connect.php new file mode 100644 index 00000000000..4ff9837f0cb --- /dev/null +++ b/lib/classes/oauth2/client_openid_connect.php @@ -0,0 +1,98 @@ +. + +/** + * Configurable oauth2 client class. + * + * @package core\oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use moodle_url; +use stdClass; +use Exception; + +/** + * Configurable oauth2 client class where the urls come from DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class client_openid_connect extends client { + + /** + * Returns a mapping of openid properties to moodle properties. + * + * @return array + */ + private function get_mapping() { + return [ + 'given_name' => 'firstname', + 'middle_name' => 'middlename', + 'family_name' => 'lastname', + 'email' => 'email', + 'username' => 'username', + 'website' => 'url', + 'nickname' => 'alternatename', + 'picture' => 'picture', + 'address' => 'address', + 'phone' => 'phone', + 'locale' => 'lang' + ]; + } + + public function get_additional_login_parameters() { + if ($this->system) { + return ['access_type' => 'offline', 'prompt' => 'consent']; + } + return []; + } + + public function get_userinfo() { + $url = $this->get_issuer()->get_endpoint_url('userinfo'); + $response = $this->get($url); + if (!$response) { + return false; + } + $userinfo = new stdClass(); + try { + $userinfo = json_decode($response); + } catch (Exception $e) { + return false; + } + if (!empty($userinfo->preferred_username)) { + $userinfo->username = $userinfo->preferred_username; + } else { + $userinfo->username = $userinfo->sub; + } + + $map = $this->get_mapping(); + + $user = new stdClass(); + foreach ($map as $openidproperty => $moodleproperty) { + if (!empty($userinfo->$openidproperty)) { + $user->$moodleproperty = $userinfo->$openidproperty; + } + } + + return (array)$user; + } + +} diff --git a/lib/classes/oauth2/endpoint.php b/lib/classes/oauth2/endpoint.php new file mode 100644 index 00000000000..fe3a0122e90 --- /dev/null +++ b/lib/classes/oauth2/endpoint.php @@ -0,0 +1,58 @@ +. + +/** + * Class for loading/storing oauth2 endpoints from the DB. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing oauth2 endpoints from the DB + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class endpoint extends persistent { + + const TABLE = 'oauth2_endpoint'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'name' => array( + 'type' => PARAM_RAW, + ), + 'url' => array( + 'type' => PARAM_URL, + ) + ); + } +} diff --git a/lib/classes/oauth2/issuer.php b/lib/classes/oauth2/issuer.php new file mode 100644 index 00000000000..71eec0994bb --- /dev/null +++ b/lib/classes/oauth2/issuer.php @@ -0,0 +1,131 @@ +. + +/** + * 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 core\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 issuer extends persistent { + + const TABLE = 'oauth2_issuer'; + + const BEHAVIOUR_OPENID_CONNECT = 'Open ID Connect'; + const BEHAVIOUR_MICROSOFT = 'Microsoft OAuth 2.0'; + const BEHAVIOUR_OAUTH2 = 'OAuth 2.0'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'name' => array( + 'type' => PARAM_TEXT + ), + 'image' => array( + 'type' => PARAM_URL, + 'null' => NULL_ALLOWED, + 'default' => null + ), + 'clientid' => array( + 'type' => PARAM_RAW + ), + 'clientsecret' => array( + 'type' => PARAM_RAW + ), + 'behaviour' => array( + 'type' => PARAM_NOTAGS, + 'choices' => array(self::BEHAVIOUR_OPENID_CONNECT, self::BEHAVIOUR_MICROSOFT, self::BEHAVIOUR_OAUTH2), + 'default' => self::BEHAVIOUR_OPENID_CONNECT + ), + 'baseurl' => array( + 'type' => PARAM_URL + ), + 'showonloginpage' => array( + 'type' => PARAM_BOOL, + 'default' => false + ), + 'scopessupported' => array( + 'type' => PARAM_RAW, + 'null' => NULL_ALLOWED, + 'default' => null + ), + 'sortorder' => array( + 'type' => PARAM_INT, + 'default' => 0, + ) + ); + } + + public function get_endpoint_url($type) { + $endpoint = endpoint::get_record([ + 'issuerid' => $this->get('id'), + 'name' => $type . '_endpoint' + ]); + + if ($endpoint) { + return $endpoint->get('url'); + } + return false; + } + public function is_authentication_supported() { + $supportedloginbehaviours = [ + self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_MICROSOFT, + ]; + return in_array($this->get('behaviour'), $supportedloginbehaviours); + } + + public function is_system_account_setup_supported() { + $supportedsystemaccountbehaviours = [ + self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_MICROSOFT, + ]; + return in_array($this->get('behaviour'), $supportedsystemaccountbehaviours); + } + + public function get_behaviour_list() { + return [ + self::BEHAVIOUR_OPENID_CONNECT => self::BEHAVIOUR_OPENID_CONNECT, + self::BEHAVIOUR_OAUTH2 => self::BEHAVIOUR_OAUTH2, + self::BEHAVIOUR_MICROSOFT => self::BEHAVIOUR_MICROSOFT + ]; + } + + public function is_system_account_connected() { + $sys = system_account::get_record(['issuerid' => $this->get('id')]); + if (!empty($sys) and !empty($sys->get('refreshtoken'))) { + return true; + } + return false; + } +} diff --git a/lib/classes/oauth2/system_account.php b/lib/classes/oauth2/system_account.php new file mode 100644 index 00000000000..ee682a7978f --- /dev/null +++ b/lib/classes/oauth2/system_account.php @@ -0,0 +1,60 @@ +. + +/** + * When using OAuth sometimes it makes sense to authenticate as a system user, and not the current user. + * In this case we use a refresh token to get an access token and the system admin must manually authorize the + * system account. + * + * @package core_oauth2 + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\oauth2; + +defined('MOODLE_INTERNAL') || die(); + +use core\persistent; + +/** + * Class for loading/storing oauth2 refresh tokens from the DB. + * + * @copyright 2017 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class system_account extends persistent { + + const TABLE = 'oauth2_system_account'; + + /** + * Return the definition of the properties of this model. + * + * @return array + */ + protected static function define_properties() { + return array( + 'issuerid' => array( + 'type' => PARAM_INT + ), + 'refreshtoken' => array( + 'type' => PARAM_RAW, + ), + 'grantedscopes' => array( + 'type' => PARAM_RAW, + ) + ); + } +} diff --git a/lib/db/install.php b/lib/db/install.php index f291828a1fa..8a45a6aa4ff 100644 --- a/lib/db/install.php +++ b/lib/db/install.php @@ -321,4 +321,6 @@ function xmldb_main_install() { require_once($CFG->libdir . '/db/upgradelib.php'); make_default_scale(); make_competence_scale(); + + \core\oauth2\api::install_default_providers(); } diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index 2e52477544f..0233dedcf37 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3463,5 +3463,55 @@ + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 69ef988fa08..189c3338734 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2611,5 +2611,98 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2017031400.00); } + if ($oldversion < 2017032400.21) { + + // Define table oauth2_issuer to be created. + $table = new xmldb_table('oauth2_issuer'); + + // Adding fields to table oauth2_issuer. + $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('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('image', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('baseurl', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('clientid', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('clientsecret', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('behaviour', XMLDB_TYPE_CHAR, '32', null, XMLDB_NOTNULL, null, 'none'); + $table->add_field('scopessupported', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('showonloginpage', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1'); + $table->add_field('sortorder', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_issuer. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Conditionally launch create table for oauth2_issuer. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.21); + } + + if ($oldversion < 2017032400.22) { + + // Define table oauth2_endpoint to be created. + $table = new xmldb_table('oauth2_endpoint'); + + // Adding fields to table oauth2_endpoint. + $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('name', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('url', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_endpoint. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('issuer_id_key', XMLDB_KEY_FOREIGN, array('issuerid'), 'oauth2_issuer', array('id')); + + // Conditionally launch create table for oauth2_endpoint. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.22); + } + + if ($oldversion < 2017032400.23) { + + // Define table oauth2_system_account to be created. + $table = new xmldb_table('oauth2_system_account'); + + // Adding fields to table oauth2_system_account. + $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('issuerid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('refreshtoken', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + $table->add_field('grantedscopes', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null); + + // Adding keys to table oauth2_system_account. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + $table->add_key('issueridkey', XMLDB_KEY_FOREIGN_UNIQUE, array('issuerid'), 'oauth2_issuer', array('id')); + + // Conditionally launch create table for oauth2_system_account. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2017032400.23); + } + + if ($oldversion < 2017033100.01) { + + \core\oauth2\api::install_default_issuers(); + // Main savepoint reached. + upgrade_main_savepoint(true, 2017033100.01); + } + return true; } diff --git a/lib/filelib.php b/lib/filelib.php index 9cca2b9ad70..bdc90bd114b 100644 --- a/lib/filelib.php +++ b/lib/filelib.php @@ -2766,9 +2766,9 @@ class curl { // All settings of this class should be init here. $this->resetopt(); - if (!empty($settings['debug'])) { + //if (!empty($settings['debug'])) { $this->debug = true; - } + //} if (!empty($settings['cookie'])) { if($settings['cookie'] === true) { $this->cookie = $CFG->dataroot.'/curl_cookie.txt'; @@ -2949,6 +2949,7 @@ class curl { * Set HTTP Request Header * * @param array $header + * @param bool $replace If true, will remove any existing headers before appending the new one. */ public function setHeader($header) { if (is_array($header)) { diff --git a/lib/oauthlib.php b/lib/oauthlib.php index 7d83e82e68b..9efb6d100f6 100644 --- a/lib/oauthlib.php +++ b/lib/oauthlib.php @@ -393,6 +393,8 @@ abstract class oauth2_client extends curl { private $scope = ''; /** var stdClass access token object */ private $accesstoken = null; + /** var stdClass refresh token string */ + private $refreshtoken = ''; /** * Returns the auth url for OAuth 2.0 request @@ -463,6 +465,10 @@ abstract class oauth2_client extends curl { return new moodle_url('/admin/oauth2callback.php'); } + public function get_additional_login_parameters() { + return []; + } + /** * Returns the login link for this oauth request * @@ -471,15 +477,32 @@ abstract class oauth2_client extends curl { public function get_login_url() { $callbackurl = self::callback_url(); - $url = new moodle_url($this->auth_url(), - array('client_id' => $this->clientid, - 'response_type' => 'code', - 'redirect_uri' => $callbackurl->out(false), - 'state' => $this->returnurl->out_as_local_url(false), - 'scope' => $this->scope, - )); + $params = array_merge( + [ + 'client_id' => $this->clientid, + 'response_type' => 'code', + 'redirect_uri' => $callbackurl->out(false), + 'state' => $this->returnurl->out_as_local_url(false), + 'scope' => $this->scope, + ], + $this->get_additional_login_parameters() + ); - return $url; + return new moodle_url($this->auth_url(), $params); + } + + /** + * Given an array of name value pairs - build a valid HTTP POST application/x-www-form-urlencoded string. + * + * @param array $params Name / value pairs. + * @return string POST data. + */ + public function build_post_data($params) { + $result = []; + foreach ($params as $name => $value) { + $result[] = str_replace('&', '%26', $name) . '=' . str_replace('&', '%26', $value); + } + return implode('&', $result); } /** @@ -490,10 +513,10 @@ abstract class oauth2_client extends curl { */ public function upgrade_token($code) { $callbackurl = self::callback_url(); - $params = array('client_id' => $this->clientid, + $params = array('code' => $code, + 'client_id' => $this->clientid, 'client_secret' => $this->clientsecret, 'grant_type' => 'authorization_code', - 'code' => $code, 'redirect_uri' => $callbackurl->out(false), ); @@ -501,7 +524,7 @@ abstract class oauth2_client extends curl { if ($this->use_http_get()) { $response = $this->get($this->token_url(), $params); } else { - $response = $this->post($this->token_url(), $params); + $response = $this->post($this->token_url(), $this->build_post_data($params)); } if (!$this->info['http_code'] === 200) { @@ -514,6 +537,10 @@ abstract class oauth2_client extends curl { return false; } + if (isset($r->refresh_token)) { + $this->refreshtoken = $r->refresh_token; + } + // Store the token an expiry time. $accesstoken = new stdClass; $accesstoken->token = $r->access_token; @@ -552,7 +579,11 @@ abstract class oauth2_client extends curl { } } - return parent::request($murl->out(false), $options); + $response = parent::request($murl->out(false), $options); + + $this->resetHeader(); + + return $response; } /** @@ -603,6 +634,15 @@ abstract class oauth2_client extends curl { } } + /** + * Get a refresh token!!! + * + * @return string + */ + public function get_refresh_token() { + return $this->refreshtoken; + } + /** * Retrieve a token stored. * diff --git a/lib/templates/login.mustache b/lib/templates/login.mustache index e30c537624f..a0545c38573 100644 --- a/lib/templates/login.mustache +++ b/lib/templates/login.mustache @@ -148,9 +148,9 @@ {{#identityproviders}} diff --git a/theme/boost/templates/core/login.mustache b/theme/boost/templates/core/login.mustache index 3496224ef75..c253f728136 100644 --- a/theme/boost/templates/core/login.mustache +++ b/theme/boost/templates/core/login.mustache @@ -178,7 +178,12 @@ diff --git a/version.php b/version.php index f8fd9032855..8a5f4a2be68 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2017033100.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2017033100.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.