ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PKB\(rll db/access.phpnu[. /** * Capability definitions for this plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'auth/oauth2:managelinkedlogins' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => array( 'user' => CAP_ALLOW ) ), ]; PKB\OVVdb/upgrade.phpnu[. /** * OAuth2 authentication plugin upgrade code * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Upgrade function * * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_oauth2_upgrade($oldversion) { // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PKB\8db/install.xmlnu[
PKB\L db/events.phpnu[. /** * This file definies observers needed by the plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // List of observers. $observers = [ [ 'eventname' => '\core\event\user_deleted', 'callback' => '\auth_oauth2\api::user_deleted', ], ]; PKB\lang/en/auth_oauth2.phpnu[. /** * Strings for component 'auth_oauth2', language 'en'. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['accountexists'] = 'A user already exists on this site with this username. If this is your account, log in by entering your username and password and add it as a linked login via your preferences page.'; $string['auth_oauth2description'] = 'OAuth 2 standards based authentication'; $string['auth_oauth2settings'] = 'OAuth 2 authentication settings.'; $string['confirmaccountemail'] = 'Hi {$a->firstname}, A new account has been requested at \'{$a->sitename}\' using your email address. To confirm your new account, please click the link below: Confirm your account If you need help, please contact the site administrator. {$a->admin} If you did not do this, someone else could be trying to compromise your account. Please contact the site administrator immediately.'; $string['confirmaccountemailsubject'] = '{$a}: account confirmation'; $string['confirmationinvalid'] = 'The confirmation link is either invalid, or has expired. Please start the login process again to generate a new confirmation email.'; $string['confirmationpending'] = 'This account is pending email confirmation.'; $string['confirmlinkedloginemail'] = 'Hi {$a->firstname}, A request has been made to link the {$a->issuername} login {$a->linkedemail} to your account at \'{$a->sitename}\' using your email address. To confirm this request and link these logins, please click the link below: Link your accounts If you need help, please contact the site administrator. {$a->admin} If you did not do this, someone else could be trying to compromise your account. Please contact the site administrator immediately.'; $string['confirmlinkedloginemailsubject'] = '{$a}: linked login confirmation'; $string['createaccountswarning'] = 'This authentication plugin allows users to create accounts on your site. You may want to enable the setting "authpreventaccountcreation" if you use this plugin.'; $string['createnewlinkedlogin'] = 'Link a new account ({$a})'; $string['emailconfirmlink'] = 'Link your accounts'; $string['emailconfirmlinksent'] = '

An existing account was found with this email address but it is not linked yet.

The accounts must be linked before you can log in.

An email should have been sent to your address at {$a}.

It contains easy instructions to link your accounts.

If you have any difficulty, contact the site administrator.

'; $string['emailpasswordchangeinfo'] = 'Hi {$a->firstname}, Someone (probably you) has requested a new password for your account on \'{$a->sitename}\'. However your password cannot be reset because you are using your account on another site to log in. Please log in as before, using the link on the login page. {$a->admin}'; $string['emailpasswordchangeinfosubject'] = '{$a}: Change password information'; $string['info'] = 'External account'; $string['issuer'] = 'OAuth 2 service'; $string['issuernologin'] = 'This issuer can not be used to log in.'; $string['key'] = 'Key'; $string['linkedlogins'] = 'Linked logins'; $string['linkedloginshelp'] = 'Help with linked logins'; $string['loggedin'] = 'User successfully authenticated with provider.'; $string['loginerror_userincomplete'] = 'The user information returned did not contain a username and email address. The OAuth 2 service may be configured incorrectly.'; $string['loginerror_nouserinfo'] = 'No user information was returned. The OAuth 2 service may be configured incorrectly.'; $string['loginerror_invaliddomain'] = 'The email address is not allowed at this site.'; $string['loginerror_authenticationfailed'] = 'The authentication process failed.'; $string['loginerror_cannotcreateaccounts'] = 'An account with your email address could not be found.'; $string['noconfiguredidps'] = 'There are no configured OAuth2 providers.'; $string['noissuersavailable'] = 'None of the configured OAuth 2 services allow you to link login accounts.'; $string['notloggedindebug'] = 'The login attempt failed. Reason: {$a}'; $string['notwhileloggedinas'] = 'Linked logins cannot be managed while logged in as another user.'; $string['oauth2:managelinkedlogins'] = 'Manage own linked login accounts'; $string['notenabled'] = 'Sorry, OAuth 2 authentication plugin is not enabled'; $string['plugindescription'] = 'This authentication plugin displays a list of the configured identity providers on the login page. Selecting an identity provider allows users to login with their credentials from an OAuth 2 provider.'; $string['pluginname'] = 'OAuth 2'; $string['alreadylinked'] = 'This external account is already linked to an account on this site'; $string['privacy:metadata:auth_oauth2'] = 'OAuth 2 authentication'; $string['privacy:metadata:auth_oauth2:authsubsystem'] = 'This plugin is connected to the authentication subsystem.'; $string['privacy:metadata:auth_oauth2:confirmtoken'] = 'The confirmation token.'; $string['privacy:metadata:auth_oauth2:confirmtokenexpires'] = 'The timestamp when the confirmation token expires.'; $string['privacy:metadata:auth_oauth2:email'] = 'The external email that maps to this account.'; $string['privacy:metadata:auth_oauth2:issuerid'] = 'The ID of the OAuth 2 issuer for this OAuth 2 login'; $string['privacy:metadata:auth_oauth2:tableexplanation'] = 'OAuth 2 accounts linked to a user\'s Moodle account.'; $string['privacy:metadata:auth_oauth2:timecreated'] = 'The timestamp when the user account was linked to the OAuth 2 login.'; $string['privacy:metadata:auth_oauth2:timemodified'] = 'The timestamp when this record was modified.'; $string['privacy:metadata:auth_oauth2:userid'] = 'The ID of the user account which the OAuth 2 login is linked to.'; $string['privacy:metadata:auth_oauth2:usermodified'] = 'The ID of the user who modified this account.'; $string['privacy:metadata:auth_oauth2:username'] = 'The external username that maps to this account.'; $string['testidplogin'] = 'Test login with:'; $string['userinfo'] = 'User data from provider:'; $string['value'] = 'Value'; PKB\gfn login.phpnu[. /** * Open ID authentication. This file is a simple login entry point for OAuth identity providers. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ require_once('../../config.php'); $issuerid = required_param('id', PARAM_INT); $wantsurl = optional_param('wantsurl', '', PARAM_LOCALURL); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/oauth2/login.php', ['id' => $issuerid])); require_sesskey(); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $issuer = new \core\oauth2\issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } $returnparams = ['wantsurl' => $wantsurl, 'sesskey' => sesskey(), 'id' => $issuerid]; $returnurl = new moodle_url('/auth/oauth2/login.php', $returnparams); $client = \core\oauth2\api::get_user_oauth_client($issuer, $returnurl); if ($client) { if (!$client->is_logged_in()) { redirect($client->get_login_url()); } $auth = new \auth_oauth2\auth(); $auth->complete_login($client, $wantsurl); } else { throw new moodle_exception('Could not get an OAuth client.'); } PKB\S" " test.phpnu[. /** * This file allows for testing of login via configured oauth2 IDP poviders. * * @package auth_oauth2 * @copyright 2021 Matt Porritt * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ // Require_login is not needed here. // phpcs:disable moodle.Files.RequireLogin.Missing require_once('../../config.php'); require_sesskey(); $issuerid = required_param('id', PARAM_INT); $url = new moodle_url('/auth/oauth2/test.php', ['id' => $issuerid, 'sesskey' => sesskey()]); $PAGE->set_context(context_system::instance()); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $issuer = new \core\oauth2\issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } $client = \core\oauth2\api::get_user_oauth_client($issuer, $url); if ($client) { // We have a valid client, now lets see if we can log into the IDP. if (!$client->is_logged_in()) { redirect($client->get_login_url()); } echo $OUTPUT->header(); // We were successful logging into the IDP. echo $OUTPUT->notification(get_string('loggedin', 'auth_oauth2'), 'notifysuccess'); // Try getting user info from the IDP. $endpointurl = $client->get_issuer()->get_endpoint_url('userinfo'); $response = $client->get($endpointurl); $userinfo = json_decode($response, true); $templateinfo = []; foreach ($userinfo as $key => $value) { // We are just displaying the data from the IdP for testing purposes, // so we are more interested in displaying it to the admin than // processing it. if (is_array($value)) { $value = json_encode($value); } $templateinfo[] = ['name' => $key, 'value' => $value]; } // Display user info. if (!empty($templateinfo)) { echo $OUTPUT->render_from_template('auth_oauth2/idpresponse', ['pairs' => $templateinfo]); } } else { throw new moodle_exception('Could not get an OAuth client.'); } echo $OUTPUT->footer(); PKB\@lib.phpnu[. /** * Callbacks for auth_oauth2 * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Navigation hook to add to preferences page. * * @param navigation_node $useraccount * @param stdClass $user * @param context_user $context * @param stdClass $course * @param context_course $coursecontext */ function auth_oauth2_extend_navigation_user_settings(navigation_node $useraccount, stdClass $user, context_user $context, stdClass $course, context_course $coursecontext) { global $USER; if (\auth_oauth2\api::is_enabled() && !\core\session\manager::is_loggedinas()) { if (has_capability('auth/oauth2:managelinkedlogins', $context) && $user->id == $USER->id) { $parent = $useraccount->parent->find('useraccount', navigation_node::TYPE_CONTAINER); $parent->add(get_string('linkedlogins', 'auth_oauth2'), new moodle_url('/auth/oauth2/linkedlogins.php')); } } } /** * Callback to remove linked logins for deleted users. * * @param stdClass $user */ function auth_oauth2_pre_user_delete($user) { global $DB; $DB->delete_records(auth_oauth2\linked_login::TABLE, ['userid' => $user->id]); } PKB\ version.phpnu[. /** * Version information * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'auth_oauth2'; // Full name of the plugin (used for diagnostics). PKB\t>u confirm-linkedlogin.phpnu[. /** * Confirm self oauth2 user. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir . '/authlib.php'); $token = required_param('token', PARAM_RAW); $username = required_param('username', PARAM_USERNAME); $userid = required_param('userid', PARAM_INT); $issuerid = required_param('issuerid', PARAM_INT); $redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. $PAGE->set_url('/auth/oauth2/confirm-linkedlogin.php'); $PAGE->set_context(context_system::instance()); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $confirmed = \auth_oauth2\api::confirm_link_login($userid, $username, $issuerid, $token); if ($confirmed) { // The user has confirmed successfully, let's log them in. if (!$user = get_complete_user_data('id', $userid)) { throw new \moodle_exception('cannotfinduser', '', '', $userid); } if ($user->id == $USER->id) { // Check where to go, $redirect has a higher preference. if (empty($redirect) and !empty($SESSION->wantsurl) ) { $redirect = $SESSION->wantsurl; unset($SESSION->wantsurl); } if (!empty($redirect)) { redirect($redirect); } } $PAGE->navbar->add(get_string("confirmed")); $PAGE->set_title(get_string("confirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "

".get_string("thanks").", ". fullname($user) . "

\n"; echo "

".get_string("confirmed")."

\n"; // If $wantsurl and $redirect are empty, then the button will navigate the identical user to the dashboard. if ($user->id == $USER->id) { echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); } else if (!isloggedin() || isguestuser()) { echo $OUTPUT->single_button(get_login_url(), get_string('login')); } else { echo $OUTPUT->single_button("$CFG->wwwroot/login/logout.php", get_string('logout')); } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else { // Avoid error if logged-in user visiting the page. if (!isloggedin()) { \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); } } redirect("$CFG->wwwroot/"); PKB\199classes/api.phpnu[. /** * Class for loading/storing oauth2 linked logins from the DB. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2; use context_user; use stdClass; use moodle_exception; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Static list of api methods for auth oauth2 configuration. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class api { /** * Remove all linked logins that are using issuers that have been deleted. * * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all) * @return boolean */ public static function clean_orphaned_linked_logins($issuerid = false) { return linked_login::delete_orphaned($issuerid); } /** * List linked logins * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param int $userid (defaults to $USER->id) * @return linked_login[] */ public static function get_linked_logins($userid = false) { global $USER; if ($userid === false) { $userid = $USER->id; } if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $context = context_user::instance($userid); require_capability('auth/oauth2:managelinkedlogins', $context); return linked_login::get_records(['userid' => $userid, 'confirmtoken' => '']); } /** * See if there is a match for this username and issuer in the linked_login table. * * @param string $username as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @return linked_login|false record if found and user exists, false otherwise. */ public static function match_username_to_user($username, $issuer) { $params = [ 'issuerid' => $issuer->get('id'), 'username' => $username ]; $result = linked_login::get_record($params); if ($result) { $user = \core_user::get_user($result->get('userid')); if (!empty($user) && !$user->deleted) { return $result; } } return false; } /** * Link a login to this account. * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @param bool $skippermissions During signup we need to set this before the user is setup for capability checks. * @return linked_login */ public static function link_login($userinfo, $issuer, $userid = false, $skippermissions = false) { global $USER; if ($userid === false) { $userid = $USER->id; } if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $context = context_user::instance($userid); if (!$skippermissions) { require_capability('auth/oauth2:managelinkedlogins', $context); } $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $userid; $existing = linked_login::get_record((array)$record); if ($existing) { $existing->set('confirmtoken', ''); $existing->update(); return $existing; } $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); return $linkedlogin->create(); } /** * Send an email with a link to confirm linking this account. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @return bool */ public static function send_confirm_link_login_email($userinfo, $issuer, $userid) { $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $userid; if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } $record->email = $userinfo['email']; $record->confirmtoken = random_string(32); $expires = new \DateTime('NOW'); $expires->add(new \DateInterval('PT30M')); $record->confirmtokenexpires = $expires->getTimestamp(); $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); // Construct the email. $site = get_site(); $supportuser = \core_user::get_support_user(); $user = get_complete_user_data('id', $userid); $data = new stdClass(); $placeholders = \core_user::get_name_placeholders($user); foreach ($placeholders as $field => $value) { $data->{$field} = $value; } $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $data->issuername = format_string($issuer->get('name')); $data->linkedemail = format_string($linkedlogin->get('email')); $subject = get_string('confirmlinkedloginemailsubject', 'auth_oauth2', format_string($site->fullname)); $params = [ 'token' => $linkedlogin->get('confirmtoken'), 'userid' => $userid, 'username' => $userinfo['username'], 'issuerid' => $issuer->get('id'), ]; $confirmationurl = new moodle_url('/auth/oauth2/confirm-linkedlogin.php', $params); $data->link = $confirmationurl->out(false); $message = get_string('confirmlinkedloginemail', 'auth_oauth2', $data); $messagehtml = text_to_html(get_string('confirmlinkedloginemail', 'auth_oauth2', $data), false, false); $user->mailformat = 1; // Always send HTML version as well. // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. return email_to_user($user, $supportuser, $subject, $message, $messagehtml); } /** * Look for a waiting confirmation token, and if we find a match - confirm it. * * @param int $userid * @param string $username * @param int $issuerid * @param string $token * @return boolean True if we linked. */ public static function confirm_link_login($userid, $username, $issuerid, $token) { if (empty($token) || empty($userid) || empty($issuerid) || empty($username)) { return false; } $params = [ 'userid' => $userid, 'username' => $username, 'issuerid' => $issuerid, 'confirmtoken' => $token, ]; $login = linked_login::get_record($params); if (empty($login)) { return false; } $expires = $login->get('confirmtokenexpires'); if (time() > $expires) { $login->delete(); return false; } $login->set('confirmtokenexpires', 0); $login->set('confirmtoken', ''); $login->update(); return true; } /** * Create an account with a linked login that is already confirmed. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @return stdClass */ public static function create_new_confirmed_account($userinfo, $issuer) { global $CFG, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $user = new stdClass(); $user->auth = 'oauth2'; $user->mnethostid = $CFG->mnet_localhost_id; $user->secret = random_string(15); $user->password = ''; $user->confirmed = 1; // Set the user to confirmed. $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $user->id; $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); return $user; } /** * Send an email with a link to confirm creating this account. * * @param array $userinfo as returned from an oauth client. * @param \core\oauth2\issuer $issuer * @param int $userid (defaults to $USER->id) * @return stdClass */ public static function send_confirm_account_email($userinfo, $issuer) { global $CFG, $DB; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); if (linked_login::has_existing_issuer_match($issuer, $userinfo['username'])) { throw new moodle_exception('alreadylinked', 'auth_oauth2'); } $user = new stdClass(); $user->auth = 'oauth2'; $user->mnethostid = $CFG->mnet_localhost_id; $user->secret = random_string(15); $user->password = ''; $user->confirmed = 0; // The user is not yet confirmed. $user = self::save_user($userinfo, $user); // The linked account is pre-confirmed. $record = new stdClass(); $record->issuerid = $issuer->get('id'); $record->username = $userinfo['username']; $record->userid = $user->id; $record->email = $userinfo['email']; $record->confirmtoken = ''; $record->confirmtokenexpires = 0; $linkedlogin = new linked_login(0, $record); $linkedlogin->create(); // Construct the email. $site = get_site(); $supportuser = \core_user::get_support_user(); $user = get_complete_user_data('id', $user->id); $data = new stdClass(); $placeholders = \core_user::get_name_placeholders($user); foreach ($placeholders as $field => $value) { $data->{$field} = $value; } $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $subject = get_string('confirmaccountemailsubject', 'auth_oauth2', format_string($site->fullname)); $params = [ 'token' => $user->secret, 'username' => $userinfo['username'] ]; $confirmationurl = new moodle_url('/auth/oauth2/confirm-account.php', $params); $data->link = $confirmationurl->out(false); $message = get_string('confirmaccountemail', 'auth_oauth2', $data); $messagehtml = text_to_html(get_string('confirmaccountemail', 'auth_oauth2', $data), false, false); $user->mailformat = 1; // Always send HTML version as well. // Directly email rather than using the messaging system to ensure its not routed to a popup or jabber. email_to_user($user, $supportuser, $subject, $message, $messagehtml); return $user; } /** * Delete a users own linked login * * Requires auth/oauth2:managelinkedlogins capability at the user context. * * @param int $linkedloginid */ public static function delete_linked_login($linkedloginid) { global $USER; if (\core\session\manager::is_loggedinas()) { throw new moodle_exception('notwhileloggedinas', 'auth_oauth2'); } $login = linked_login::get_record([ 'id' => $linkedloginid, 'userid' => $USER->id, 'confirmtoken' => '', ], MUST_EXIST); $context = context_user::instance($login->get('userid')); require_capability('auth/oauth2:managelinkedlogins', $context); $login->delete(); } /** * Delete linked logins for a user. * * @param \core\event\user_deleted $event * @return boolean */ public static function user_deleted(\core\event\user_deleted $event) { global $DB; $userid = $event->objectid; return $DB->delete_records(linked_login::TABLE, ['userid' => $userid]); } /** * Is the plugin enabled. * * @return bool */ public static function is_enabled() { return is_enabled_auth('oauth2'); } /** * Create a new user & update the profile fields * * @param array $userinfo * @param object $user * @return stdClass */ private static function save_user(array $userinfo, object $user): object { // Map supplied issuer user info to Moodle user fields. $userfieldmapping = new \core\oauth2\user_field_mapping(); $userfieldlist = $userfieldmapping->get_internalfields(); $hasprofilefield = false; foreach ($userfieldlist as $field) { if (isset($userinfo[$field]) && $userinfo[$field]) { $user->$field = $userinfo[$field]; // Check whether the profile fields exist or not. $hasprofilefield = $hasprofilefield || strpos($field, \core_user\fields::PROFILE_FIELD_PREFIX) === 0; } } // Create a new user. $user->id = user_create_user($user, false, true); // If profile fields exist then save custom profile fields data. if ($hasprofilefield) { profile_save_data($user); } return $user; } } PKB\MJ% classes/privacy/provider.phpnu[. /** * Privacy class for requesting user data for auth_oauth2. * * @package auth_oauth2 * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\metadata\collection; use core_privacy\local\request\contextlist; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_privacy\local\request\userlist; use core_privacy\local\request\approved_userlist; /** * Privacy provider for auth_oauth2 * * @package auth_oauth2 * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Get information about the user data stored by this plugin. * * @param collection $collection An object for storing metadata. * @return collection The metadata. */ public static function get_metadata(collection $collection): collection { $authfields = [ 'timecreated' => 'privacy:metadata:auth_oauth2:timecreated', 'timemodified' => 'privacy:metadata:auth_oauth2:timemodified', 'usermodified' => 'privacy:metadata:auth_oauth2:usermodified', 'userid' => 'privacy:metadata:auth_oauth2:userid', 'issuerid' => 'privacy:metadata:auth_oauth2:issuerid', 'username' => 'privacy:metadata:auth_oauth2:username', 'email' => 'privacy:metadata:auth_oauth2:email', 'confirmtoken' => 'privacy:metadata:auth_oauth2:confirmtoken', 'confirmtokenexpires' => 'privacy:metadata:auth_oauth2:confirmtokenexpires' ]; $collection->add_database_table('auth_oauth2_linked_login', $authfields, 'privacy:metadata:auth_oauth2:tableexplanation'); // Regarding this block, we are unable to export or purge this data, as // it would damage the oauth2 data across the whole site. foreach ([ 'oauth2_endpoint', 'oauth2_user_field_mapping', 'oauth2_access_token', 'oauth2_system_account', ] as $tablename) { $collection->add_database_table($tablename, [ 'usermodified' => 'privacy:metadata:auth_oauth2:usermodified', ], 'privacy:metadata:auth_oauth2:tableexplanation'); } $collection->link_subsystem('core_auth', 'privacy:metadata:auth_oauth2:authsubsystem'); return $collection; } /** * Return all contexts for this userid. In this situation the user context. * * @param int $userid The user ID. * @return contextlist The list of context IDs. */ public static function get_contexts_for_userid(int $userid): contextlist { $sql = "SELECT ctx.id FROM {auth_oauth2_linked_login} ao JOIN {context} ctx ON ctx.instanceid = ao.userid AND ctx.contextlevel = :contextlevel WHERE ao.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!$context instanceof \context_user) { return; } $sql = "SELECT userid FROM {auth_oauth2_linked_login} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * Export all oauth2 information for the list of contexts and this user. * * @param approved_contextlist $contextlist The list of approved contexts for a user. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; // Export oauth2 linked accounts. $context = \context_user::instance($contextlist->get_user()->id); $sql = "SELECT ll.id, ll.username, ll.email, ll.timecreated, ll.timemodified, oi.name as issuername FROM {auth_oauth2_linked_login} ll JOIN {oauth2_issuer} oi ON oi.id = ll.issuerid WHERE ll.userid = :userid"; if ($oauth2accounts = $DB->get_records_sql($sql, ['userid' => $contextlist->get_user()->id])) { foreach ($oauth2accounts as $oauth2account) { $data = (object)[ 'timecreated' => transform::datetime($oauth2account->timecreated), 'timemodified' => transform::datetime($oauth2account->timemodified), 'issuerid' => $oauth2account->issuername, 'username' => $oauth2account->username, 'email' => $oauth2account->email ]; writer::with_context($context)->export_data([ get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'), $oauth2account->issuername ], $data); } } } /** * Delete all user data for this context. * * @param \context $context The context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { if ($context->contextlevel != CONTEXT_USER) { return; } static::delete_user_data($context->instanceid); } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if ($context instanceof \context_user) { static::delete_user_data($context->instanceid); } } /** * Delete all user data for this user only. * * @param approved_contextlist $contextlist The list of approved contexts for a user. */ public static function delete_data_for_user(approved_contextlist $contextlist) { if (empty($contextlist->count())) { return; } $userid = $contextlist->get_user()->id; foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel != CONTEXT_USER) { continue; } if ($context->instanceid == $userid) { // Because we only use user contexts the instance ID is the user ID. static::delete_user_data($context->instanceid); } } } /** * This does the deletion of user data for the auth_oauth2. * * @param int $userid The user ID */ protected static function delete_user_data(int $userid) { global $DB; // Because we only use user contexts the instance ID is the user ID. $DB->delete_records('auth_oauth2_linked_login', ['userid' => $userid]); } } PKB\Elclasses/linked_login.phpnu[. /** * Class for loading/storing issuers from the DB. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2; defined('MOODLE_INTERNAL') || die(); use core\persistent; /** * Class for loading/storing issuer from the DB * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class linked_login extends persistent { const TABLE = 'auth_oauth2_linked_login'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'issuerid' => array( 'type' => PARAM_INT ), 'userid' => array( 'type' => PARAM_INT ), 'username' => array( 'type' => PARAM_RAW ), 'email' => array( 'type' => PARAM_RAW ), 'confirmtoken' => array( 'type' => PARAM_RAW ), 'confirmtokenexpires' => array( 'type' => PARAM_INT ) ); } /** * Check whether there are any valid linked accounts for this issuer * and username combination. * * @param \core\oauth2\issuer $issuer The issuer * @param string $username The username to check */ public static function has_existing_issuer_match(\core\oauth2\issuer $issuer, $username) { global $DB; $where = "issuerid = :issuerid AND username = :username AND (confirmtokenexpires = 0 OR confirmtokenexpires > :maxexpiry)"; $count = $DB->count_records_select(static::TABLE, $where, [ 'issuerid' => $issuer->get('id'), 'username' => $username, 'maxexpiry' => (new \DateTime('NOW'))->getTimestamp(), ]); return $count > 0; } /** * Remove all linked logins that are using issuers that have been deleted. * * @param int $issuerid The issuer id of the issuer to check, or false to check all (defaults to all) * @return boolean */ public static function delete_orphaned($issuerid = false) { global $DB; // Delete any linked_login entries with a issuerid // which does not exist in the issuer table. // In the left join, the issuer id will be null // where a match linked_login.issuerid is not found. $sql = "DELETE FROM {" . self::TABLE . "} WHERE issuerid NOT IN (SELECT id FROM {" . \core\oauth2\issuer::TABLE . "})"; $params = []; if (!empty($issuerid)) { $sql .= ' AND issuerid = ?'; $params['issuerid'] = $issuerid; } return $DB->execute($sql, $params); } } PKB\ 8 8 classes/output/renderer.phpnu[. /** * Output rendering for the plugin. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\output; use plugin_renderer_base; use html_table; use html_table_cell; use html_table_row; use html_writer; use auth_oauth2\linked_login; use moodle_url; defined('MOODLE_INTERNAL') || die(); /** * Implements the plugin renderer * * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * This function will render one beautiful table with all the linked_logins. * * @param linked_login[] $linkedlogins - list of all linked logins. * @return string HTML to output. */ public function linked_logins_table($linkedlogins) { global $CFG; $table = new html_table(); $table->head = [ get_string('issuer', 'auth_oauth2'), get_string('info', 'auth_oauth2'), get_string('edit'), ]; $table->attributes['class'] = 'admintable generaltable table table-hover'; $data = []; $index = 0; foreach ($linkedlogins as $linkedlogin) { // Issuer. $issuerid = $linkedlogin->get('issuerid'); $issuer = \core\oauth2\api::get_issuer($issuerid); $issuercell = new html_table_cell(s($issuer->get('name'))); // Issuer. $username = $linkedlogin->get('username'); $email = $linkedlogin->get('email'); $usernamecell = new html_table_cell(s($email) . ', (' . s($username) . ')'); $links = ''; // Delete. $deleteparams = ['linkedloginid' => $linkedlogin->get('id'), 'action' => 'delete', 'sesskey' => sesskey()]; $deleteurl = new moodle_url('/auth/oauth2/linkedlogins.php', $deleteparams); $deletelink = html_writer::link($deleteurl, $this->pix_icon('t/delete', get_string('delete'))); $links .= ' ' . $deletelink; $editcell = new html_table_cell($links); $row = new html_table_row([ $issuercell, $usernamecell, $editcell, ]); $data[] = $row; $index++; } $table->data = $data; return html_writer::table($table); } } PKB\=bhhclasses/auth.phpnu[. /** * Anobody can login with any password. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ namespace auth_oauth2; defined('MOODLE_INTERNAL') || die(); use pix_icon; use moodle_url; use core_text; use context_system; use stdClass; use core\oauth2\issuer; use core\oauth2\client; global $CFG; require_once($CFG->libdir.'/authlib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/login/lib.php'); /** * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class auth extends \auth_plugin_base { /** * @var stdClass $userinfo The set of user info returned from the oauth handshake */ private static $userinfo; /** * @var stdClass $userpicture The url to a picture. */ private static $userpicture; /** * Constructor. */ public function __construct() { $this->authtype = 'oauth2'; $this->config = get_config('auth_oauth2'); } /** * Returns true if the username and password work or don't exist and false * if the user exists and the password is wrong. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ public function user_login($username, $password) { $cached = $this->get_static_user_info(); if (empty($cached)) { // This means we were called as part of a normal login flow - without using oauth. return false; } $verifyusername = $cached['username']; if ($verifyusername == $username) { return true; } return false; } /** * We don't want to allow users setting an internal password. * * @return bool */ public function prevent_local_passwords() { return true; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ public function is_internal() { return false; } /** * Indicates if moodle should automatically update internal user * records with data from external sources using the information * from auth_plugin_base::get_userinfo(). * * @return bool true means automatically copy data from ext to user table */ public function is_synchronised_with_external() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ public function can_change_password() { return false; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ public function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ public function can_reset_password() { return false; } /** * Returns true if plugin can be manually set. * * @return bool */ public function can_be_manually_set() { return true; } /** * Return the userinfo from the oauth handshake. Will only be valid * for the logged in user. * @param string $username */ public function get_userinfo($username) { $cached = $this->get_static_user_info(); if (!empty($cached) && $cached['username'] == $username) { return $cached; } return false; } /** * Return a list of identity providers to display on the login page. * * @param string|moodle_url $wantsurl The requested URL. * @return array List of arrays with keys url, iconurl and name. */ public function loginpage_idp_list($wantsurl) { $providers = \core\oauth2\api::get_all_issuers(true); $result = []; foreach ($providers as $idp) { if ($idp->is_available_for_login()) { $params = ['id' => $idp->get('id'), 'sesskey' => sesskey()]; if (!empty($wantsurl)) { $params['wantsurl'] = $wantsurl; } $url = new moodle_url('/auth/oauth2/login.php', $params); $icon = $idp->get('image'); $result[] = ['url' => $url, 'iconurl' => $icon, 'name' => $idp->get_display_name()]; } } return $result; } /** * Statically cache the user info from the oauth handshake * @param stdClass $userinfo */ private function set_static_user_info($userinfo) { self::$userinfo = $userinfo; } /** * Get the static cached user info * @return stdClass */ private function get_static_user_info() { return self::$userinfo; } /** * Statically cache the user picture from the oauth handshake * @param string $userpicture */ private function set_static_user_picture($userpicture) { self::$userpicture = $userpicture; } /** * Get the static cached user picture * @return string */ private function get_static_user_picture() { return self::$userpicture; } /** * If this user has no picture - but we got one from oauth - set it. * @param stdClass $user * @return boolean True if the image was updated. */ private function update_picture($user) { global $CFG, $DB, $USER; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/gdlib.php'); require_once($CFG->dirroot . '/user/lib.php'); $fs = get_file_storage(); $userid = $user->id; if (!empty($user->picture)) { return false; } if (!empty($CFG->enablegravatar)) { return false; } $picture = $this->get_static_user_picture(); if (empty($picture)) { return false; } $context = \context_user::instance($userid, MUST_EXIST); $fs->delete_area_files($context->id, 'user', 'newicon'); $filerecord = array( 'contextid' => $context->id, 'component' => 'user', 'filearea' => 'newicon', 'itemid' => 0, 'filepath' => '/', 'filename' => 'image' ); try { $fs->create_file_from_string($filerecord, $picture); } catch (\file_exception $e) { return get_string($e->errorcode, $e->module, $e->a); } $iconfile = $fs->get_area_files($context->id, 'user', 'newicon', false, 'itemid', false); // There should only be one. $iconfile = reset($iconfile); // Something went wrong while creating temp file - remove the uploaded file. if (!$iconfile = $iconfile->copy_content_to_temp()) { $fs->delete_area_files($context->id, 'user', 'newicon'); return false; } // Copy file to temporary location and the send it for processing icon. $newpicture = (int) process_new_icon($context, 'user', 'icon', 0, $iconfile); // Delete temporary file. @unlink($iconfile); // Remove uploaded file. $fs->delete_area_files($context->id, 'user', 'newicon'); // Set the user's picture. $updateuser = new stdClass(); $updateuser->id = $userid; $updateuser->picture = $newpicture; $USER->picture = $newpicture; user_update_user($updateuser); return true; } /** * Update user data according to data sent by authorization server. * * @param array $externaldata data from authorization server * @param stdClass $userdata Current data of the user to be updated * @return stdClass The updated user record, or the existing one if there's nothing to be updated. */ private function update_user(array $externaldata, $userdata) { $user = (object) [ 'id' => $userdata->id, ]; // We can only update if the default authentication type of the user is set to OAuth2 as well. Otherwise, we might mess // up the user data of other users that use different authentication mechanisms (e.g. linked logins). if ($userdata->auth !== $this->authtype) { return $userdata; } $allfields = array_merge($this->userfields, $this->get_custom_user_profile_fields()); // Go through each field from the external data. foreach ($externaldata as $fieldname => $value) { if (!in_array($fieldname, $allfields)) { // Skip if this field doesn't belong to the list of fields that can be synced with the OAuth2 issuer. continue; } $userhasfield = property_exists($userdata, $fieldname); // Find out if it is a profile field. $isprofilefield = strpos($fieldname, 'profile_field_') === 0; $profilefieldname = str_replace('profile_field_', '', $fieldname); $userhasprofilefield = $isprofilefield && array_key_exists($profilefieldname, $userdata->profile); // Just in case this field is on the list, but not part of the user data. This shouldn't happen though. if (!($userhasfield || $userhasprofilefield)) { continue; } // Get the old value. $oldvalue = $isprofilefield ? (string) $userdata->profile[$profilefieldname] : (string) $userdata->$fieldname; // Get the lock configuration of the field. if (!empty($this->config->{'field_lock_' . $fieldname})) { $lockvalue = $this->config->{'field_lock_' . $fieldname}; } else { $lockvalue = 'unlocked'; } // We should update fields that meet the following criteria: // - Lock value set to 'unlocked'; or 'unlockedifempty', given the current value is empty. // - The value has changed. if ($lockvalue === 'unlocked' || ($lockvalue === 'unlockedifempty' && empty($oldvalue))) { $value = (string)$value; if ($oldvalue !== $value) { $user->$fieldname = $value; } } } // Update the user data. user_update_user($user, false); // Save user profile data. profile_save_data($user); // Refresh user for $USER variable. return get_complete_user_data('id', $user->id); } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ public function user_confirm($username, $confirmsecret) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->auth != $this->authtype) { return AUTH_CONFIRM_ERROR; } else if ($user->secret === $confirmsecret && $user->confirmed) { return AUTH_CONFIRM_ALREADY; } else if ($user->secret === $confirmsecret) { // They have provided the secret key to get in. $DB->set_field("user", "confirmed", 1, array("id" => $user->id)); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } /** * Print a page showing that a confirm email was sent with instructions. * * @param string $title * @param string $message */ public function print_confirm_required($title, $message) { global $PAGE, $OUTPUT, $CFG; $PAGE->navbar->add($title); $PAGE->set_title($title); $PAGE->set_heading($PAGE->course->fullname); echo $OUTPUT->header(); notice($message, "$CFG->wwwroot/index.php"); } /** * Complete the login process after oauth handshake is complete. * @param \core\oauth2\client $client * @param string $redirecturl * @return void Either redirects or throws an exception */ public function complete_login(client $client, $redirecturl) { global $CFG, $SESSION, $PAGE; $rawuserinfo = $client->get_raw_userinfo(); $userinfo = $client->get_userinfo(); if (!$userinfo) { // Trigger login failed event. $failurereason = AUTH_LOGIN_NOUSER; $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('loginerror_nouserinfo', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (empty($userinfo['username']) || empty($userinfo['email'])) { // Trigger login failed event. $failurereason = AUTH_LOGIN_NOUSER; $event = \core\event\user_login_failed::create(['other' => ['username' => 'unknown', 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('loginerror_userincomplete', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } $userinfo['username'] = trim(core_text::strtolower($userinfo['username'])); $oauthemail = $userinfo['email']; // Once we get here we have the user info from oauth. $userwasmapped = false; // Clean and remember the picture / lang. if (!empty($userinfo['picture'])) { $this->set_static_user_picture($userinfo['picture']); unset($userinfo['picture']); } if (!empty($userinfo['lang'])) { $userinfo['lang'] = str_replace('-', '_', trim(core_text::strtolower($userinfo['lang']))); if (!get_string_manager()->translation_exists($userinfo['lang'], false)) { unset($userinfo['lang']); } } $issuer = $client->get_issuer(); // First we try and find a defined mapping. $linkedlogin = api::match_username_to_user($userinfo['username'], $issuer); if (!empty($linkedlogin) && empty($linkedlogin->get('confirmtoken'))) { $mappeduser = get_complete_user_data('id', $linkedlogin->get('userid')); if ($mappeduser && $mappeduser->suspended) { // Check if there's another user with the same email that is not suspended. $moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE); if ($moodleuser->id == $mappeduser->id) { $failurereason = AUTH_LOGIN_SUSPENDED; $event = \core\event\user_login_failed::create([ 'userid' => $mappeduser->id, 'other' => [ 'username' => $userinfo['username'], 'reason' => $failurereason, ], ]); $event->trigger(); $SESSION->loginerrormsg = get_string('invalidlogin'); $client->log_out(); redirect(new moodle_url('/login/index.php')); } else if ($moodleuser && !$moodleuser->suspended) { // Update the OAuth2 linked login to point to the active user account. $linkedlogin->set('userid', $moodleuser->id); $linkedlogin->set('timemodified', time()); $linkedlogin->update(); // Update user fields and continue with login. $userinfo = $this->update_user($userinfo, $moodleuser); $userwasmapped = true; } } else if ($mappeduser && ($mappeduser->confirmed || !$issuer->get('requireconfirmation'))) { // Update user fields. $userinfo = $this->update_user($userinfo, $mappeduser); $userwasmapped = true; } else { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('confirmationpending', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } } else if (!empty($linkedlogin)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('confirmationpending', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!$issuer->is_valid_login_domain($oauthemail)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); $errormsg = get_string('notloggedindebug', 'auth_oauth2', get_string('loginerror_invaliddomain', 'auth_oauth2')); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!$userwasmapped) { // No defined mapping - we need to see if there is an existing account with the same email. $moodleuser = \core_user::get_user_by_email($userinfo['email'], '*', null, IGNORE_MULTIPLE); // Ensure we don't link a login for a suspended user. if (!empty($moodleuser) && $moodleuser->suspended) { $failurereason = AUTH_LOGIN_SUSPENDED; $event = \core\event\user_login_failed::create([ 'userid' => $moodleuser->id, 'other' => [ 'username' => $userinfo['email'], 'reason' => $failurereason, ], ]); $event->trigger(); $SESSION->loginerrormsg = get_string('invalidlogin'); $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!empty($moodleuser)) { if ($issuer->get('requireconfirmation')) { $PAGE->set_url('/auth/oauth2/confirm-link-login.php'); $PAGE->set_context(context_system::instance()); \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $moodleuser->id); // Request to link to existing account. $emailconfirm = get_string('emailconfirmlink', 'auth_oauth2'); $message = get_string('emailconfirmlinksent', 'auth_oauth2', $moodleuser->email); $this->print_confirm_required($emailconfirm, $message); exit(); } else { \auth_oauth2\api::link_login($userinfo, $issuer, $moodleuser->id, true); // We dont have profile loaded on $moodleuser, so load it. require_once($CFG->dirroot.'/user/profile/lib.php'); profile_load_custom_fields($moodleuser); $userinfo = $this->update_user($userinfo, $moodleuser); // No redirect, we will complete this login. } } else { // This is a new account. $exists = \core_user::get_user_by_username($userinfo['username']); // Creating a new user? if ($exists) { // Trigger login failed event. $failurereason = AUTH_LOGIN_FAILED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username exists but the emails don't match. Refuse to continue. $errormsg = get_string('accountexists', 'auth_oauth2'); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (email_is_not_allowed($userinfo['email'])) { // Trigger login failed event. $failurereason = AUTH_LOGIN_FAILED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username exists but the emails don't match. Refuse to continue. $reason = get_string('loginerror_invaliddomain', 'auth_oauth2'); $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if (!empty($CFG->authpreventaccountcreation)) { // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = \core\event\user_login_failed::create(['other' => ['username' => $userinfo['username'], 'reason' => $failurereason]]); $event->trigger(); // The username does not exist and settings prevent creating new accounts. $reason = get_string('loginerror_cannotcreateaccounts', 'auth_oauth2'); $errormsg = get_string('notloggedindebug', 'auth_oauth2', $reason); $SESSION->loginerrormsg = $errormsg; $client->log_out(); redirect(new moodle_url('/login/index.php')); } if ($issuer->get('requireconfirmation')) { $PAGE->set_url('/auth/oauth2/confirm-account.php'); $PAGE->set_context(context_system::instance()); // Create a new (unconfirmed account) and send an email to confirm it. $user = \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); $this->update_picture($user); $emailconfirm = get_string('emailconfirm'); $message = get_string('emailconfirmsent', '', $userinfo['email']); $this->print_confirm_required($emailconfirm, $message); exit(); } else { // Create a new confirmed account. $newuser = \auth_oauth2\api::create_new_confirmed_account($userinfo, $issuer); $userinfo = get_complete_user_data('id', $newuser->id); // No redirect, we will complete this login. } } } // We used to call authenticate_user - but that won't work if the current user has a different default authentication // method. Since we now ALWAYS link a login - if we get to here we can directly allow the user in. $user = (object) $userinfo; // Add extra loggedin info. $this->set_extrauserinfo((array)$rawuserinfo); complete_user_login($user, $this->get_extrauserinfo()); $this->update_picture($user); if (empty($redirecturl)) { $redirecturl = core_login_get_return_url(); } redirect(new moodle_url($redirecturl)); } /** * Returns information on how the specified user can change their password. * The password of the oauth2 accounts is not stored in Moodle. * * @param stdClass $user A user object * @return string[] An array of strings with keys subject and message */ public function get_password_change_info(stdClass $user): array { $site = get_site(); $data = new stdClass(); $data->firstname = $user->firstname; $data->lastname = $user->lastname; $data->username = $user->username; $data->sitename = format_string($site->fullname); $data->admin = generate_email_signoff(); $message = get_string('emailpasswordchangeinfo', 'auth_oauth2', $data); $subject = get_string('emailpasswordchangeinfosubject', 'auth_oauth2', format_string($site->fullname)); return [ 'subject' => $subject, 'message' => $message ]; } } PKB\Q)templates/idps.mustachenu[{{! 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 . }} {{! @template auth_oauth2/idps Render available oauth2 idps for testing. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * idps - array of oAuth IdP objects * name - the display name of the IdP * url - the url of the oAuth test endpoint * iconurl - the icon of the oAuth IdP Example context (json): { "idps": [{ "name": "Microsoft", "url": "http:\/\/localhost\/auth\/oauth2\/test.php?id=2&sesskey=4js6afElZh", "iconurl": "https:\/\/www.microsoft.com\/favicon.ico" }] } }}

{{#str}}testidplogin, auth_oauth2{{/str}}

PKB\墺EEtemplates/idpresponse.mustachenu[{{! 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 . }} {{! @template auth_oauth2/idpresponse Render response returned from the oAuth IdP for supplied test user. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * pairs - array of key value pairs return from the oAuth IdP * name - the name field from the IdP for the field * value - the value of the field for the supplied test user Example context (json): { "pairs": [{ "name": "firstname", "value": "Jane" }, { "name": "lastname", "value": "Awesome" }] } }}

{{#str}}userinfo, auth_oauth2{{/str}}

{{#pairs}} {{/pairs}}
{{#str}}key, auth_oauth2{{/str}} {{#str}}value, auth_oauth2{{/str}}
{{name}} {{{value}}}
PKB\9pplinkedlogins.phpnu[. /** * OAuth 2 Linked login configuration page. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->libdir.'/tablelib.php'); $PAGE->set_url('/auth/oauth2/linkedlogins.php'); $PAGE->set_context(context_user::instance($USER->id)); $PAGE->set_pagelayout('admin'); $strheading = get_string('linkedlogins', 'auth_oauth2'); $PAGE->set_title($strheading); $PAGE->set_heading($strheading); require_login(); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $action = optional_param('action', '', PARAM_ALPHAEXT); if ($action == 'new') { require_sesskey(); $issuerid = required_param('issuerid', PARAM_INT); $issuer = \core\oauth2\api::get_issuer($issuerid); if (!$issuer->is_available_for_login()) { throw new \moodle_exception('issuernologin', 'auth_oauth2'); } // We do a login dance with this issuer. $addparams = ['action' => 'new', 'issuerid' => $issuerid, 'sesskey' => sesskey()]; $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); $client = \core\oauth2\api::get_user_oauth_client($issuer, $addurl); if (optional_param('logout', false, PARAM_BOOL)) { $client->log_out(); } if (!$client->is_logged_in()) { redirect($client->get_login_url()); } $userinfo = $client->get_userinfo(); if (!empty($userinfo)) { try { \auth_oauth2\api::link_login($userinfo, $issuer); redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } catch (Exception $e) { redirect($PAGE->url, $e->getMessage(), null, \core\output\notification::NOTIFY_ERROR); } } else { redirect($PAGE->url, get_string('notloggedin', 'auth_oauth2'), null, \core\output\notification::NOTIFY_ERROR); } } else if ($action == 'delete') { require_sesskey(); $linkedloginid = required_param('linkedloginid', PARAM_INT); auth_oauth2\api::delete_linked_login($linkedloginid); redirect($PAGE->url, get_string('changessaved'), null, \core\output\notification::NOTIFY_SUCCESS); } $renderer = $PAGE->get_renderer('auth_oauth2'); $linkedloginid = optional_param('id', '', PARAM_RAW); $linkedlogin = null; auth_oauth2\api::clean_orphaned_linked_logins(); $issuers = \core\oauth2\api::get_all_issuers(true); $anyshowinloginpage = false; $issuerbuttons = array(); foreach ($issuers as $issuer) { if (!$issuer->is_available_for_login()) { continue; } $anyshowinloginpage = true; $addparams = ['action' => 'new', 'issuerid' => $issuer->get('id'), 'sesskey' => sesskey(), 'logout' => true]; $addurl = new moodle_url('/auth/oauth2/linkedlogins.php', $addparams); $issuerbuttons[$issuer->get('id')] = $renderer->single_button($addurl, get_string('createnewlinkedlogin', 'auth_oauth2', s($issuer->get_display_name()))); } if (!$anyshowinloginpage) { // Just a notification that we can't make it. $preferencesurl = new moodle_url('/user/preferences.php'); redirect($preferencesurl, get_string('noissuersavailable', 'auth_oauth2'), null, \core\output\notification::NOTIFY_WARNING); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('linkedlogins', 'auth_oauth2')); echo $OUTPUT->doc_link('Linked_Logins', get_string('linkedloginshelp', 'auth_oauth2')); $linkedlogins = auth_oauth2\api::get_linked_logins(); echo $renderer->linked_logins_table($linkedlogins); foreach ($issuerbuttons as $issuerbutton) { echo $issuerbutton; } echo $OUTPUT->footer(); PKB\7b confirm-account.phpnu[. /** * Confirm self oauth2 user. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir . '/authlib.php'); $usersecret = required_param('token', PARAM_RAW); $username = required_param('username', PARAM_USERNAME); $redirect = optional_param('redirect', '', PARAM_LOCALURL); // Where to redirect the browser once the user has been confirmed. $PAGE->set_url('/auth/oauth2/confirm-account.php'); $PAGE->set_context(context_system::instance()); $auth = get_auth_plugin('oauth2'); if (!\auth_oauth2\api::is_enabled()) { throw new \moodle_exception('notenabled', 'auth_oauth2'); } $confirmed = $auth->user_confirm($username, $usersecret); if ($confirmed == AUTH_CONFIRM_ALREADY && !isloggedin()) { $user = get_complete_user_data('username', $username); $PAGE->navbar->add(get_string("alreadyconfirmed")); $PAGE->set_title(get_string("alreadyconfirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "

".get_string("alreadyconfirmed")."

\n"; echo $OUTPUT->single_button("$CFG->wwwroot/course/", get_string('courses')); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else if ($confirmed == AUTH_CONFIRM_OK) { // The user has confirmed successfully, let's log them in. if (!$user = get_complete_user_data('username', $username)) { throw new \moodle_exception('cannotfinduser', '', '', s($username)); } if ($user->id == $USER->id) { // Check where to go, $redirect has a higher preference. if (empty($redirect) and !empty($SESSION->wantsurl) ) { $redirect = $SESSION->wantsurl; unset($SESSION->wantsurl); } if (!empty($redirect)) { redirect($redirect); } } $PAGE->navbar->add(get_string("confirmed")); $PAGE->set_title(get_string("confirmed")); $PAGE->set_heading($COURSE->fullname); echo $OUTPUT->header(); echo $OUTPUT->box_start('generalbox centerpara boxwidthnormal boxaligncenter'); echo "

".get_string("thanks").", ". fullname($user) . "

\n"; echo "

".get_string("confirmed")."

\n"; if (!isloggedin() || isguestuser()) { echo $OUTPUT->single_button(get_login_url(), get_string('login')); } else { echo $OUTPUT->single_button("$CFG->wwwroot/login/logout.php", get_string('logout')); } echo $OUTPUT->box_end(); echo $OUTPUT->footer(); exit; } else { if (!isloggedin()) { \core\notification::error(get_string('confirmationinvalid', 'auth_oauth2')); } } redirect("$CFG->wwwroot/"); PKB\z'--tests/privacy/provider_test.phpnu[. /** * Privacy test for the authentication oauth2 * * @package auth_oauth2 * @category test * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_oauth2\privacy; defined('MOODLE_INTERNAL') || die(); use auth_oauth2\privacy\provider; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_userlist; /** * Privacy test for the authentication oauth2 * * @package auth_oauth2 * @category test * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { /** * Set up method. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->setAdminUser(); } /** * Check that a user context is returned if there is any user data for this user. */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); $contextlist = provider::get_contexts_for_userid($user->id); // Check that we only get back one context. $this->assertCount(1, $contextlist); // Check that a context is returned is the expected. $usercontext = \context_user::instance($user->id); $this->assertEquals($usercontext->id, $contextlist->get_contextids()[0]); } /** * Test that user data is exported correctly. */ public function test_export_user_data(): void { $user = $this->getDataGenerator()->create_user(); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); $usercontext = \context_user::instance($user->id); $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'auth_oauth2', [$usercontext->id]); provider::export_user_data($approvedlist); $data = $writer->get_data([get_string('privacy:metadata:auth_oauth2', 'auth_oauth2'), $issuer->get('name')]); $this->assertEquals($info['username'], $data->username); $this->assertEquals($info['email'], $data->email); } /** * Test deleting all user data for a specific context. */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $user1 = $this->getDataGenerator()->create_user(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'jerry'; $info['email'] = 'jerry@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); $user2context = \context_user::instance($user2->id); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be two. $this->assertCount(2, $oauth2accounts); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all oauth2 accounts match with user1. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $oauth2accounts); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be one. $this->assertCount(1, $oauth2accounts); } /** * This should work identical to the above test. */ public function test_delete_data_for_user(): void { global $DB; $user1 = $this->getDataGenerator()->create_user(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'jerry'; $info['email'] = 'jerry@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); $user2context = \context_user::instance($user2->id); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be two. $this->assertCount(2, $oauth2accounts); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'auth_oauth2', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all oauth2 accounts match with user1. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $oauth2accounts); // Get all oauth2 accounts. $oauth2accounts = $DB->get_records('auth_oauth2_linked_login', array()); // There should be one user. $this->assertCount(1, $oauth2accounts); } /** * Test that only users with a user context are fetched. */ public function test_get_users_in_context(): void { $this->resetAfterTest(); $component = 'auth_oauth2'; // Create a user. $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (related data still haven't been created). $userlist = new \core_privacy\local\request\userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $issuer = \core\oauth2\api::create_standard_issuer('google'); $info = []; $info['username'] = 'gina'; $info['email'] = 'gina@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); // The list of users for user context should return the user. provider::get_users_in_context($userlist); $this->assertCount(1, $userlist); $expected = [$user->id]; $actual = $userlist->get_userids(); $this->assertEquals($expected, $actual); // The list of users for system context should not return any users. $systemcontext = \context_system::instance(); $userlist = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { $this->resetAfterTest(); $component = 'auth_oauth2'; // Create user1. $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); // Create user2. $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $info1 = []; $info1['username'] = 'gina1'; $info1['email'] = 'gina@example1.com'; \auth_oauth2\api::link_login($info1, $issuer1, $user1->id, false); $issuer2 = \core\oauth2\api::create_standard_issuer('google'); $info2 = []; $info2['username'] = 'gina2'; $info2['email'] = 'gina@example2.com'; \auth_oauth2\api::link_login($info2, $issuer2, $user2->id, false); // The list of users for usercontext1 should return user1. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(1, $userlist1); $expected = [$user1->id]; $actual = $userlist1->get_userids(); $this->assertEquals($expected, $actual); // The list of users for usercontext2 should return user2. $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $expected = [$user2->id]; $actual = $userlist2->get_userids(); $this->assertEquals($expected, $actual); // Add userlist1 to the approved user list. $approvedlist = new approved_userlist($usercontext1, $component, $userlist1->get_userids()); // Delete user data using delete_data_for_user for usercontext1. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext1 - The user list should now be empty. $userlist1 = new \core_privacy\local\request\userlist($usercontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // User data should be only removed in the user context. $systemcontext = \context_system::instance(); // Add userlist2 to the approved user list in the system context. $approvedlist = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete user1 data using delete_data_for_user. provider::delete_data_for_users($approvedlist); // Re-fetch users in usercontext2 - The user list should not be empty (user2). $userlist2 = new \core_privacy\local\request\userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } PKB\<tests/auth_test.phpnu[. namespace auth_oauth2; /** * Auth oauth2 auth functions tests. * * @package auth_oauth2 * @category test * @copyright 2019 Shamim Rezaie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \auth_oauth2\auth */ final class auth_test extends \advanced_testcase { public function test_get_password_change_info(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['auth' => 'oauth2']); $auth = get_auth_plugin($user->auth); $info = $auth->get_password_change_info($user); $this->assertEqualsCanonicalizing(['subject', 'message'], array_keys($info)); $this->assertStringContainsString( 'your password cannot be reset because you are using your account on another site to log in', $info['message']); } /** * Test complete_login for oauth2. * @covers ::complete_login */ public function test_oauth2_complete_login(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $wantsurl = new \moodle_url('/'); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['url'] = 'http://apple.com/'; $info['alternamename'] = 'Beatles'; $info['auth'] = 'oauth2'; $user = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); $auth = get_auth_plugin($user->auth); // Set up mock data. $client = $this->createMock(\core\oauth2\client::class); $client->expects($this->once())->method('get_raw_userinfo')->willReturn((object)$info); $client->expects($this->once())->method('get_userinfo')->willReturn($info); $client->expects($this->once())->method('get_issuer')->willReturn($issuer); $sink = $this->redirectEvents(); try { // Need @ as it will fail at \core\session\manager::login_user for session_regenerate_id. @$auth->complete_login($client, $wantsurl); } catch (\Exception $e) { // This happens as complete login is using 'redirect'. $this->assertInstanceOf(\moodle_exception::class, $e); } $events = $sink->get_events(); $sink->close(); // There are 2 events. First is core\event\user_updated and second is core\event\user_loggedin. $event = $events[1]; $this->assertInstanceOf('core\event\user_loggedin', $event); // Make sure the extra record is in the user_loggedin event. $extrauserinfo = $event->other['extrauserinfo']; $this->assertEquals($info, $extrauserinfo); } /** * Test case for checking the email greetings in the password change information email. */ public function test_email_greetings(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(['auth' => 'oauth2']); $auth = get_auth_plugin($user->auth); $info = $auth->get_password_change_info($user); $this->assertStringContainsString('Hi ' . $user->firstname, quoted_printable_decode($info['message'])); } } PKB\?7((tests/api_test.phpnu[. namespace auth_oauth2; /** * External auth oauth2 API tests. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * * @covers \auth_oauth2\api */ final class api_test extends \advanced_testcase { /** * Test the cleaning of orphaned linked logins for all issuers. */ public function test_clean_orphaned_linked_logins(): void { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('google'); \core\oauth2\api::create_standard_issuer('microsoft'); $user = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); \core\oauth2\api::delete_issuer($issuer->get('id')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); $this->assertCount(1, $linkedlogins); \auth_oauth2\api::clean_orphaned_linked_logins(); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); $this->assertCount(0, $linkedlogins); $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertFalse($match); } /** * Test the cleaning of orphaned linked logins for a specific issuer. */ public function test_clean_orphaned_linked_logins_with_issuer_id(): void { $this->resetAfterTest(); $this->setAdminUser(); $issuer1 = \core\oauth2\api::create_standard_issuer('google'); $issuer2 = \core\oauth2\api::create_standard_issuer('microsoft'); $user1 = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer1, $user1->id, false); $user2 = $this->getDataGenerator()->create_user(); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; \auth_oauth2\api::link_login($info, $issuer2, $user2->id, false); \core\oauth2\api::delete_issuer($issuer1->get('id')); \auth_oauth2\api::clean_orphaned_linked_logins($issuer1->get('id')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user1->id, $issuer1); $this->assertCount(0, $linkedlogins); $linkedlogins = \auth_oauth2\api::get_linked_logins($user2->id, $issuer2); $this->assertCount(1, $linkedlogins); } /** * Test creating a new confirmed account. * Including testing that user profile fields are correctly set. * * @covers \auth_oauth2\api::create_new_confirmed_account */ public function test_create_new_confirmed_account(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['alternatename'] = 'Beatles'; $info['idnumber'] = '123456'; $info['city'] = 'Melbourne'; $info['country'] = 'AU'; $info['institution'] = 'ACME Inc'; $info['department'] = 'Misc Explosives'; $createduser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); // Get actual user record from DB to check. $userdata = $DB->get_record('user', ['id' => $createduser->id]); // Confirm each value supplied from issuers is saved into the user record. foreach ($info as $key => $value) { $this->assertEquals($value, $userdata->$key); } // Explicitly test the user is confirmed. $this->assertEquals(1, $userdata->confirmed); } /** * Test auto-confirming linked logins. */ public function test_linked_logins(): void { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('google'); $user = $this->getDataGenerator()->create_user(); $this->setUser($user); $info = []; $info['username'] = 'banana'; $info['email'] = 'banana@example.com'; \auth_oauth2\api::link_login($info, $issuer, $user->id, false); // Try and match a user with a linked login. $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertEquals($user->id, $match->get('userid')); $linkedlogins = \auth_oauth2\api::get_linked_logins($user->id, $issuer); \auth_oauth2\api::delete_linked_login($linkedlogins[0]->get('id')); $match = \auth_oauth2\api::match_username_to_user('banana', $issuer); $this->assertFalse($match); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['url'] = 'http://apple.com/'; $info['alternamename'] = 'Beatles'; $newuser = \auth_oauth2\api::create_new_confirmed_account($info, $issuer); $match = \auth_oauth2\api::match_username_to_user('apple', $issuer); $this->assertEquals($newuser->id, $match->get('userid')); } /** * Test that we cannot deleted a linked login for another user */ public function test_delete_linked_login_other_user(): void { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('google'); $user = $this->getDataGenerator()->create_user(); api::link_login([ 'username' => 'banana', 'email' => 'banana@example.com', ], $issuer, $user->id); /** @var linked_login $linkedlogin */ $linkedlogin = api::get_linked_logins($user->id)[0]; // We are logged in as a different user, so cannot delete this. $this->expectException(\dml_missing_record_exception::class); api::delete_linked_login($linkedlogin->get('id')); } /** * Test that is_enabled correctly identifies when the plugin is enabled. */ public function test_is_enabled(): void { $this->resetAfterTest(); set_config('auth', 'manual,oauth2'); $this->assertTrue(\auth_oauth2\api::is_enabled()); } /** * Test that is_enabled correctly identifies when the plugin is disabled. */ public function test_is_enabled_disabled(): void { $this->resetAfterTest(); set_config('auth', 'manual'); $this->assertFalse(\auth_oauth2\api::is_enabled()); } /** * Test creating a user via the send confirm account email method. * Including testing that user profile fields are correctly set. * * @covers \auth_oauth2\api::send_confirm_account_email */ public function test_send_confirm_account_email(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $info = []; $info['username'] = 'apple'; $info['email'] = 'apple@example.com'; $info['firstname'] = 'Apple'; $info['lastname'] = 'Fruit'; $info['alternatename'] = 'Beatles'; $info['idnumber'] = '123456'; $info['city'] = 'Melbourne'; $info['country'] = 'AU'; $info['institution'] = 'ACME Inc'; $info['department'] = 'Misc Explosives'; $createduser = \auth_oauth2\api::send_confirm_account_email($info, $issuer); // Get actual user record from DB to check. $userdata = $DB->get_record('user', ['id' => $createduser->id]); // Confirm each value supplied from issuers is saved into the user record. foreach ($info as $key => $value) { $this->assertEquals($value, $userdata->$key); } // Explicitly test the user is not yet confirmed. $this->assertEquals(0, $userdata->confirmed); } /** * Test case for checking the email greetings in OAuth2 confirmation emails. */ public function test_email_greetings(): void { $this->resetAfterTest(); $this->setAdminUser(); $issuer = \core\oauth2\api::create_standard_issuer('microsoft'); $userinfo = []; $userinfo['username'] = 'apple'; $userinfo['email'] = 'apple@example.com'; $userinfo['firstname'] = 'Apple'; $userinfo['lastname'] = 'Fruit'; $sink = $this->redirectEmails(); // Make sure we are redirecting emails. \auth_oauth2\api::send_confirm_account_email($userinfo, $issuer); $result = $sink->get_messages(); $sink->close(); // Test greetings. $this->assertStringContainsString('Hi ' . $userinfo['firstname'], quoted_printable_decode($result[0]->body)); $userinfo = []; $userinfo['username'] = 'banana'; $userinfo['email'] = 'banana@example.com'; $userinfo['firstname'] = 'Banana'; $userinfo['lastname'] = 'Fruit'; $user = $this->getDataGenerator()->create_user(); $sink = $this->redirectEmails(); // Make sure we are redirecting emails. \auth_oauth2\api::send_confirm_link_login_email($userinfo, $issuer, $user->id); $result = $sink->get_messages(); $sink->close(); // Test greetings. $this->assertStringContainsString('Hi ' . $user->firstname, quoted_printable_decode($result[0]->body)); } } PKB\P!tests/behat/settings_test.featurenu[@auth @auth_oauth2 @javascript Feature: OAuth2 settings test functionality In order to use them later for authentication As an administrator I need to be able to test configured OAuth2 login services. Background: Given I log in as "admin" And I change window size to "large" Scenario: Test oAuth2 authentication settings with no configured service. Given I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Test settings" "link" in the "OAuth 2" "table_row" Then I should see "There are no configured OAuth2 providers" Scenario: Test oAuth2 authentication settings for a configured service. Given I navigate to "Server > OAuth 2 services" in site administration And I press "Google" And I set the following fields to these values: | Name | Testing service | | Client ID | thisistheclientid | | Client secret | supersecret | And I press "Save changes" And I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Test settings" "link" in the "OAuth 2" "table_row" Then I should see "Testing service" PKB\zXv= auth.phpnu[. /** * This file to proccess Oauth2 connects for backpack. * * @package core_badges * @subpackage badges * @copyright 2020 Tung Thai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Tung Thai */ namespace core_badges\oauth2; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); use stdClass; /** * Proccess Oauth2 connects to backpack site. * * @copyright 2020 Tung Thai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Tung Thai */ class auth extends \auth_oauth2\auth { /** * To complete data after login. * * @param client $client object. * @param string $redirecturl the url redirect. */ public function complete_data(\core_badges\oauth2\client $client, $redirecturl) { global $DB, $USER; $userinfo = $client->get_userinfo(); $badgebackpack = new stdClass(); $badgebackpack->userid = $USER->id; if ($userinfo && isset($userinfo->email)) { $badgebackpack->email = $userinfo->email; } else { $badgebackpack->email = $USER->email; } $badgebackpack->externalbackpackid = $client->backpack->id; $badgebackpack->backpackuid = 0; $badgebackpack->autosync = 0; $badgebackpack->password = ''; $record = $DB->get_record('badge_backpack', ['userid' => $USER->id, 'externalbackpackid' => $client->backpack->id]); if (!$record) { $DB->insert_record('badge_backpack', $badgebackpack); } else { $badgebackpack->id = $record->id; $DB->update_record('badge_backpack', $badgebackpack); } redirect($redirecturl, get_string('backpackconnected', 'badges'), null, \core\output\notification::NOTIFY_SUCCESS); } /** * Check user has been logged the backpack site. * * @param int $externalbackpackid ID of external backpack. * @param int $userid ID of user. * @return bool */ public static function is_logged_oauth2($externalbackpackid, $userid) { global $USER; if (empty($userid)) { $userid = $USER->id; } $persistedtoken = badge_backpack_oauth2::get_record(['externalbackpackid' => $externalbackpackid, 'userid' => $userid]); if ($persistedtoken) { return true; } return false; } } PKB\@+dGG settings.phpnu[. /** * Admin settings and defaults. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $warning = $OUTPUT->notification(get_string('createaccountswarning', 'auth_oauth2'), 'warning'); $settings->add(new admin_setting_heading('auth_oauth2/pluginname', '', $warning)); $authplugin = get_auth_plugin('oauth2'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false, $authplugin->customfields); } PK\sC!badge_backpack_oauth2.phpnu[. /** * This file contains the form add/update oauth2 for backpack is connected. * * @package core_badges * @subpackage badges * @copyright 2020 Tung Thai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Tung Thai */ namespace core_badges\oauth2; defined('MOODLE_INTERNAL') || die(); use core\persistent; /** * Class badge_backpack_oauth2 for backpack is connected. * * @copyright 2020 Tung Thai * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Tung Thai */ class badge_backpack_oauth2 extends persistent { /** * The table name. */ const TABLE = 'badge_backpack_oauth2'; /** * Return the definition of the properties of this model. * * @return array */ protected static function define_properties() { return array( 'userid' => array( 'type' => PARAM_INT, ), 'issuerid' => array( 'type' => PARAM_INT ), 'externalbackpackid' => array( 'type' => PARAM_INT ), 'token' => array( 'type' => PARAM_TEXT ), 'refreshtoken' => array( 'type' => PARAM_TEXT ), 'expires' => array( 'type' => PARAM_INT ), 'scope' => array( 'type' => PARAM_TEXT ), ); } }PK\/MuYY client.phpnu[. /** * 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 = []; // Replace the language tag if it appears in the string. $lang = current_language(); $tags = ["{lang}", "{LANG}", "{language}", "{LANGUAGE}", '{lan-guage}', '{LAN-GUAGE}']; $langcode = [ strtolower(substr($lang, 0, 2)), strtoupper(substr($lang, 0, 2)), strtolower($lang), strtoupper($lang), str_replace('_', '-', strtolower($lang)), str_replace('_', '-', strtoupper($lang)), ]; $params = str_replace($tags, $langcode, $params); 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; } } PK\ earefresh_system_tokens_task.phpnu[. /** * 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'); } } } PK\[P&W&Wapi.phpnu[. /** * 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; // Get email. if (isset($userinfo['email'])) { $record->email = $userinfo['email']; } else if ($issuer->get_system_email()) { $record->email = $issuer->get_system_email(); } else { $record->email = ''; } // Get username. if (isset($userinfo['username'])) { $record->username = $userinfo['username']; } else if ($issuer->get_system_email()) { $record->username = $issuer->get_system_email(); } $systemaccount = new system_account(0, $record); $systemaccount->create(); $client->log_out(); return true; } } PK\*0vvsystem_account.phpnu[. /** * 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, ) ); } } PK\Jservice/issuer_interface.phpnu[. 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; } PK\!a  service/clever.phpnu[. 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(); } } PK\"'RRservice/imsobv2p1.phpnu[. 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. } } PK\ " " service/linkedin.phpnu[. 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; } } PK\g"service/moodlenet.phpnu[. 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 * @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); } } } } } PK\Օservice/facebook.phpnu[. 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; } } PK\Z||service/custom.phpnu[. 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; } } PK\yk k service/microsoft.phpnu[. namespace core\oauth2\service; use core\oauth2\issuer; 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' => 'https://login.microsoftonline.com/common/v2.0', '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; } #[\Override] 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 field mappings. $mapping = [ 'sub' => 'idnumber', 'given_name' => 'firstname', 'family_name' => 'lastname', 'email' => 'email', 'displayName' => 'alternatename', 'officeLocation' => 'address', 'mobilePhone' => '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(); } } } PK\ߴservice/google.phpnu[. 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; } } PK\~T9 service/nextcloud.phpnu[. 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; } } PK\Uo"o" issuer.phpnu[. /** * 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 used for sending email using SMTP with XOAUTH2 */ const SMTPWITHXOAUTH2 = 3; /** @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, ), 'systememail' => [ 'type' => PARAM_EMAIL, '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('showonloginpage') != self::SMTPWITHXOAUTH2 && $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'); } /** * Get the system email address for this issuer. * * @return string|null The system email address or null if not set. */ public function get_system_email(): ?string { return $this->get('systememail') ? $this->get('systememail') : null; } } PK\Ruser_field_mapping.phpnu[. /** * 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; } } PK\ ~~client/linkedin.phpnu[. 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); } } PK\ocI I client/microsoft.phpnu[. namespace core\oauth2\client; use core\oauth2\client; use stdClass; /** * Custom oauth2 client for Microsoft to handle specific requirements. * * @package core * @copyright 2025 Huong Nguyen * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class microsoft extends client { #[\Override] public function get_additional_upgrade_token_parameters(): array { $issuer = $this->get_issuer(); if ($issuer->get('showonloginpage') == $issuer::SMTPWITHXOAUTH2) { // We are using this issuer for SMTP with XOAUTH2. // We need to add the SMTP scope to the token request. return [ 'scope' => 'https://outlook.office.com/SMTP.Send', ]; } return parent::get_additional_upgrade_token_parameters(); } #[\Override] protected function map_userinfo_to_fields(stdClass $userinfo): array { // Microsoft returns different field names depending on account type: // - Work/School accounts: OpenID Connect standard (given_name, family_name) // - Personal accounts: Non-standard lowercase (givenname, familyname) // We need to check both formats to support all Microsoft account types. // // Additionally, we provide bidirectional fallback to handle sites that have not yet // run the database upgrade to update field mappings from the old format to the new format. // Add fallback mappings for personal accounts if the standard fields are not present. if (empty($userinfo->given_name) && !empty($userinfo->givenname)) { $userinfo->given_name = $userinfo->givenname; } if (empty($userinfo->family_name) && !empty($userinfo->familyname)) { $userinfo->family_name = $userinfo->familyname; } // Add reverse fallback for sites with old database mappings (givenname/familyname). // This ensures work/school accounts work even before the database upgrade runs. if (empty($userinfo->givenname) && !empty($userinfo->given_name)) { $userinfo->givenname = $userinfo->given_name; } if (empty($userinfo->familyname) && !empty($userinfo->family_name)) { $userinfo->familyname = $userinfo->family_name; } // Call parent to handle the standard mapping. return parent::map_userinfo_to_fields($userinfo); } } PK\xrest.phpnu[. /** * 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); } } } PK\discovery/imsbadgeconnect.phpnu[. 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); } } } } } PK\'discovery/auth_server_config_reader.phpnu[. 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 * @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); } } PK\sf  discovery/base_definition.phpnu[. 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; } } PK\discovery/openidconnect.phpnu[. 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). } } PK\4rest_exception.phpnu[. /** * 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 { } PK\N'' endpoint.phpnu[. /** * 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; } } PK\N\q access_token.phpnu[. /** * 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 * @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 * @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, ), ); } } PKB\(rll db/access.phpnu[PKB\OVVdb/upgrade.phpnu[PKB\8= db/install.xmlnu[PKB\L db/events.phpnu[PKB\Ulang/en/auth_oauth2.phpnu[PKB\gfn 3login.phpnu[PKB\S" " p;test.phpnu[PKB\@Flib.phpnu[PKB\ Oversion.phpnu[PKB\t>u ATconfirm-linkedlogin.phpnu[PKB\1996aclasses/api.phpnu[PKB\MJ% Yclasses/privacy/provider.phpnu[PKB\Elclasses/linked_login.phpnu[PKB\ 8 8 classes/output/renderer.phpnu[PKB\=bhhclasses/auth.phpnu[PKB\Q)>templates/idps.mustachenu[PKB\墺EEFtemplates/idpresponse.mustachenu[PKB\9ppmNlinkedlogins.phpnu[PKB\7b `confirm-account.phpnu[PKB\z'--mtests/privacy/provider_test.phpnu[PKB\<ϛtests/auth_test.phpnu[PKB\?7((ǫtests/api_test.phpnu[PKB\P!tests/behat/settings_test.featurenu[PKB\zXv= 5auth.phpnu[PKB\@+dGG settings.phpnu[PK\sC!badge_backpack_oauth2.phpnu[PK\/MuYY client.phpnu[PK\ eacOrefresh_system_tokens_task.phpnu[PK\[P&W&W^api.phpnu[PK\*0vvsystem_account.phpnu[PK\Jֽservice/issuer_interface.phpnu[PK\!a   service/clever.phpnu[PK\"'RRgservice/imsobv2p1.phpnu[PK\ " " service/linkedin.phpnu[PK\g"dservice/moodlenet.phpnu[PK\Օ,service/facebook.phpnu[PK\Z|| service/custom.phpnu[PK\yk k service/microsoft.phpnu[PK\ߴservice/google.phpnu[PK\~T9 "service/nextcloud.phpnu[PK\Uo"o" 0issuer.phpnu[PK\R6Suser_field_mapping.phpnu[PK\ ~~bclient/linkedin.phpnu[PK\ocI I iclient/microsoft.phpnu[PK\x]vrest.phpnu[PK\discovery/imsbadgeconnect.phpnu[PK\'xdiscovery/auth_server_config_reader.phpnu[PK\sf  gdiscovery/base_definition.phpnu[PK\discovery/openidconnect.phpnu[PK\4 rest_exception.phpnu[PK\N'' endpoint.phpnu[PK\N\q ~access_token.phpnu[PK44