���ѧۧݧ�ӧ�� �ާ֧ߧ֧էا֧� - ���֧էѧܧ�ڧ��ӧѧ�� - /home3/cpr76684/public_html/oauth2.tar
���ѧ٧ѧ�
user_field_mapping.php 0000644 00000007076 15151222307 0011121 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing oauth2 endpoints from the DB. * * @package core * @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; use lang_string; /** * Class for loading/storing oauth2 user field mappings from the DB * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_field_mapping extends persistent { const TABLE = 'oauth2_user_field_mapping'; /** * Return the list of valid internal user fields. * * @return array */ private static function get_user_fields() { return array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username'], get_profile_field_names()); } /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'issuerid' => array( 'type' => PARAM_INT ), 'externalfield' => array( 'type' => PARAM_RAW_TRIMMED, ), 'internalfield' => array( 'type' => PARAM_ALPHANUMEXT, 'choices' => self::get_user_fields() ) ); } /** * Return the list of internal fields * in a format they can be used for choices in a select menu * @return array */ public function get_internalfield_list() { $userfields = array_merge(\core_user::AUTHSYNCFIELDS, ['picture', 'username']); $internalfields = array_combine($userfields, $userfields); return array_merge(['' => $internalfields], get_profile_field_list()); } /** * Return the list of internal fields with flat array * * Profile fields element has its array based on profile category. * These elements need to be turned flat to make it easier to read. * * @return array */ public function get_internalfields() { $userfieldlist = $this->get_internalfield_list(); $userfields = []; array_walk_recursive($userfieldlist, function($value, $key) use (&$userfields) { $userfields[] = $key; } ); return $userfields; } /** * Ensures that no HTML is saved to externalfield field * but preserves all special characters that can be a part of the claim * @return boolean true if validation is successful, string error if externalfield is not validated */ protected function validate_externalfield($value){ // This parameter type is set to PARAM_RAW_TRIMMED and HTML check is done here. if (clean_param($value, PARAM_NOTAGS) !== $value){ return new lang_string('userfieldexternalfield_error', 'tool_oauth2'); } return true; } } client/clever.php 0000644 00000003671 15151222307 0010020 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\client; use core\oauth2\client; /** * Class clever - Custom client handler to fetch data from Clever * * @package core * @copyright 2022 OpenStax * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class clever extends client { /** * Fetch the user id from the userinfo endpoint and then query userdata * * @return array|false */ public function get_userinfo() { $userinfo = parent::get_userinfo(); $userid = $userinfo['idnumber']; return $this->get_userdata($userid); } /** * Obtain user name and email data via the userdata endpoint * * @param string $userid User ID value * @return array|false */ private function get_userdata($userid) { $url = $this->get_issuer()->get_endpoint_url('userdata'); $url .= '/' . $userid; $response = $this->get($url); if (!$response) { return false; } $userinfo = json_decode($response); if (json_last_error() != JSON_ERROR_NONE) { debugging('Error encountered while decoding user information: ' . json_last_error_msg()); return false; } return $this->map_userinfo_to_fields($userinfo); } } client/linkedin.php 0000644 00000003661 15151222307 0010334 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\client; use core\oauth2\client; /** * Class linkedin - Custom client handler to fetch data from linkedin * * Custom oauth2 client for linkedin as it doesn't support OIDC and has a different way to get * key information for users - firstname, lastname, email. * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core */ class linkedin extends client { /** * Fetch the user info from the userinfo and email endpoint and map fields back * * @return array|false */ public function get_userinfo() { $user = array_merge(parent::get_userinfo(), $this->get_useremail()); return $user; } /** * Get the email address of the user from the email endpoint * * @return array|false */ private function get_useremail() { $url = $this->get_issuer()->get_endpoint_url('email'); $response = $this->get($url); if (!$response) { return false; } $userinfo = new \stdClass(); try { $userinfo = json_decode($response); } catch (\Exception $e) { return false; } return $this->map_userinfo_to_fields($userinfo); } } rest.php 0000644 00000011007 15151222307 0006227 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Rest API base class mapping rest api methods to endpoints with http methods, args and post body. * * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; use curl; use coding_exception; use stdClass; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); /** * Rest API base class mapping rest api methods to endpoints with http methods, args and post body. * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class rest { /** @var curl $curl */ protected $curl; /** * Constructor. * * @param curl $curl */ public function __construct(curl $curl) { $this->curl = $curl; } /** * Abstract function to define the functions of the rest API. * * @return array Example: * [ 'listFiles' => [ 'method' => 'get', 'args' => [ 'folder' => PARAM_STRING ], 'response' => 'json' ] ] */ public abstract function get_api_functions(); /** * Call a function from the Api with a set of arguments and optional data. * * @param string $functionname * @param array $functionargs * @param string $rawpost Optional param to include in the body of a post. * @param string $contenttype The MIME type for the request's Content-Type header. * @return string|stdClass */ public function call($functionname, $functionargs, $rawpost = false, $contenttype = false) { $functions = $this->get_api_functions(); $supportedmethods = [ 'get', 'put', 'post', 'patch', 'head', 'delete' ]; if (empty($functions[$functionname])) { throw new coding_exception('unsupported api functionname: ' . $functionname); } $method = $functions[$functionname]['method']; $endpoint = $functions[$functionname]['endpoint']; $responsetype = $functions[$functionname]['response']; if (!in_array($method, $supportedmethods)) { throw new coding_exception('unsupported api method: ' . $method); } $args = $functions[$functionname]['args']; $callargs = []; foreach ($args as $argname => $argtype) { if (isset($functionargs[$argname])) { $callargs[$argname] = clean_param($functionargs[$argname], $argtype); } } // Allow params in the URL path like /me/{parent}/children. foreach ($callargs as $argname => $value) { $newendpoint = str_replace('{' . $argname . '}', $value, $endpoint); if ($newendpoint != $endpoint) { $endpoint = $newendpoint; unset($callargs[$argname]); } } if ($rawpost !== false) { $queryparams = $this->curl->build_post_data($callargs); if (!empty($queryparams)) { $endpoint .= '?' . $queryparams; } $callargs = $rawpost; } if (empty($contenttype)) { $this->curl->setHeader('Content-type: application/json'); } else { $this->curl->setHeader('Content-type: ' . $contenttype); } $response = $this->curl->$method($endpoint, $callargs); if ($this->curl->errno == 0) { if ($responsetype == 'json') { $json = json_decode($response); if (!empty($json->error)) { throw new rest_exception($json->error->code . ': ' . $json->error->message); } return $json; } else if ($responsetype == 'headers') { $response = $this->curl->get_raw_response(); } return $response; } else { throw new rest_exception($this->curl->error, $this->curl->errno); } } } discovery/openidconnect.php 0000644 00000010404 15151222307 0012111 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\discovery; use stdClass; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; /** * Class for Open ID Connect discovery definition. * * @package core * @since Moodle 3.11 * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class openidconnect extends base_definition { /** * Get the URL for the discovery manifest. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return string The URL of the discovery file, containing the endpoints. */ public static function get_discovery_endpoint_url(issuer $issuer): string { $url = $issuer->get('baseurl'); if (!empty($url)) { // Add slash at the end of the base url. $url .= (substr($url, -1) == '/' ? '' : '/'); // Append the well-known file for OIDC. $url .= '.well-known/openid-configuration'; } return $url; } /** * Process the discovery information and create endpoints defined with the expected format. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @param stdClass $info The discovery information, with the endpoints to process and create. * @return void */ protected static function process_configuration_json(issuer $issuer, stdClass $info): void { 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(); } } } /** * Process how to map user field information. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return void */ protected static function create_field_mappings(issuer $issuer): void { // Remove existing user field mapping. foreach (user_field_mapping::get_records(['issuerid' => $issuer->get('id')]) as $userfieldmapping) { $userfieldmapping->delete(); } // Create the default user field mapping list. $mapping = [ 'given_name' => 'firstname', 'middle_name' => 'middlename', 'family_name' => 'lastname', 'email' => 'email', 'nickname' => 'alternatename', 'picture' => 'picture', 'address' => 'address', 'phone' => 'phone1', 'locale' => 'lang', ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } } /** * Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined. * * @param issuer $issuer The OAuth issuer to register. * @return void */ protected static function register(issuer $issuer): void { // Registration not supported (at least for now). } } discovery/imsbadgeconnect.php 0000644 00000016362 15151222307 0012417 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\discovery; use curl; use stdClass; use moodle_exception; use core\oauth2\issuer; use core\oauth2\endpoint; /** * Class for IMS Open Badge Connect API (aka OBv2.1) discovery definition. * * @package core * @since Moodle 3.11 * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class imsbadgeconnect extends base_definition { /** * Get the URL for the discovery manifest. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return string The URL of the discovery file, containing the endpoints. */ public static function get_discovery_endpoint_url(issuer $issuer): string { $url = $issuer->get('baseurl'); if (!empty($url)) { // Add slash at the end of the base url. $url .= (substr($url, -1) == '/' ? '' : '/'); // Append the well-known file for IMS OBv2.1. $url .= '.well-known/badgeconnect.json'; } return $url; } /** * Process the discovery information and create endpoints defined with the expected format. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @param stdClass $info The discovery information, with the endpoints to process and create. * @return void */ protected static function process_configuration_json(issuer $issuer, stdClass $info): void { $info = array_pop($info->badgeConnectAPI); foreach ($info as $key => $value) { if (substr_compare($key, 'Url', - strlen('Url')) === 0 && !empty($value)) { $record = new stdClass(); $record->issuerid = $issuer->get('id'); // Convert key names from xxxxUrl to xxxx_endpoint, in order to make it compliant with the Moodle oAuth API. $record->name = strtolower(substr($key, 0, - strlen('Url'))) . '_endpoint'; $record->url = $value; $endpoint = new endpoint(0, $record); $endpoint->create(); } else if ($key == 'scopesOffered') { // Get and update supported scopes. $issuer->set('scopessupported', implode(' ', $value)); $issuer->update(); } else if ($key == 'image' && empty($issuer->get('image'))) { // Update the image with the value in the manifest file if it's valid and empty in the issuer. $url = filter_var($value, FILTER_SANITIZE_URL); // Remove multiple slashes in URL. It will fix the Badgr bug with image URL defined in their manifest. $url = preg_replace('/([^:])(\/{2,})/', '$1/', $url); if (filter_var($url, FILTER_VALIDATE_URL) !== false) { $issuer->set('image', $url); $issuer->update(); } } } } /** * Process how to map user field information. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return void */ protected static function create_field_mappings(issuer $issuer): void { // In that case, there are no user fields to map. } /** * Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined. * * @param issuer $issuer The OAuth issuer to register. * @return void */ protected static function register(issuer $issuer): void { global $CFG, $SITE; $clientid = $issuer->get('clientid'); $clientsecret = $issuer->get('clientsecret'); // Registration request for getting client id and secret will be done only they are empty in the issuer. // For now this can't be run from PHPUNIT (because IMS testing platform needs real URLs). In the future, this // request can be moved to the moodle-exttests repository. if (empty($clientid) && empty($clientsecret) && (!defined('PHPUNIT_TEST') || !PHPUNIT_TEST)) { $url = $issuer->get_endpoint_url('registration'); if ($url) { $scopes = str_replace("\r", " ", $issuer->get('scopessupported')); // Add slash at the end of the site URL. $hosturl = $CFG->wwwroot; $hosturl .= (substr($CFG->wwwroot, -1) == '/' ? '' : '/'); // Create the registration request following the format defined in the IMS OBv2.1 specification. $request = [ 'client_name' => $SITE->fullname, 'client_uri' => $hosturl, 'logo_uri' => $hosturl . 'pix/f/moodle-256.png', 'tos_uri' => $hosturl, 'policy_uri' => $hosturl, 'software_id' => 'moodle', 'software_version' => $CFG->version, 'redirect_uris' => [ $hosturl . 'admin/oauth2callback.php' ], 'token_endpoint_auth_method' => 'client_secret_basic', 'grant_types' => [ 'authorization_code', 'refresh_token' ], 'response_types' => [ 'code' ], 'scope' => $scopes ]; $jsonrequest = json_encode($request); $curl = new curl(); $curl->setHeader(['Content-type: application/json']); $curl->setHeader(['Accept: application/json']); // Send the registration request. if (!$jsonresponse = $curl->post($url, $jsonrequest)) { $msg = 'Could not self-register identity issuer: ' . $issuer->get('name') . ". Wrong URL or JSON data [URL: $url]"; throw new moodle_exception($msg); } // Process the response and update client id and secret if they are valid. $response = json_decode($jsonresponse); if (property_exists($response, 'client_id')) { $issuer->set('clientid', $response->client_id); $issuer->set('clientsecret', $response->client_secret); $issuer->update(); } else { $msg = 'Could not self-register identity issuer: ' . $issuer->get('name') . '. Invalid response ' . $jsonresponse; throw new moodle_exception($msg); } } } } } discovery/base_definition.php 0000644 00000013412 15151222307 0012405 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\discovery; use curl; use stdClass; use moodle_exception; use core\oauth2\issuer; use core\oauth2\endpoint; /** * Class for provider discovery definition, to allow services easily discover and process information. * This abstract class is called from core\oauth2\api when discovery points need to be updated. * * @package core * @since Moodle 3.11 * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class base_definition { /** * Get the URL for the discovery manifest. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return string The URL of the discovery file, containing the endpoints. */ public abstract static function get_discovery_endpoint_url(issuer $issuer): string; /** * Process the discovery information and create endpoints defined with the expected format. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @param stdClass $info The discovery information, with the endpoints to process and create. * @return void */ protected abstract static function process_configuration_json(issuer $issuer, stdClass $info): void; /** * Process how to map user field information. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return void */ protected abstract static function create_field_mappings(issuer $issuer): void; /** * Self-register the issuer if the 'registration' endpoint exists and client id and secret aren't defined. * * @param issuer $issuer The OAuth issuer to register. * @return void */ protected abstract static function register(issuer $issuer): void; /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { static::discover_endpoints($issuer); return $issuer; } /** * If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints. * * @param issuer $issuer * @return int The number of discovered services. */ public static function discover_endpoints($issuer): int { // Early return if baseurl is empty. if (empty($issuer->get('baseurl'))) { return 0; } // Get the discovery URL and check if it has changed. $creatediscoveryendpoint = false; $url = $issuer->get_endpoint_url('discovery'); $providerurl = static::get_discovery_endpoint_url($issuer); if (!$url || $url != $providerurl) { $url = $providerurl; $creatediscoveryendpoint = true; } // Remove the existing endpoints before starting discovery. foreach (endpoint::get_records(['issuerid' => $issuer->get('id')]) as $endpoint) { // Discovery endpoint will be removed only if it will be created later, once we confirm it's working as expected. if ($creatediscoveryendpoint || $endpoint->get('name') != 'discovery_endpoint') { $endpoint->delete(); } } // Early return if discovery URL is empty. if (empty($url)) { return 0; } $curl = new curl(); if (!$json = $curl->get($url)) { $msg = 'Could not discover end points for identity issuer: ' . $issuer->get('name') . " [URL: $url]"; 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') . " [URL: $url]"; throw new moodle_exception($msg); } if ($creatediscoveryendpoint) { // Create the discovery endpoint (because it didn't exist and the URL exists and is returning some valid JSON content). static::create_discovery_endpoint($issuer, $url); } static::process_configuration_json($issuer, $info); static::create_field_mappings($issuer); static::register($issuer); return endpoint::count_records(['issuerid' => $issuer->get('id')]); } /** * Helper method to create discovery endpoint. * * @param issuer $issuer Issuer the endpoints should be created for. * @param string $url Discovery endpoint URL. * @return endpoint The endpoint created. * * @throws \core\invalid_persistent_exception */ protected static function create_discovery_endpoint(issuer $issuer, string $url): endpoint { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => 'discovery_endpoint', 'url' => $url, ]; $endpoint = new endpoint(0, $record); $endpoint->create(); return $endpoint; } } client.php 0000644 00000053573 15151222307 0006546 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Configurable oauth2 client class. * * @package core * @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 moodle_exception; use stdClass; /** * Configurable oauth2 client class. URLs come from DB and access tokens from either DB (system accounts) or session (users'). * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class client extends \oauth2_client { /** @var \core\oauth2\issuer $issuer */ private $issuer; /** @var bool $system */ protected $system = false; /** @var bool $autorefresh whether this client will use a refresh token to automatically renew access tokens.*/ protected $autorefresh = false; /** @var array $rawuserinfo Keep rawuserinfo from . */ protected $rawuserinfo = []; /** * Constructor. * * @param issuer $issuer * @param moodle_url|null $returnurl * @param string $scopesrequired * @param boolean $system * @param boolean $autorefresh whether refresh_token grants are used to allow continued access across sessions. */ public function __construct(issuer $issuer, $returnurl, $scopesrequired, $system = false, $autorefresh = false) { $this->issuer = $issuer; $this->system = $system; $this->autorefresh = $autorefresh; $scopes = $this->get_login_scopes(); $additionalscopes = explode(' ', $scopesrequired); foreach ($additionalscopes as $scope) { if (!empty($scope)) { if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { $scopes .= ' ' . $scope; } } } if (empty($returnurl)) { $returnurl = new moodle_url('/'); } $this->basicauth = $issuer->get('basicauth'); parent::__construct($issuer->get('clientid'), $issuer->get('clientsecret'), $returnurl, $scopes); } /** * 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'); } /** * Get the oauth2 issuer for this client. * * @return \core\oauth2\issuer Issuer */ public function get_issuer() { return $this->issuer; } /** * Override to append additional params to a authentication request. * * @return array (name value pairs). */ public function get_additional_login_parameters() { $params = ''; if ($this->system || $this->can_autorefresh()) { // System clients and clients supporting the refresh_token grant (provided the user is authenticated) add // extra params to the login request, depending on the issuer settings. The extra params allow a refresh // token to be returned during the authorization_code flow. if (!empty($this->issuer->get('loginparamsoffline'))) { $params = $this->issuer->get('loginparamsoffline'); } } else { // This is not a system client, nor a client supporting the refresh_token grant type, so just return the // vanilla login params. if (!empty($this->issuer->get('loginparams'))) { $params = $this->issuer->get('loginparams'); } } if (empty($params)) { return []; } $result = []; parse_str($params, $result); return $result; } /** * Override to change the scopes requested with an authentiction request. * * @return string */ protected function get_login_scopes() { if ($this->system || $this->can_autorefresh()) { // System clients and clients supporting the refresh_token grant (provided the user is authenticated) add // extra scopes to the login request, depending on the issuer settings. The extra params allow a refresh // token to be returned during the authorization_code flow. return $this->issuer->get('loginscopesoffline'); } else { // This is not a system client, nor a client supporting the refresh_token grant type, so just return the // vanilla login scopes. return $this->issuer->get('loginscopes'); } } /** * Returns the token url for OAuth 2.0 request * * We are overriding the parent function so we get this from the configured endpoint. * * @return string the auth url */ protected function token_url() { return $this->issuer->get_endpoint_url('token'); } /** * We want a unique key for each issuer / and a different key for system vs user oauth. * * @return string The unique key for the session value. */ protected function get_tokenname() { $name = 'oauth2-state-' . $this->issuer->get('id'); if ($this->system) { $name .= '-system'; } return $name; } /** * Store a token between requests. Uses session named by get_tokenname for user account tokens * and a database record for system account tokens. * * @param stdClass|null $token token object to store or null to clear */ protected function store_token($token) { if (!$this->system) { parent::store_token($token); return; } $this->accesstoken = $token; // Create or update a DB record with the new token. $persistedtoken = access_token::get_record(['issuerid' => $this->issuer->get('id')]); if ($token !== null) { if (!$persistedtoken) { $persistedtoken = new access_token(); $persistedtoken->set('issuerid', $this->issuer->get('id')); } // Update values from $token. Don't use from_record because that would skip validation. $persistedtoken->set('token', $token->token); if (isset($token->expires)) { $persistedtoken->set('expires', $token->expires); } else { // Assume an arbitrary time span of 1 week for access tokens without expiration. // The "refresh_system_tokens_task" is run hourly (by default), so the token probably won't last that long. $persistedtoken->set('expires', time() + WEEKSECS); } $persistedtoken->set('scope', $token->scope); $persistedtoken->save(); } else { if ($persistedtoken) { $persistedtoken->delete(); } } } /** * Retrieve a stored token from session (user accounts) or database (system accounts). * * @return stdClass|null token object */ protected function get_stored_token() { if ($this->system) { $token = access_token::get_record(['issuerid' => $this->issuer->get('id')]); if ($token !== false) { return $token->to_record(); } return null; } return parent::get_stored_token(); } /** * Get a list of the mapping user fields in an associative array. * * @return array */ protected function get_userinfo_mapping() { $fields = user_field_mapping::get_records(['issuerid' => $this->issuer->get('id')]); $map = []; foreach ($fields as $field) { $map[$field->get('externalfield')] = $field->get('internalfield'); } return $map; } /** * Override which upgrades the authorization code to an access token and stores any refresh token in the DB. * * @param string $code the authorisation code * @return bool true if the token could be upgraded * @throws moodle_exception */ public function upgrade_token($code) { $upgraded = parent::upgrade_token($code); if (!$this->can_autorefresh()) { return $upgraded; } // For clients supporting auto-refresh, try to store a refresh token. if (!empty($this->refreshtoken)) { $refreshtoken = (object) [ 'token' => $this->refreshtoken, 'scope' => $this->scope ]; $this->store_user_refresh_token($refreshtoken); } return $upgraded; } /** * Override which in addition to auth code upgrade, also attempts to exchange a refresh token for an access token. * * @return bool true if the user is logged in as a result, false otherwise. */ public function is_logged_in() { global $DB, $USER; $isloggedin = parent::is_logged_in(); // Attempt to exchange a user refresh token, but only if required and supported. if ($isloggedin || !$this->can_autorefresh()) { return $isloggedin; } // Autorefresh is supported. Try to negotiate a login by exchanging a stored refresh token for an access token. $issuerid = $this->issuer->get('id'); $refreshtoken = $DB->get_record('oauth2_refresh_token', ['userid' => $USER->id, 'issuerid' => $issuerid]); if ($refreshtoken) { try { $tokensreceived = $this->exchange_refresh_token($refreshtoken->token); if (empty($tokensreceived)) { // No access token was returned, so invalidate the refresh token and return false. $DB->delete_records('oauth2_refresh_token', ['id' => $refreshtoken->id]); return false; } // Otherwise, save the access token and, if provided, the new refresh token. $this->store_token($tokensreceived['access_token']); if (!empty($tokensreceived['refresh_token'])) { $this->store_user_refresh_token($tokensreceived['refresh_token']); } return true; } catch (\moodle_exception $e) { // The refresh attempt failed either due to an error or a bad request. A bad request could be received // for a number of reasons including expired refresh token (lifetime is not specified in OAuth 2 spec), // scope change or if app access has been revoked manually by the user (tokens revoked). // Remove the refresh token and suppress the exception, allowing the user to be taken through the // authorization_code flow again. $DB->delete_records('oauth2_refresh_token', ['id' => $refreshtoken->id]); } } return false; } /** * Whether this client should automatically exchange a refresh token for an access token as part of login checks. * * @return bool true if supported, false otherwise. */ protected function can_autorefresh(): bool { global $USER; // Auto refresh is only supported when the follow criteria are met: // a) The client is not a system client. The exchange process for system client refresh tokens is handled // externally, via a call to client->upgrade_refresh_token(). // b) The user is authenticated. // c) The client has been configured with autorefresh enabled. return !$this->system && ($this->autorefresh && !empty($USER->id)); } /** * Store the user's refresh token for later use. * * @param stdClass $token a refresh token. */ protected function store_user_refresh_token(stdClass $token): void { global $DB, $USER; $id = $DB->get_field('oauth2_refresh_token', 'id', ['userid' => $USER->id, 'scopehash' => sha1($token->scope), 'issuerid' => $this->issuer->get('id')]); $time = time(); if ($id) { $record = [ 'id' => $id, 'timemodified' => $time, 'token' => $token->token ]; $DB->update_record('oauth2_refresh_token', $record); } else { $record = [ 'timecreated' => $time, 'timemodified' => $time, 'userid' => $USER->id, 'issuerid' => $this->issuer->get('id'), 'token' => $token->token, 'scopehash' => sha1($token->scope) ]; $DB->insert_record('oauth2_refresh_token', $record); } } /** * Attempt to exchange a refresh token for a new access token. * * If successful, will return an array of token objects in the form: * Array * ( * [access_token] => stdClass object * ( * [token] => 'the_token_string' * [expires] => 123456789 * [scope] => 'openid files etc' * ) * [refresh_token] => stdClass object * ( * [token] => 'the_refresh_token_string' * [scope] => 'openid files etc' * ) * ) * where the 'refresh_token' will only be provided if supplied by the auth server in the response. * * @param string $refreshtoken the refresh token to exchange. * @return null|array array containing access token and refresh token if provided, null if the exchange was denied. * @throws moodle_exception if an invalid response is received or if the response contains errors. */ protected function exchange_refresh_token(string $refreshtoken): ?array { $params = array('refresh_token' => $refreshtoken, 'grant_type' => 'refresh_token' ); if ($this->basicauth) { $idsecret = urlencode($this->issuer->get('clientid')) . ':' . urlencode($this->issuer->get('clientsecret')); $this->setHeader('Authorization: Basic ' . base64_encode($idsecret)); } else { $params['client_id'] = $this->issuer->get('clientid'); $params['client_secret'] = $this->issuer->get('clientsecret'); } // Requests can either use http GET or POST. if ($this->use_http_get()) { $response = $this->get($this->token_url(), $params); } else { $response = $this->post($this->token_url(), $this->build_post_data($params)); } if ($this->info['http_code'] !== 200) { $debuginfo = !empty($this->error) ? $this->error : $response; throw new moodle_exception('oauth2refreshtokenerror', 'core_error', '', $this->info['http_code'], $debuginfo); } $r = json_decode($response); if (!empty($r->error)) { throw new moodle_exception($r->error . ' ' . $r->error_description); } if (!isset($r->access_token)) { return null; } // Store the token an expiry time. $accesstoken = new stdClass(); $accesstoken->token = $r->access_token; if (isset($r->expires_in)) { // Expires 10 seconds before actual expiry. $accesstoken->expires = (time() + ($r->expires_in - 10)); } $accesstoken->scope = $this->scope; $tokens = ['access_token' => $accesstoken]; if (isset($r->refresh_token)) { $this->refreshtoken = $r->refresh_token; $newrefreshtoken = new stdClass(); $newrefreshtoken->token = $this->refreshtoken; $newrefreshtoken->scope = $this->scope; $tokens['refresh_token'] = $newrefreshtoken; } return $tokens; } /** * Override which, in addition to deleting access tokens, also deletes any stored refresh token. */ public function log_out() { global $DB, $USER; parent::log_out(); if (!$this->can_autorefresh()) { return; } // For clients supporting autorefresh, delete the stored refresh token too. $issuerid = $this->issuer->get('id'); $refreshtoken = $DB->get_record('oauth2_refresh_token', ['userid' => $USER->id, 'issuerid' => $issuerid, 'scopehash' => sha1($this->scope)]); if ($refreshtoken) { $DB->delete_records('oauth2_refresh_token', ['id' => $refreshtoken->id]); } } /** * Upgrade a refresh token from oauth 2.0 to an access token, for system clients only. * * @param \core\oauth2\system_account $systemaccount * @return boolean true if token is upgraded succesfully */ public function upgrade_refresh_token(system_account $systemaccount) { $receivedtokens = $this->exchange_refresh_token($systemaccount->get('refreshtoken')); // No access token received, so return false. if (empty($receivedtokens)) { return false; } // Store the access token and, if provided by the server, the new refresh token. $this->store_token($receivedtokens['access_token']); if (isset($receivedtokens['refresh_token'])) { $systemaccount->set('refreshtoken', $receivedtokens['refresh_token']->token); $systemaccount->update(); } return true; } /** * Fetch the user info from the user info endpoint. * * @return stdClass|false Moodle user fields for the logged in user (or false if request failed) * @throws moodle_exception if the response is empty after decoding it. */ public function get_raw_userinfo() { if (!empty($this->rawuserinfo)) { return $this->rawuserinfo; } $url = $this->get_issuer()->get_endpoint_url('userinfo'); if (empty($url)) { return false; } $response = $this->get($url); if (!$response) { return false; } $userinfo = new stdClass(); try { $userinfo = json_decode($response); } catch (\Exception $e) { return false; } if (is_null($userinfo)) { // Throw an exception displaying the original response, because, at this point, $userinfo shouldn't be empty. throw new moodle_exception($response); } $this->rawuserinfo = $userinfo; return $userinfo; } /** * Fetch the user info from the user info endpoint and map all * the fields back into moodle fields. * * @return stdClass|false Moodle user fields for the logged in user (or false if request failed) * @throws moodle_exception if the response is empty after decoding it. */ public function get_userinfo() { $userinfo = $this->get_raw_userinfo(); if ($userinfo === false) { return false; } return $this->map_userinfo_to_fields($userinfo); } /** * Maps the oauth2 response to userfields. * * @param stdClass $userinfo * @return array */ protected function map_userinfo_to_fields(stdClass $userinfo): array { $map = $this->get_userinfo_mapping(); $user = new stdClass(); foreach ($map as $openidproperty => $moodleproperty) { // We support nested objects via a-b-c syntax. $getfunc = function($obj, $prop) use (&$getfunc) { $proplist = explode('-', $prop, 2); // The value of proplist[0] can be falsey, so just check if not set. if (empty($obj) || !isset($proplist[0])) { return false; } if (preg_match('/^(.*)\[([0-9]*)\]$/', $proplist[0], $matches) && count($matches) == 3) { $property = $matches[1]; $index = $matches[2]; $obj = $obj->{$property}[$index] ?? null; } else if (!empty($obj->{$proplist[0]})) { $obj = $obj->{$proplist[0]}; } else if (is_array($obj) && !empty($obj[$proplist[0]])) { $obj = $obj[$proplist[0]]; } else { // Nothing found after checking all possible valid combinations, return false. return false; } if (count($proplist) > 1) { return $getfunc($obj, $proplist[1]); } return $obj; }; $resolved = $getfunc($userinfo, $openidproperty); if (!empty($resolved)) { $user->$moodleproperty = $resolved; } } if (empty($user->username) && !empty($user->email)) { $user->username = $user->email; } if (!empty($user->picture)) { $user->picture = download_file_content($user->picture, null, null, false, 10, 10, true, null, false); } else { $pictureurl = $this->issuer->get_endpoint_url('userpicture'); if (!empty($pictureurl)) { $user->picture = $this->get($pictureurl); } } if (!empty($user->picture)) { // If it doesn't look like a picture lets unset it. if (function_exists('imagecreatefromstring')) { $img = @imagecreatefromstring($user->picture); if (empty($img)) { unset($user->picture); } else { imagedestroy($img); } } } return (array)$user; } } issuer.php 0000644 00000020003 15151222307 0006560 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing issuers from the DB. * * @package core * @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; use lang_string; /** * 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 { /** @var int Issuer is displayed on both login page and in the services lists */ const EVERYWHERE = 1; /** @var int Issuer is displayed on the login page only */ const LOGINONLY = 2; /** @var int Issuer is displayed only in the services lists and can not be used for login */ const SERVICEONLY = 0; const TABLE = 'oauth2_issuer'; /** * 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_TRIMMED, 'default' => '' ), 'clientsecret' => array( 'type' => PARAM_RAW_TRIMMED, 'default' => '' ), 'baseurl' => array( 'type' => PARAM_URL, 'default' => '' ), 'enabled' => array( 'type' => PARAM_BOOL, 'default' => true ), 'showonloginpage' => array( 'type' => PARAM_INT, 'default' => self::SERVICEONLY, ), 'basicauth' => array( 'type' => PARAM_BOOL, 'default' => false ), 'scopessupported' => array( 'type' => PARAM_RAW, 'null' => NULL_ALLOWED, 'default' => null ), 'loginscopes' => array( 'type' => PARAM_RAW, 'default' => 'openid profile email' ), 'loginscopesoffline' => array( 'type' => PARAM_RAW, 'default' => 'openid profile email' ), 'loginparams' => array( 'type' => PARAM_RAW, 'default' => '' ), 'loginparamsoffline' => array( 'type' => PARAM_RAW, 'default' => '' ), 'alloweddomains' => array( 'type' => PARAM_RAW, 'default' => '' ), 'sortorder' => array( 'type' => PARAM_INT, 'default' => 0, ), 'requireconfirmation' => array( 'type' => PARAM_BOOL, 'default' => true ), 'servicetype' => array( 'type' => PARAM_ALPHANUM, 'null' => NULL_ALLOWED, 'default' => null, ), 'loginpagename' => array( 'type' => PARAM_TEXT, 'null' => NULL_ALLOWED, 'default' => null, ), ); } /** * Hook to execute before validate. * * @return void */ protected function before_validate() { if (($this->get('id') && $this->get('sortorder') === null) || !$this->get('id')) { $this->set('sortorder', $this->count_records()); } } /** * Helper the get a named service endpoint. * @param string $type * @return string|false */ 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; } /** * Perform matching against the list of allowed login domains for this issuer. * * @param string $email The email to check. * @return boolean */ public function is_valid_login_domain($email) { if (empty($this->get('alloweddomains'))) { return true; } $validdomains = explode(',', $this->get('alloweddomains')); $parts = explode('@', $email, 2); $emaildomain = ''; if (count($parts) > 1) { $emaildomain = $parts[1]; } return \core\ip_utils::is_domain_in_allowed_list($emaildomain, $validdomains); } /** * Does this OAuth service support user authentication? * @return boolean */ public function is_authentication_supported() { debugging('Method is_authentication_supported() is deprecated, please use is_available_for_login()', DEBUG_DEVELOPER); return (!empty($this->get_endpoint_url('userinfo'))); } /** * Is this issue fully configured and enabled and can be used for login/signup * * @return bool * @throws \coding_exception */ public function is_available_for_login(): bool { return $this->get('id') && $this->is_configured() && $this->get('showonloginpage') != self::SERVICEONLY && $this->get('enabled') && !empty($this->get_endpoint_url('userinfo')); } /** * Return true if this issuer looks like it has been configured. * * @return boolean */ public function is_configured() { return (!empty($this->get('clientid')) && !empty($this->get('clientsecret'))); } /** * Do we have a refresh token for a system account? * @return boolean */ public function is_system_account_connected() { if (!$this->is_configured()) { return false; } $sys = system_account::get_record(['issuerid' => $this->get('id')]); if (empty($sys) || empty($sys->get('refreshtoken'))) { return false; } $scopes = api::get_system_scopes_for_issuer($this); $grantedscopes = $sys->get('grantedscopes'); $scopes = explode(' ', $scopes); foreach ($scopes as $scope) { if (!empty($scope)) { if (strpos(' ' . $grantedscopes . ' ', ' ' . $scope . ' ') === false) { // We have not been granted all the scopes that are required. return false; } } } return true; } /** * Custom validator for end point URLs. * Because we send Bearer tokens we must ensure SSL. * * @param string $value The value to check. * @return lang_string|boolean */ protected function validate_baseurl($value) { global $CFG; include_once($CFG->dirroot . '/lib/validateurlsyntax.php'); if (!empty($value) && !validateUrlSyntax($value, 'S+')) { return new lang_string('sslonlyaccess', 'error'); } return true; } /** * Display name for the issuers used on the login page * * @return string */ public function get_display_name(): string { return $this->get('loginpagename') ? $this->get('loginpagename') : $this->get('name'); } } api.php 0000644 00000052311 15151222307 0006026 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing oauth2 endpoints from the DB. * * @package core * @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 . '/filelib.php'); use stdClass; use moodle_url; use context_system; use moodle_exception; /** * 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 { /** * Initializes a record for one of the standard issuers to be displayed in the settings. * The issuer is not yet created in the database. * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 * @return \core\oauth2\issuer */ public static function init_standard_issuer($type) { require_capability('moodle/site:config', context_system::instance()); $classname = self::get_service_classname($type); if (class_exists($classname)) { return $classname::init(); } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * Create endpoints for standard issuers, based on the issuer created from submitted data. * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1 * @param issuer $issuer issuer the endpoints should be created for. * @return \core\oauth2\issuer */ public static function create_endpoints_for_standard_issuer($type, $issuer) { require_capability('moodle/site:config', context_system::instance()); $classname = self::get_service_classname($type); if (class_exists($classname)) { $classname::create_endpoints($issuer); return $issuer; } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * Create one of the standard issuers. * * @param string $type One of google, facebook, microsoft, nextcloud or imsobv2p1 * @param string|false $baseurl Baseurl (only required for nextcloud and imsobv2p1) * @return \core\oauth2\issuer */ public static function create_standard_issuer($type, $baseurl = false) { require_capability('moodle/site:config', context_system::instance()); switch ($type) { case 'imsobv2p1': if (!$baseurl) { throw new moodle_exception('IMS OBv2.1 service type requires the baseurl parameter.'); } case 'nextcloud': if (!$baseurl) { throw new moodle_exception('Nextcloud service type requires the baseurl parameter.'); } case 'google': case 'facebook': case 'microsoft': $classname = self::get_service_classname($type); $issuer = $classname::init(); if ($baseurl) { $issuer->set('baseurl', $baseurl); } $issuer->create(); return self::create_endpoints_for_standard_issuer($type, $issuer); } throw new moodle_exception('OAuth 2 service type not recognised: ' . $type); } /** * List all the issuers, ordered by the sortorder field * * @param bool $includeloginonly also include issuers that are configured to be shown only on login page, * By default false, in this case the method returns all issuers that can be used in services * @return \core\oauth2\issuer[] */ public static function get_all_issuers(bool $includeloginonly = false) { if ($includeloginonly) { return issuer::get_records([], 'sortorder'); } else { return array_values(issuer::get_records_select('showonloginpage<>?', [issuer::LOGINONLY], 'sortorder')); } } /** * Get a single issuer by id. * * @param int $id * @return \core\oauth2\issuer */ public static function get_issuer($id) { return new issuer($id); } /** * Get a single endpoint by id. * * @param int $id * @return \core\oauth2\endpoint */ public static function get_endpoint($id) { return new endpoint($id); } /** * Get a single user field mapping by id. * * @param int $id * @return \core\oauth2\user_field_mapping */ public static function get_user_field_mapping($id) { return new user_field_mapping($id); } /** * Get the system account for an installed OAuth service. * Never ever ever expose this to a webservice because it contains the refresh token which grants API access. * * @param \core\oauth2\issuer $issuer * @return system_account|false */ public static function get_system_account(issuer $issuer) { return system_account::get_record(['issuerid' => $issuer->get('id')]); } /** * Get the full list of system scopes required by an oauth issuer. * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins. * * @param \core\oauth2\issuer $issuer * @return string */ public static function get_system_scopes_for_issuer($issuer) { $scopes = $issuer->get('loginscopesoffline'); $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php'); foreach ($pluginsfunction as $plugintype => $plugins) { foreach ($plugins as $pluginfunction) { // Get additional scopes from the plugin. $pluginscopes = $pluginfunction($issuer); if (empty($pluginscopes)) { continue; } // Merge the additional scopes with the existing ones. $additionalscopes = explode(' ', $pluginscopes); foreach ($additionalscopes as $scope) { if (!empty($scope)) { if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) { $scopes .= ' ' . $scope; } } } } } return $scopes; } /** * Get an authenticated oauth2 client using the system account. * This call uses the refresh token to get an access token. * * @param \core\oauth2\issuer $issuer * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded) * @throws moodle_exception Request for token upgrade failed for technical reasons */ public static function get_system_oauth_client(issuer $issuer) { $systemaccount = self::get_system_account($issuer); if (empty($systemaccount)) { return false; } // Get all the scopes! $scopes = self::get_system_scopes_for_issuer($issuer); $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, null, $scopes, true); if (!$client->is_logged_in()) { if (!$client->upgrade_refresh_token($systemaccount)) { return false; } } return $client; } /** * Get an authenticated oauth2 client using the current user account. * This call does the redirect dance back to the current page after authentication. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @param moodle_url $currenturl The url to the current page. * @param string $additionalscopes The additional scopes required for authorization. * @param bool $autorefresh Should the client support the use of refresh tokens to persist access across sessions. * @return \core\oauth2\client */ public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '', $autorefresh = false) { $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh); return $client; } /** * Get the client classname for an issuer. * * @param string $type The OAuth issuer type (google, facebook...). * @return string The classname for the custom client or core client class if the class for the defined type * doesn't exist or null type is defined. */ protected static function get_client_classname(?string $type): string { // Default core client class. $classname = 'core\\oauth2\\client'; if (!empty($type)) { $typeclassname = 'core\\oauth2\\client\\' . $type; if (class_exists($typeclassname)) { $classname = $typeclassname; } } return $classname; } /** * Get the list of defined endpoints for this OAuth issuer * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @return \core\oauth2\endpoint[] */ public static function get_endpoints(issuer $issuer) { return endpoint::get_records(['issuerid' => $issuer->get('id')]); } /** * Get the list of defined mapping from OAuth user fields to moodle user fields. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer * @return \core\oauth2\user_field_mapping[] */ public static function get_user_field_mappings(issuer $issuer) { return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]); } /** * Guess an image from the discovery URL. * * @param \core\oauth2\issuer $issuer The desired OAuth issuer */ protected static function guess_image($issuer) { if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) { $baseurl = parse_url($issuer->get('baseurl')); $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico'; $issuer->set('image', $imageurl); $issuer->update(); } } /** * Take the data from the mform and update the issuer. * * @param stdClass $data * @return \core\oauth2\issuer */ public static function update_issuer($data) { return self::create_or_update_issuer($data, false); } /** * Take the data from the mform and create the issuer. * * @param stdClass $data * @return \core\oauth2\issuer */ public static function create_issuer($data) { return self::create_or_update_issuer($data, true); } /** * Take the data from the mform and create or update the issuer. * * @param stdClass $data Form data for them issuer to be created/updated. * @param bool $create If true, the issuer will be created; otherwise, it will be updated. * @return issuer The created/updated issuer. */ protected static function create_or_update_issuer($data, bool $create): issuer { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($data->id ?? 0, $data); // Will throw exceptions on validation failures. if ($create) { $issuer->create(); // Perform service discovery. $classname = self::get_service_classname($issuer->get('servicetype')); $classname::discover_endpoints($issuer); self::guess_image($issuer); } else { $issuer->update(); } return $issuer; } /** * Get the service classname for an issuer. * * @param string $type The OAuth issuer type (google, facebook...). * * @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist * or null type is defined. */ protected static function get_service_classname(?string $type): string { // Default custom service class. $classname = 'core\\oauth2\\service\\custom'; if (!empty($type)) { $typeclassname = 'core\\oauth2\\service\\' . $type; if (class_exists($typeclassname)) { $classname = $typeclassname; } } return $classname; } /** * Take the data from the mform and update the endpoint. * * @param stdClass $data * @return \core\oauth2\endpoint */ public static function update_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); // Will throw exceptions on validation failures. $endpoint->update(); return $endpoint; } /** * Take the data from the mform and create the endpoint. * * @param stdClass $data * @return \core\oauth2\endpoint */ public static function create_endpoint($data) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint(0, $data); // Will throw exceptions on validation failures. $endpoint->create(); return $endpoint; } /** * Take the data from the mform and update the user field mapping. * * @param stdClass $data * @return \core\oauth2\user_field_mapping */ public static function update_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); // Will throw exceptions on validation failures. $userfieldmapping->update(); return $userfieldmapping; } /** * Take the data from the mform and create the user field mapping. * * @param stdClass $data * @return \core\oauth2\user_field_mapping */ public static function create_user_field_mapping($data) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping(0, $data); // Will throw exceptions on validation failures. $userfieldmapping->create(); return $userfieldmapping; } /** * 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; } /** * 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_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; } /** * Disable an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to disable. * @return boolean */ public static function disable_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); $issuer->set('enabled', 0); return $issuer->update(); } /** * Enable an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to enable. * @return boolean */ public static function enable_issuer($id) { require_capability('moodle/site:config', context_system::instance()); $issuer = new issuer($id); $issuer->set('enabled', 1); return $issuer->update(); } /** * Delete an identity issuer. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the identity issuer to delete. * @return boolean */ 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. return $issuer->delete(); } /** * Delete an endpoint. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the endpoint to delete. * @return boolean */ public static function delete_endpoint($id) { require_capability('moodle/site:config', context_system::instance()); $endpoint = new endpoint($id); // Will throw exceptions on validation failures. return $endpoint->delete(); } /** * Delete a user_field_mapping. * * Requires moodle/site:config capability at the system context. * * @param int $id The id of the user_field_mapping to delete. * @return boolean */ public static function delete_user_field_mapping($id) { require_capability('moodle/site:config', context_system::instance()); $userfieldmapping = new user_field_mapping($id); // Will throw exceptions on validation failures. return $userfieldmapping->delete(); } /** * Perform the OAuth dance and get a refresh token. * * Requires moodle/site:config capability at the system context. * * @param \core\oauth2\issuer $issuer * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication). * @return boolean */ 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. $scopes = self::get_system_scopes_for_issuer($issuer); // Allow callbacks to inject non-standard scopes to the auth request. $class = self::get_client_classname($issuer->get('servicetype')); $client = new $class($issuer, $returnurl, $scopes, true); if (!optional_param('response', false, PARAM_BOOL)) { $client->log_out(); } if (optional_param('error', '', PARAM_RAW)) { return false; } 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(); } $userinfo = $client->get_userinfo(); $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->refreshtoken = $refreshtoken; $record->grantedscopes = $scopes; $record->email = isset($userinfo['email']) ? $userinfo['email'] : ''; $record->username = $userinfo['username']; $systemaccount = new system_account(0, $record); $systemaccount->create(); $client->log_out(); return true; } } endpoint.php 0000644 00000004047 15151222307 0007100 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Class for loading/storing oauth2 endpoints from the DB. * * @package core * @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; use lang_string; /** * 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_ALPHANUMEXT, ), 'url' => array( 'type' => PARAM_URL, ) ); } /** * Custom validator for end point URLs. * Because we send Bearer tokens we must ensure SSL. * * @param string $value The value to check. * @return lang_string|boolean */ protected function validate_url($value) { if (strpos($value, 'https://') !== 0) { return new lang_string('sslonlyaccess', 'error'); } return true; } } system_account.php 0000644 00000003566 15151222307 0010325 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * When using OAuth sometimes it makes sense to authenticate as a system user, and not the current user. * * @package core * @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, ), 'email' => array( 'type' => PARAM_RAW, ), 'username' => array( 'type' => PARAM_RAW, ) ); } } service/facebook.php 0000644 00000007251 15151222307 0010471 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; use core\oauth2\discovery\openidconnect; /** * Class for Facebook oAuth service, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class facebook extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer The issuer initialised with proper default values. */ public static function init(): issuer { $record = (object) [ 'name' => 'Facebook', 'image' => 'https://facebookbrand.com/wp-content/uploads/2016/05/flogo_rgb_hex-brc-site-250.png', 'baseurl' => '', 'loginscopes' => 'public_profile email', 'loginscopesoffline' => 'public_profile email', 'showonloginpage' => issuer::EVERYWHERE, 'servicetype' => 'facebook', ]; $issuer = new issuer(0, $record); return $issuer; } /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { // The Facebook API version. $apiversion = '2.12'; // The Graph API URL. $graphurl = 'https://graph.facebook.com/v' . $apiversion; // User information fields that we want to fetch. $infofields = [ 'id', 'first_name', 'last_name', 'picture.type(large)', 'name', 'email', ]; $endpoints = [ 'authorization_endpoint' => sprintf('https://www.facebook.com/v%s/dialog/oauth', $apiversion), 'token_endpoint' => $graphurl . '/oauth/access_token', 'userinfo_endpoint' => $graphurl . '/me?fields=' . implode(',', $infofields) ]; foreach ($endpoints as $name => $url) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => $name, 'url' => $url ]; $endpoint = new endpoint(0, $record); $endpoint->create(); } // Create the field mappings. $mapping = [ 'name' => 'alternatename', 'last_name' => 'lastname', 'email' => 'email', 'first_name' => 'firstname', 'picture-data-url' => 'picture', ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } return $issuer; } } service/imsobv2p1.php 0000644 00000003522 15151222307 0010537 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\discovery\imsbadgeconnect; /** * Class for IMS Open Badges v2.1 oAuth service, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class imsobv2p1 extends imsbadgeconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer|null The issuer initialised with proper default values. */ public static function init(): ?issuer { $record = (object) [ 'name' => 'Open Badges', 'image' => '', 'servicetype' => 'imsobv2p1', ]; $issuer = new issuer(0, $record); return $issuer; } /** * Process how to map user field information. * * @param issuer $issuer The OAuth issuer the endpoints should be discovered for. * @return void */ public static function create_field_mappings(issuer $issuer): void { // There are no specific field mappings for this service. } } service/issuer_interface.php 0000644 00000003376 15151222307 0012256 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; /** * Interface for services, with the methods to be implemented by all the issuer implementing it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer|null The issuer initialised with proper default values, or null if no issuer is initialised. */ public static function init(): ?issuer; /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer; /** * If the discovery endpoint exists for this issuer, try and determine the list of valid endpoints. * * @param issuer $issuer * @return int The number of discovered services. */ public static function discover_endpoints($issuer): int; } service/nextcloud.php 0000644 00000006660 15151222307 0010730 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; use core\oauth2\discovery\openidconnect; /** * Class for Nextcloud oAuth service, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class nextcloud extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer The issuer initialised with proper default values. */ public static function init(): issuer { $record = (object) [ 'name' => 'Nextcloud', 'image' => 'https://nextcloud.com/wp-content/uploads/2022/03/favicon.png', 'basicauth' => 1, 'servicetype' => 'nextcloud', ]; $issuer = new issuer(0, $record); return $issuer; } /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { // Nextcloud has a custom baseurl. Thus, the creation of endpoints has to be done later. $baseurl = $issuer->get('baseurl'); // Add trailing slash to baseurl, if needed. if (substr($baseurl, -1) !== '/') { $baseurl .= '/'; } $endpoints = [ // Baseurl will be prepended later. 'authorization_endpoint' => 'index.php/apps/oauth2/authorize', 'token_endpoint' => 'index.php/apps/oauth2/api/v1/token', 'userinfo_endpoint' => 'ocs/v2.php/cloud/user?format=json', 'webdav_endpoint' => 'remote.php/webdav/', 'ocs_endpoint' => 'ocs/v1.php/apps/files_sharing/api/v1/shares', ]; foreach ($endpoints as $name => $url) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => $name, 'url' => $baseurl . $url, ]; $endpoint = new \core\oauth2\endpoint(0, $record); $endpoint->create(); } // Create the field mappings. $mapping = [ 'ocs-data-email' => 'email', 'ocs-data-id' => 'username', ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new \core\oauth2\user_field_mapping(0, $record); $userfieldmapping->create(); } return $issuer; } } service/google.php 0000644 00000003317 15151222307 0010173 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\discovery\openidconnect; /** * Class for Google oAuth service, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class google extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer|null The issuer initialised with proper default values. */ public static function init(): ?issuer { $record = (object) [ 'name' => 'Google', 'image' => 'https://accounts.google.com/favicon.ico', 'baseurl' => 'https://accounts.google.com/', 'loginparamsoffline' => 'access_type=offline&prompt=consent', 'showonloginpage' => issuer::EVERYWHERE, 'servicetype' => 'google', ]; $issuer = new issuer(0, $record); return $issuer; } } service/custom.php 0000644 00000002574 15151222307 0010235 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\discovery\openidconnect; /** * Class for Custom services, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class custom extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer|null The issuer initialised with proper default values. */ public static function init(): ?issuer { // Custom service doesn't require any particular initialization. return null; } } service/clever.php 0000644 00000006117 15151222307 0010200 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\discovery\openidconnect; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; /** * Class for Clever OAuth service, with the specific methods related to it. * * @package core * @copyright 2022 OpenStax * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class clever extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer The issuer initialised with proper default values. */ public static function init(): issuer { $record = (object) [ 'name' => 'Clever', 'image' => 'https://apps.clever.com/favicon.ico', 'basicauth' => 1, 'baseurl' => '', 'showonloginpage' => issuer::LOGINONLY, 'servicetype' => 'clever', ]; return new issuer(0, $record); } /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { $endpoints = [ 'authorization_endpoint' => 'https://clever.com/oauth/authorize', 'token_endpoint' => 'https://clever.com/oauth/tokens', 'userinfo_endpoint' => 'https://api.clever.com/v3.0/me', 'userdata_endpoint' => 'https://api.clever.com/v3.0/users' ]; foreach ($endpoints as $name => $url) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => $name, 'url' => $url ]; $endpoint = new endpoint(0, $record); $endpoint->create(); } // Create the field mappings. $mapping = [ 'data-id' => 'idnumber', 'data-name-first' => 'firstname', 'data-name-last' => 'lastname', 'data-email' => 'email' ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } return $issuer; } } service/microsoft.php 0000644 00000006725 15151222307 0010732 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; use core\oauth2\discovery\openidconnect; /** * Class for Microsoft oAuth service, with the specific methods related to it. * * @package core * @copyright 2021 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class microsoft extends openidconnect implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer The issuer initialised with proper default values. */ public static function init(): issuer { $record = (object) [ 'name' => 'Microsoft', 'image' => 'https://www.microsoft.com/favicon.ico', 'baseurl' => '', 'loginscopes' => 'openid profile email user.read', 'loginscopesoffline' => 'openid profile email user.read offline_access', 'showonloginpage' => issuer::EVERYWHERE, 'servicetype' => 'microsoft', ]; $issuer = new issuer(0, $record); return $issuer; } /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { $endpoints = [ 'authorization_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 'token_endpoint' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token', 'userinfo_endpoint' => 'https://graph.microsoft.com/v1.0/me/', 'userpicture_endpoint' => 'https://graph.microsoft.com/v1.0/me/photo/$value', ]; foreach ($endpoints as $name => $url) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => $name, 'url' => $url ]; $endpoint = new endpoint(0, $record); $endpoint->create(); } // Create the field mappings. $mapping = [ 'givenName' => 'firstname', 'surname' => 'lastname', 'userPrincipalName' => 'email', 'displayName' => 'alternatename', 'officeLocation' => 'address', 'mobilePhone' => 'phone1', 'preferredLanguage' => 'lang' ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } return $issuer; } } service/linkedin.php 0000644 00000007544 15151222307 0010522 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. namespace core\oauth2\service; use core\oauth2\issuer; use core\oauth2\endpoint; use core\oauth2\user_field_mapping; /** * Class linkedin. * * Custom oauth2 issuer for linkedin as it doesn't support OIDC and has a different way to get * key information for users - firstname, lastname, email. * * @copyright 2021 Peter Dias * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @package core */ class linkedin implements issuer_interface { /** * Build an OAuth2 issuer, with all the default values for this service. * * @return issuer The issuer initialised with proper default values. */ public static function init(): issuer { $record = (object) [ 'name' => 'LinkedIn', 'image' => 'https://static.licdn.com/scds/common/u/images/logos/favicons/v1/favicon.ico', 'baseurl' => 'https://api.linkedin.com/v2', 'loginscopes' => 'r_liteprofile r_emailaddress', 'loginscopesoffline' => 'r_liteprofile r_emailaddress', 'showonloginpage' => issuer::EVERYWHERE, 'servicetype' => 'linkedin', ]; $issuer = new issuer(0, $record); return $issuer; } /** * Create endpoints for this issuer. * * @param issuer $issuer Issuer the endpoints should be created for. * @return issuer */ public static function create_endpoints(issuer $issuer): issuer { $endpoints = [ 'authorization_endpoint' => 'https://www.linkedin.com/oauth/v2/authorization', 'token_endpoint' => 'https://www.linkedin.com/oauth/v2/accessToken', 'email_endpoint' => 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', 'userinfo_endpoint' => "https://api.linkedin.com/v2/me?projection=(localizedFirstName,localizedLastName," . "profilePicture(displayImage~digitalmediaAsset:playableStreams))", ]; foreach ($endpoints as $name => $url) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'name' => $name, 'url' => $url ]; $endpoint = new endpoint(0, $record); $endpoint->create(); } // Create the field mappings. $mapping = [ 'localizedFirstName' => 'firstname', 'localizedLastName' => 'lastname', 'elements[0]-handle~-emailAddress' => 'email', 'profilePicture-displayImage~-elements[0]-identifiers[0]-identifier' => 'picture' ]; foreach ($mapping as $external => $internal) { $record = (object) [ 'issuerid' => $issuer->get('id'), 'externalfield' => $external, 'internalfield' => $internal ]; $userfieldmapping = new user_field_mapping(0, $record); $userfieldmapping->create(); } return $issuer; } /** * Linkedin does not have a discovery url that could be found. Return empty. * @param issuer $issuer * @return int */ public static function discover_endpoints($issuer): int { return 0; } } refresh_system_tokens_task.php 0000644 00000007420 15151222307 0012725 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * A scheduled task. * * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; use \core\task\scheduled_task; use core_user; use moodle_exception; defined('MOODLE_INTERNAL') || die(); /** * Task to refresh system tokens regularly. Admins are notified in case an authorisation expires. * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class refresh_system_tokens_task extends scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('taskrefreshsystemtokens', 'admin'); } /** * Notify admins when an OAuth refresh token expires. Should not happen if cron is running regularly. * @param \core\oauth2\issuer $issuer */ protected function notify_admins(\core\oauth2\issuer $issuer) { global $CFG; $admins = get_admins(); if (empty($admins)) { return; } foreach ($admins as $admin) { $strparams = ['siteurl' => $CFG->wwwroot, 'issuer' => $issuer->get('name')]; $long = get_string('oauthrefreshtokenexpired', 'core_admin', $strparams); $short = get_string('oauthrefreshtokenexpiredshort', 'core_admin', $strparams); $message = new \core\message\message(); $message->courseid = SITEID; $message->component = 'moodle'; $message->name = 'errors'; $message->userfrom = core_user::get_noreply_user(); $message->userto = $admin; $message->subject = $short; $message->fullmessage = $long; $message->fullmessageformat = FORMAT_PLAIN; $message->fullmessagehtml = $long; $message->smallmessage = $short; $message->notification = 1; message_send($message); } } /** * Do the job. * Throw exceptions on errors (the job will be retried). */ public function execute() { $issuers = \core\oauth2\api::get_all_issuers(true); $tasksuccess = true; foreach ($issuers as $issuer) { if ($issuer->is_system_account_connected()) { try { // Try to get an authenticated client; renew token if necessary. // Returns false or throws a moodle_exception on error. $success = \core\oauth2\api::get_system_oauth_client($issuer); } catch (moodle_exception $e) { mtrace($e->getMessage()); $success = false; } if ($success === false) { $this->notify_admins($issuer); $tasksuccess = false; } } } if (!$tasksuccess) { throw new moodle_exception('oauth2refreshtokentaskerror', 'core_error'); } } } rest_exception.php 0000644 00000002314 15151222307 0010306 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Rest Exception class containing error code and message. * * @package core * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; use Exception; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/filelib.php'); /** * Rest Exception class containing error code and message. * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class rest_exception extends Exception { } access_token.php 0000644 00000005351 15151222307 0007720 0 ustar 00 <?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * Loads/stores oauth2 access tokens in DB for system accounts in order to use a single token across multiple sessions. * * @package core * @copyright 2018 Jan Dageförde <jan.dagefoerde@ercis.uni-muenster.de> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core\oauth2; defined('MOODLE_INTERNAL') || die(); use core\persistent; /** * Loads/stores oauth2 access tokens in DB for system accounts in order to use a single token across multiple sessions. * * When a system user is authenticated via OAuth, we need to use a single access token across user sessions, * because we want to avoid using multiple tokens at the same time for a single remote user. Reasons are that, * first, redeeming the refresh token for an access token requires an additional request, and second, there is * no guarantee that redeeming the refresh token doesn't invalidate *all* corresponding previous access tokes. * As a result, we would need to either continuously request lots and lots of new access tokens, or persist the * access token in the DB where it can be used from all sessions. Let's do the latter! * * @copyright 2018 Jan Dageförde <jan.dagefoerde@ercis.uni-muenster.de> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class access_token extends persistent { /** The table name. */ const TABLE = 'oauth2_access_token'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( // Issuer id instead of the system account id because, at the time of storing/loading a token we may not // know the system account id. 'issuerid' => array( 'type' => PARAM_INT ), 'token' => array( 'type' => PARAM_RAW, ), 'expires' => array( 'type' => PARAM_INT, ), 'scope' => array( 'type' => PARAM_RAW, ), ); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | ���֧ߧ֧�ѧ�ڧ� ����ѧߧڧ��: 0 |
proxy
|
phpinfo
|
���ѧ����ۧܧ�