first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
<?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,
),
);
}
}
+654
View File
@@ -0,0 +1,654 @@
<?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, MoodleNet, nextcloud or imsobv2p1
* @param string|false $baseurl Baseurl (only required for nextcloud, imsobv2p1 and moodlenet)
* @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 'moodlenet':
if (!$baseurl) {
throw new moodle_exception('MoodleNet 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;
}
}
+611
View File
@@ -0,0 +1,611 @@
<?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;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?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 {
/**
* Override to handle LinkedIn's non-spec-compliant 'locale' field, which isn't a string (e.g. 'en-US') but an object.
*
* @return array|false
*/
public function get_userinfo() {
$rawuserinfo = $this->get_raw_userinfo();
if ($rawuserinfo === false) {
return false;
}
if (!empty($rawuserinfo->locale) && is_object($rawuserinfo->locale)) {
if (!empty($rawuserinfo->locale->language) && !empty($rawuserinfo->locale->country)) {
$rawuserinfo->locale = "{$rawuserinfo->locale->language}-{$rawuserinfo->locale->country}";
} else {
unset($rawuserinfo->locale);
}
}
return $this->map_userinfo_to_fields($rawuserinfo);
}
}
@@ -0,0 +1,109 @@
<?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 core\http_client;
use GuzzleHttp\Exception\ClientException;
/**
* Simple reader class, allowing OAuth 2 Authorization Server Metadata to be read from an auth server's well-known.
*
* {@link https://www.rfc-editor.org/rfc/rfc8414}
*
* @package core
* @copyright 2023 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class auth_server_config_reader {
/** @var \stdClass the config object read from the discovery document. */
protected \stdClass $metadata;
/** @var \moodle_url the base URL for the auth server which was last used during a read.*/
protected \moodle_url $issuerurl;
/**
* Constructor.
*
* @param http_client $httpclient an http client instance.
* @param string $wellknownsuffix the well-known suffix, defaulting to 'oauth-authorization-server'.
*/
public function __construct(protected http_client $httpclient,
protected string $wellknownsuffix = 'oauth-authorization-server') {
}
/**
* Read the metadata from the remote host.
*
* @param \moodle_url $issuerurl the auth server issuer URL.
* @return \stdClass the configuration data object.
* @throws ClientException|\GuzzleHttp\Exception\GuzzleException if the http client experiences any problems.
*/
public function read_configuration(\moodle_url $issuerurl): \stdClass {
$this->issuerurl = $issuerurl;
$this->validate_uri();
$url = $this->get_configuration_url()->out(false);
$response = $this->httpclient->request('GET', $url);
$this->metadata = json_decode($response->getBody());
return $this->metadata;
}
/**
* Make sure the base URI is suitable for use in discovery.
*
* @return void
* @throws \moodle_exception if the URI fails validation.
*/
protected function validate_uri() {
if (!empty($this->issuerurl->get_query_string())) {
throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL cannot contain a query component.');
}
if (strtolower($this->issuerurl->get_scheme()) !== 'https') {
throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL must use HTTPS scheme.');
}
// This catches URL fragments. Since a query string is ruled out above, out_omit_querystring(false) returns only fragments.
if ($this->issuerurl->out_omit_querystring() != $this->issuerurl->out(false)) {
throw new \moodle_exception('Error: '.__METHOD__.': Auth server base URL must not contain fragments.');
}
}
/**
* Get the Auth server metadata URL.
*
* Per {@link https://www.rfc-editor.org/rfc/rfc8414#section-3}, if the issuer URL contains a path component,
* the well known suffix is added between the host and path components.
*
* @return \moodle_url the full URL to the auth server metadata.
*/
protected function get_configuration_url(): \moodle_url {
$path = $this->issuerurl->get_path();
if (!empty($path) && $path !== '/') {
// Insert the well known suffix between the host and path components.
$port = $this->issuerurl->get_port() ? ':'.$this->issuerurl->get_port() : '';
$uri = $this->issuerurl->get_scheme() . "://" . $this->issuerurl->get_host() . $port ."/".
".well-known/" . $this->wellknownsuffix . $path;
} else {
// No path, just append the well known suffix.
$uri = $this->issuerurl->out(false);
$uri .= (substr($uri, -1) == '/' ? '' : '/');
$uri .= ".well-known/$this->wellknownsuffix";
}
return new \moodle_url($uri);
}
}
@@ -0,0 +1,164 @@
<?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.
*/
abstract public 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
*/
abstract protected 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
*/
abstract protected 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
*/
abstract protected 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;
}
}
@@ -0,0 +1,183 @@
<?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();
}
} else if ($key == 'apiBase') {
(new endpoint(0, (object) [
'issuerid' => $issuer->get('id'),
'name' => $key,
'url' => $value,
]))->create();
} else if ($key == 'name') {
// Get and update issuer name.
$issuer->set('name', $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 {
// 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/moodlelogo.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);
}
}
}
}
}
@@ -0,0 +1,124 @@
<?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).
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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;
}
}
+271
View File
@@ -0,0 +1,271 @@
<?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');
}
}
@@ -0,0 +1,111 @@
<?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');
}
}
}
+134
View File
@@ -0,0 +1,134 @@
<?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' ] ]
*/
abstract public 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);
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?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 {
}
+67
View File
@@ -0,0 +1,67 @@
<?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\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' => 'https://clever.com',
'showonloginpage' => issuer::LOGINONLY,
'servicetype' => 'clever',
];
return new issuer(0, $record);
}
/**
* Create field mappings for this issuer.
*
* @param issuer $issuer Issuer the field mappings should be created for.
*/
public static function create_field_mappings(issuer $issuer): void {
// Perform OIDC default field mapping.
parent::create_field_mappings($issuer);
// Create the additional 'sub' field mapping.
$record = (object) [
'issuerid' => $issuer->get('id'),
'externalfield' => 'sub',
'internalfield' => 'idnumber',
];
$userfieldmapping = new user_field_mapping(0, $record);
$userfieldmapping->create();
}
}
+41
View File
@@ -0,0 +1,41 @@
<?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;
}
}
+109
View File
@@ -0,0 +1,109 @@
<?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;
}
}
+50
View File
@@ -0,0 +1,50 @@
<?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;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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.
}
}
@@ -0,0 +1,53 @@
<?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;
}
+60
View File
@@ -0,0 +1,60 @@
<?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\discovery\openidconnect;
use core\oauth2\issuer;
/**
* Class linkedin.
*
* OAuth 2 issuer for linkedin which is mostly OIDC compliant, with a few notable exceptions which require working around:
*
* 1. LinkedIn don't provide their OIDC discovery doc at {ISSUER}/.well-known/openid-configuration as the spec requires.
* i.e. https://www.linkedin.com/.well-known/openid-configuration isn't present.
* Instead, they make the configuration available at https://www.linkedin.com/oauth/.well-known/openid-configuration.
* See: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
*
* 2. LinkedIn don't return 'locale' as a string in the userinfo but instead return an object with 'language' and 'country' props.
* See: https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
* This is resolved in {@see \core\oauth2\client\linkedin::get_userinfo()}
*
* @copyright 2021 Peter Dias
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package core
*/
class linkedin extends openidconnect {
/**
* 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://www.linkedin.com/oauth', // The /oauth is where .well-known/openid-configuration lives.
'loginscopes' => 'openid profile email',
'loginscopesoffline' => 'openid profile email',
'showonloginpage' => issuer::EVERYWHERE,
'servicetype' => 'linkedin',
];
$issuer = new issuer(0, $record);
return $issuer;
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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;
}
}
+189
View File
@@ -0,0 +1,189 @@
<?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\http_client;
use core\oauth2\discovery\auth_server_config_reader;
use core\oauth2\endpoint;
use core\oauth2\issuer;
use GuzzleHttp\Psr7\Request;
/**
* MoodleNet OAuth 2 configuration.
*
* @package core
* @copyright 2023 Jake Dallimore <jrhdallimore@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class moodlenet implements issuer_interface {
/**
* Get the issuer template to display in the form.
*
* @return issuer the issuer.
*/
public static function init(): ?issuer {
$record = (object) [
'name' => 'MoodleNet',
'image' => 'https://moodle.net/favicon.ico',
'baseurl' => 'https://moodle.net',
'loginscopes' => '',
'loginscopesoffline' => '',
'loginparamsoffline' => '',
'showonloginpage' => issuer::SERVICEONLY,
'servicetype' => 'moodlenet',
];
$issuer = new issuer(0, $record);
return $issuer;
}
/**
* Create the endpoints for the issuer.
*
* @param issuer $issuer the issuer instance.
* @return issuer the issuer instance.
*/
public static function create_endpoints(issuer $issuer): issuer {
self::discover_endpoints($issuer);
return $issuer;
}
/**
* Read the OAuth 2 Auth Server Metadata.
*
* @param issuer $issuer the issuer instance.
* @return int the number of endpoints created.
*/
public static function discover_endpoints($issuer): int {
$baseurl = $issuer->get('baseurl');
if (empty($baseurl)) {
return 0;
}
$endpointscreated = 0;
$config = [];
if (defined('BEHAT_SITE_RUNNING')) {
$config['verify'] = false;
}
$configreader = new auth_server_config_reader(new http_client($config));
try {
$config = $configreader->read_configuration(new \moodle_url($baseurl));
foreach ($config 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();
$endpointscreated++;
}
if ($key == 'scopes_supported') {
$issuer->set('scopessupported', implode(' ', $value));
$issuer->update();
}
}
} catch (\Exception $e) {
throw new \moodle_exception('Could not read service configuration for issuer: ' . $issuer->get('name'));
}
try {
self::client_registration($issuer);
} catch (\Exception $e) {
throw new \moodle_exception('Could not register client for issuer: ' . $issuer->get('name'));
}
return $endpointscreated;
}
/**
* Perform (open) OAuth 2 Dynamic Client Registration with the MoodleNet application.
*
* @param issuer $issuer the issuer instance containing the service baseurl.
* @return void
*/
protected static function client_registration(issuer $issuer): void {
global $CFG, $SITE;
$clientid = $issuer->get('clientid');
$clientsecret = $issuer->get('clientsecret');
if (empty($clientid) && empty($clientsecret)) {
$url = $issuer->get_endpoint_url('registration');
if ($url) {
$scopes = str_replace("\r", " ", $issuer->get('scopessupported'));
$hosturl = $CFG->wwwroot;
$request = [
'client_name' => $SITE->fullname,
'client_uri' => $hosturl,
'logo_uri' => $hosturl . '/pix/moodlelogo.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
];
$config = [];
if (defined('BEHAT_SITE_RUNNING')) {
$config['verify'] = false;
}
$client = new http_client($config);
$request = new Request(
'POST',
$url,
[
'Content-type' => 'application/json',
'Accept' => 'application/json',
],
json_encode($request)
);
try {
$response = $client->send($request);
$responsebody = $response->getBody()->getContents();
$decodedbody = json_decode($responsebody, true);
if (is_null($decodedbody)) {
throw new \moodle_exception('Error: ' . __METHOD__ . ': Failed to decode response body. Invalid JSON.');
}
$issuer->set('clientid', $decodedbody['client_id']);
$issuer->set('clientsecret', $decodedbody['client_secret']);
$issuer->update();
} catch (\Exception $e) {
$msg = "Could not self-register {$issuer->get('name')}. Wrong URL or JSON data [URL: $url]";
throw new \moodle_exception($msg);
}
}
}
}
}
+100
View File
@@ -0,0 +1,100 @@
<?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;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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,
)
);
}
}
+115
View File
@@ -0,0 +1,115 @@
<?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() {
global $CFG;
require_once($CFG->dirroot . '/user/profile/lib.php');
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;
}
}