ÿØÿà 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-Ї Ç PK&9\bgT T test_settings.phpnu[. /** * Test auth settings. * * @package core_auth * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../config.php'); require_once("$CFG->libdir/adminlib.php"); $auth = optional_param('auth', '', PARAM_RAW); if (!core_component::is_valid_plugin_name('auth', $auth)) { $auth = ''; } else if (!file_exists("$CFG->dirroot/auth/$auth/auth.php")) { $auth = ''; } navigation_node::override_active_url(new moodle_url('/admin/settings.php', array('section'=>'manageauths'))); admin_externalpage_setup('authtestsettings'); $returnurl = new moodle_url('/admin/settings.php', array('section'=>'manageauths')); echo $OUTPUT->header(); if (!$auth) { $options = array(); $plugins = core_component::get_plugin_list('auth'); foreach ($plugins as $name => $fulldir) { $plugin = get_auth_plugin($name); if (!$plugin or !method_exists($plugin, 'test_settings')) { continue; } $options[$name] = get_string('pluginname', 'auth_'.$name); } if (!$options) { redirect($returnurl); } echo $OUTPUT->heading(get_string('testsettings', 'core_auth')); $url = new moodle_url('/auth/test_settings.php'); echo $OUTPUT->single_select($url, 'auth', $options); echo $OUTPUT->footer(); } $plugin = get_auth_plugin($auth); if (!$plugin or !method_exists($plugin, 'test_settings')) { redirect($returnurl); } echo $OUTPUT->heading(get_string('testsettingsheading', 'core_auth', get_string('pluginname', 'auth_'.$auth))); $plugin->test_settings(); echo $OUTPUT->continue_button($returnurl); echo $OUTPUT->footer(); PK&9\db/db/install.phpnu[. /** * auth_db installer script. * * @package auth_db * @copyright 2009 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ function xmldb_auth_db_install() { global $CFG, $DB; } PK&9\7Kdb/db/tasks.phpnu[. /** * Task definition for auth_db. * @author Guy Thomas * @copyright Copyright (c) 2017 Blackboard Inc. * @package auth_db * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => '\auth_db\task\sync_users', 'blocking' => 0, 'minute' => 'R', 'hour' => 'R', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); PK&9\RVVdb/db/upgrade.phpnu[. /** * DB authentication plugin upgrade code * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_db. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_db_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; } PK&9\!`]]db/lang/en/auth_db.phpnu[. /** * Strings for component 'auth_db', language 'en'. * * @package auth_db * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_dbcantconnect'] = 'Could not connect to the specified authentication database...'; $string['auth_dbdebugauthdb'] = 'Debug ADOdb'; $string['auth_dbdebugauthdbhelp'] = 'Debug ADOdb connection to external database - use when getting empty page during login. Not suitable for production sites.'; $string['auth_dbdeleteuser'] = 'Deleted user {$a->name} id {$a->id}'; $string['auth_dbdeleteusererror'] = 'Error deleting user {$a}'; $string['auth_dbdescription'] = 'This method uses an external database table to check whether a given username and password is valid. If the account is a new one, then information from other fields may also be copied across into Moodle.'; $string['auth_dbextencoding'] = 'External db encoding'; $string['auth_dbextencodinghelp'] = 'Encoding used in external database'; $string['auth_dbextrafields'] = 'These fields are optional. You can choose to pre-fill some Moodle user fields with information from the external database fields that you specify here.

If you leave these blank, then defaults will be used.

In either case, the user will be able to edit all of these fields after they log in.

'; $string['auth_dbfieldpass'] = 'Name of the field containing passwords'; $string['auth_dbfieldpass_key'] = 'Password field'; $string['auth_dbfielduser'] = 'Name of the field containing usernames. This field must be a varchar data type.'; $string['auth_dbfielduser_key'] = 'Username field'; $string['auth_dbhost'] = 'The computer hosting the database server. Use a system DSN entry if using ODBC. Use a PDO DSN entry if using PDO.'; $string['auth_dbhost_key'] = 'Host'; $string['auth_dbchangepasswordurl_key'] = 'Password-change URL'; $string['auth_dbinsertuser'] = 'Inserted user {$a->name} id {$a->id}'; $string['auth_dbinsertuserduplicate'] = 'Error inserting user {$a->username} - user with this username was already created through \'{$a->auth}\' plugin.'; $string['auth_dbinsertusererror'] = 'Error inserting user {$a}'; $string['auth_dbname'] = 'Name of the database itself. Leave empty if using an ODBC DSN. Leave empty if your PDO DSN already contains the database name.'; $string['auth_dbname_key'] = 'DB name'; $string['auth_dbpass'] = 'Password matching the above username'; $string['auth_dbpass_key'] = 'Password'; $string['auth_dbpasstype'] = '

Specify the format that the password field is using.

Use \'internal\' if you want the external database to manage usernames and email addresses, but Moodle to manage passwords. If you use \'internal\', you must provide a populated email address field in the external database, and you must enable the \auth_db\task\sync_users scheduled task. Moodle will send an email to new users with a temporary password.

'; $string['auth_dbpasstype_key'] = 'Password format'; $string['auth_dbreviveduser'] = 'Revived user {$a->name} id {$a->id}'; $string['auth_dbrevivedusererror'] = 'Error reviving user {$a}'; $string['auth_dbsaltedcrypt'] = 'Crypt one-way string hashing'; $string['auth_dbsetupsql'] = 'SQL setup command'; $string['auth_dbsetupsqlhelp'] = 'SQL command for special database setup, often used to setup communication encoding - example for MySQL and PostgreSQL: SET NAMES \'utf8\''; $string['auth_dbsuspenduser'] = 'Suspended user {$a->name} id {$a->id}'; $string['auth_dbsuspendusererror'] = 'Error suspending user {$a}'; $string['auth_dbsybasequoting'] = 'Use sybase quotes'; $string['auth_dbsybasequotinghelp'] = 'Sybase style single quote escaping - needed for MS SQL and some other databases. Do not use for MySQL!'; $string['auth_dbsyncuserstask'] = 'Synchronise users task'; $string['auth_dbtable'] = 'Name of the table in the database'; $string['auth_dbtable_key'] = 'Table'; $string['auth_dbtype'] = 'The database type (see the documentation ADOdb - Database Abstraction Layer for PHP for details).'; $string['auth_dbtype_key'] = 'Database'; $string['auth_dbupdateusers'] = 'Update users'; $string['auth_dbupdateusers_description'] = 'As well as inserting new users, update existing users.'; $string['auth_dbupdatinguser'] = 'Updating user {$a->name} id {$a->id}'; $string['auth_dbuser'] = 'Username with read access to the database'; $string['auth_dbuser_key'] = 'DB user'; $string['auth_dbuserstoadd'] = 'User entries to add: {$a}'; $string['auth_dbuserstoremove'] = 'User entries to remove: {$a}'; $string['auth_dbnoexttable'] = 'External table not specified.'; $string['auth_dbnouserfield'] = 'External user field not specified.'; $string['auth_dbcannotconnect'] = 'Cannot connect to external database.'; $string['auth_dbcannotreadtable'] = 'Cannot read external table.'; $string['auth_dbtableempty'] = 'External table is empty.'; $string['auth_dbcolumnlist'] = 'External table contains the following columns:
{$a}'; $string['auth_dbupdateerror'] = 'Error updating external database.'; $string['pluginname'] = 'External database'; $string['privacy:metadata'] = 'The External database authentication plugin does not store any personal data.'; PK&9\rdb/version.phpnu[. /** * Version details * * @package auth_db * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @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_db'; // Full name of the plugin (used for diagnostics) PK&9\n귲db/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_db. * * @package auth_db * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_db\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_db implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\3Pss4db/classes/admin_setting_special_auth_configtext.phpnu[. /** * Special settings for auth_db password_link. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special settings for auth_db password_link. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_db_admin_setting_special_auth_configtext extends admin_setting_configtext { /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { if (get_config('auth_db', 'passtype') === 'internal') { // We need to clear the auth_db change password link. $data = ''; } return parent::write_setting($data); } } PK&9\!b``db/classes/task/sync_users.phpnu[. namespace auth_db\task; /** * Sync users task class * @package auth_db * @author Guy Thomas * @copyright Copyright (c) 2017 Blackboard Inc. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_users extends \core\task\scheduled_task { /** * Name for this task. * * @return string */ public function get_name() { return get_string('auth_dbsyncuserstask', 'auth_db'); } /** * Run task for synchronising users. */ public function execute() { if (!is_enabled_auth('db')) { mtrace('auth_db plugin is disabled, synchronisation stopped'); return; } $dbauth = get_auth_plugin('db'); $config = get_config('auth_db'); $trace = new \text_progress_trace(); $update = !empty($config->updateusers); $dbauth->sync_users($trace, $update); } } PK&9\/] Cdb/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/db/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/db' to 'auth_db'. === 3.1 === * The auth_plugin_db::clean_data() has been deprecated and will be removed in a future version. Please update to use core_user::clean_data() instead. === 2.9 === Some alterations have been made to the handling of case sensitity handling of passwords and password hashes which previously varied depending on database configuration: * Plain text password matching is now always case sensitive * sha1/md5 hash comparisons are now enforced case insensitive (as underlying they are hexidecimal values) PK&9\ƌ  db/cli/sync_users.phpnu[. /** * Extdb user sync script. * * This script is meant to be called from a system cronjob to * sync moodle user accounts with external database. * It is required when using internal passwords (== passwords not defined in external database). * * Sample cron entry: * # 5 minutes past 4am * 5 4 * * * sudo -u www-data /usr/bin/php /var/www/moodle/auth/db/cli/sync_users.php * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d memory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * + The code is simpler, but not as optimized as its LDAP counterpart. * * @package auth_db * @copyright 2006 Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); require_once("$CFG->libdir/clilib.php"); // Now get cli options. list($options, $unrecognized) = cli_get_params(array('noupdate'=>false, 'verbose'=>false, 'help'=>false), array('n'=>'noupdate', 'v'=>'verbose', 'h'=>'help')); if ($unrecognized) { $unrecognized = implode("\n ", $unrecognized); cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } if ($options['help']) { $help = "Execute user account sync with external database. The auth_db plugin must be enabled and properly configured. Options: -n, --noupdate Skip update of existing users -v, --verbose Print verbose progress information -h, --help Print out this help Example: \$ sudo -u www-data /usr/bin/php auth/db/cli/sync_users.php Sample cron entry: # 5 minutes past 4am 5 4 * * * sudo -u www-data /usr/bin/php /var/www/moodle/auth/db/cli/sync_users.php "; echo $help; die; } if (!is_enabled_auth('db')) { cli_error('auth_db plugin is disabled, synchronisation stopped', 2); } cli_problem('[AUTH DB] The sync users cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the \auth_db\task\sync_users is enabled. $task = \core\task\manager::get_scheduled_task('auth_db\task\sync_users'); if (!$task->get_disabled()) { cli_error('[AUTH DB] The scheduled task sync_users is enabled, the cron execution has been aborted.'); } if (empty($options['verbose'])) { $trace = new null_progress_trace(); } else { $trace = new text_progress_trace(); } $update = empty($options['noupdate']); /** @var auth_plugin_db $dbauth */ $dbauth = get_auth_plugin('db'); $result = $dbauth->sync_users($trace, $update); exit($result); PK&9\H,5\\db/tests/db_test.phpnu[. namespace auth_db; /** * External database auth sync tests, this also tests adodb drivers * that are matching our four supported Moodle database drivers. * * @package auth_db * @category phpunit * @copyright 2012 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class db_test extends \advanced_testcase { /** @var string Original error log */ protected $oldlog; /** @var int The amount of users to create for the large user set deletion test */ protected $largedeletionsetsize = 128; public static function tearDownAfterClass(): void { global $DB; // Apply sqlsrv native driver error and logging default // settings while finishing the AdoDB tests. if ($DB->get_dbfamily() === 'mssql') { sqlsrv_configure("WarningsReturnAsErrors", false); sqlsrv_configure("LogSubsystems", SQLSRV_LOG_SYSTEM_OFF); sqlsrv_configure("LogSeverity", SQLSRV_LOG_SEVERITY_ERROR); } parent::tearDownAfterClass(); } protected function init_auth_database() { global $DB, $CFG; require_once("$CFG->dirroot/auth/db/auth.php"); // Discard error logs from AdoDB. $this->oldlog = ini_get('error_log'); ini_set('error_log', "$CFG->dataroot/testlog.log"); $dbman = $DB->get_manager(); set_config('extencoding', 'utf-8', 'auth_db'); set_config('host', $CFG->dbhost, 'auth_db'); set_config('user', $CFG->dbuser, 'auth_db'); set_config('pass', $CFG->dbpass, 'auth_db'); set_config('name', $CFG->dbname, 'auth_db'); if (!empty($CFG->dboptions['dbport'])) { set_config('host', $CFG->dbhost.':'.$CFG->dboptions['dbport'], 'auth_db'); } switch ($DB->get_dbfamily()) { case 'mysql': set_config('type', 'mysqli', 'auth_db'); set_config('setupsql', "SET NAMES 'UTF-8'", 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket'])) { $dbsocket = $CFG->dboptions['dbsocket']; if ((strpos($dbsocket, '/') === false and strpos($dbsocket, '\\') === false)) { $dbsocket = ini_get('mysqli.default_socket'); } set_config('type', 'mysqli://'.rawurlencode($CFG->dbuser).':'.rawurlencode($CFG->dbpass).'@'.rawurlencode($CFG->dbhost).'/'.rawurlencode($CFG->dbname).'?socket='.rawurlencode($dbsocket), 'auth_db'); } break; case 'postgres': set_config('type', 'postgres7', 'auth_db'); $setupsql = "SET NAMES 'UTF-8'"; if (!empty($CFG->dboptions['dbschema'])) { $setupsql .= "; SET search_path = '".$CFG->dboptions['dbschema']."'"; } set_config('setupsql', $setupsql, 'auth_db'); set_config('sybasequoting', '0', 'auth_db'); if (!empty($CFG->dboptions['dbsocket']) and ($CFG->dbhost === 'localhost' or $CFG->dbhost === '127.0.0.1')) { if (strpos($CFG->dboptions['dbsocket'], '/') !== false) { $socket = $CFG->dboptions['dbsocket']; if (!empty($CFG->dboptions['dbport'])) { $socket .= ':' . $CFG->dboptions['dbport']; } set_config('host', $socket, 'auth_db'); } else { set_config('host', '', 'auth_db'); } } break; case 'mssql': set_config('type', 'mssqlnative', 'auth_db'); set_config('sybasequoting', '1', 'auth_db'); // The native sqlsrv driver uses a comma as separator between host and port. $dbhost = $CFG->dbhost; if (!empty($dboptions['dbport'])) { $dbhost .= ',' . $dboptions['dbport']; } set_config('host', $dbhost, 'auth_db'); break; default: throw new exception('Unknown database family ' . $DB->get_dbfamily()); } $table = new \xmldb_table('auth_db_users'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('name', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('pass', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('email', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('firstname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('lastname', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_field('animal', XMLDB_TYPE_CHAR, '255', null, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); if ($dbman->table_exists($table)) { $dbman->drop_table($table); } $dbman->create_table($table); set_config('table', $CFG->prefix.'auth_db_users', 'auth_db'); set_config('fielduser', 'name', 'auth_db'); set_config('fieldpass', 'pass', 'auth_db'); set_config('field_map_lastname', 'lastname', 'auth_db'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_db'); set_config('field_lock_lastname', 'unlocked', 'auth_db'); // Setu up field mappings. set_config('field_map_email', 'email', 'auth_db'); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); set_config('field_updateremote_email', '0', 'auth_db'); set_config('field_lock_email', 'unlocked', 'auth_db'); // Create a user profile field and add mapping to it. $this->getDataGenerator()->create_custom_profile_field(['shortname' => 'pet', 'name' => 'Pet', 'datatype' => 'text']); set_config('field_map_profile_field_pet', 'animal', 'auth_db'); set_config('field_updatelocal_profile_field_pet', 'oncreate', 'auth_db'); set_config('field_updateremote_profile_field_pet', '0', 'auth_db'); set_config('field_lock_profile_field_pet', 'unlocked', 'auth_db'); // Init the rest of settings. set_config('passtype', 'plaintext', 'auth_db'); set_config('changepasswordurl', '', 'auth_db'); set_config('debugauthdb', 0, 'auth_db'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); } protected function cleanup_auth_database() { global $DB; $dbman = $DB->get_manager(); $table = new \xmldb_table('auth_db_users'); $dbman->drop_table($table); ini_set('error_log', $this->oldlog); } public function test_plugin(): void { global $DB, $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); $this->resetAfterTest(true); // NOTE: It is strongly discouraged to create new tables in advanced_testcase classes, // but there is no other simple way to test ext database enrol sync, so let's // disable transactions are try to cleanup after the tests. $this->preventResetByRollback(); $this->init_auth_database(); /** @var auth_plugin_db $auth */ $auth = get_auth_plugin('db'); $authdb = $auth->db_init(); // Test adodb may access the table. $user1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $user1->id = $DB->insert_record('auth_db_users', $user1); $sql = "SELECT * FROM {$auth->config->table}"; $rs = $authdb->Execute($sql); $this->assertInstanceOf('ADORecordSet', $rs); $this->assertFalse($rs->EOF); $fields = $rs->FetchRow(); $this->assertTrue(is_array($fields)); $this->assertTrue($rs->EOF); $rs->Close(); $authdb->Close(); // Test bulk user account creation. $user2 = (object)['name' => 'u2', 'pass' => 'heslo', 'email' => 'u2@example.com', 'animal' => 'cat']; $user2->id = $DB->insert_record('auth_db_users', $user2); $user3 = (object)array('name'=>'admin', 'pass'=>'heslo', 'email'=>'admin@example.com'); // Should be skipped. $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertCount(2, $DB->get_records('user')); $trace = new \null_progress_trace(); // Sync users and make sure that two events user_created werer triggered. $sink = $this->redirectEvents(); $auth->sync_users($trace, false); $events = $sink->get_events(); $sink->close(); $this->assertCount(2, $events); $this->assertTrue($events[0] instanceof \core\event\user_created); $this->assertTrue($events[1] instanceof \core\event\user_created); // Assert the two users were created. $this->assertEquals(4, $DB->count_records('user')); $u1 = $DB->get_record('user', array('username'=>$user1->name, 'auth'=>'db')); $this->assertSame($user1->email, $u1->email); $this->assertEmpty(profile_user_record($u1->id)->pet); $u2 = $DB->get_record('user', array('username'=>$user2->name, 'auth'=>'db')); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $admin = $DB->get_record('user', array('username'=>'admin', 'auth'=>'manual')); $this->assertNotEmpty($admin); // Test sync updates. $user2b = clone($user2); $user2b->email = 'u2b@example.com'; $user2b->animal = 'dog'; $DB->update_record('auth_db_users', $user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $this->assertSame($user2->animal, profile_user_record($u2->id)->pet); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; set_config('field_updatelocal_profile_field_pet', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_profile_field_pet = 'onlogin'; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2->email, $u2->email); $auth->sync_users($trace, true); $this->assertEquals(4, $DB->count_records('user')); $u2 = $DB->get_record('user', array('username'=>$user2->name)); $this->assertSame($user2b->email, $u2->email); $this->assertSame($user2b->animal, profile_user_record($u2->id)->pet); // Test sync deletes and suspends. $DB->delete_records('auth_db_users', array('id'=>$user2->id)); $this->assertCount(2, $DB->get_records('auth_db_users')); unset($user2); unset($user2b); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $DB->delete_records('auth_db_users', array('id'=>$user2->id)); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $auth->sync_users($trace, false); $this->assertEquals(4, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $user2 = (object)array('name'=>'u2', 'pass'=>'heslo', 'email'=>'u2@example.com'); $user2->id = $DB->insert_record('auth_db_users', $user2); $auth->sync_users($trace, false); $this->assertEquals(5, $DB->count_records('user')); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); // Test user_login(). $user3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com'); $user3->id = $DB->insert_record('auth_db_users', $user3); $this->assertFalse($auth->user_login('u4', 'heslo')); $this->assertTrue($auth->user_login('u1', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); $this->assertTrue($auth->user_login('u3', 'heslo')); $this->assertFalse($DB->record_exists('user', array('username'=>'u3', 'auth'=>'db'))); set_config('passtype', 'md5', 'auth_db'); $auth->config->passtype = 'md5'; $user3->pass = md5('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usermd5 = (object)['name' => 'usermd5', 'pass' => '0e462097431906509019562988736854']; $usermd5->id = $DB->insert_record('auth_db_users', $usermd5); // md5('240610708') === '0e462097431906509019562988736854'. $this->assertTrue($auth->user_login('usermd5', '240610708')); $this->assertFalse($auth->user_login('usermd5', 'QNKCDZO')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $user3->pass = sha1('heslo'); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); // Test user created to see if the checking happens strictly. $usersha1 = (object)['name' => 'usersha1', 'pass' => '0e66507019969427134894567494305185566735']; $usersha1->id = $DB->insert_record('auth_db_users', $usersha1); // sha1('aaroZmOk') === '0e66507019969427134894567494305185566735'. $this->assertTrue($auth->user_login('usersha1', 'aaroZmOk')); $this->assertFalse($auth->user_login('usersha1', 'aaK1STfY')); set_config('passtype', 'saltedcrypt', 'auth_db'); $auth->config->passtype = 'saltedcrypt'; $user3->pass = password_hash('heslo', PASSWORD_BCRYPT); $DB->update_record('auth_db_users', $user3); $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('passtype', 'internal', 'auth_db'); $auth->config->passtype = 'internal'; create_user_record('u3', 'heslo', 'db'); $this->assertTrue($auth->user_login('u3', 'heslo')); $DB->delete_records('auth_db_users', array('id'=>$user3->id)); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_KEEP; $this->assertTrue($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_SUSPEND; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; $this->assertFalse($auth->user_login('u3', 'heslo')); set_config('passtype', 'sh1', 'auth_db'); $auth->config->passtype = 'sha1'; $this->assertFalse($auth->user_login('u3', 'heslo')); // Test login create and update. $user4 = (object)array('name'=>'u4', 'pass'=>'heslo', 'email'=>'u4@example.com'); $user4->id = $DB->insert_record('auth_db_users', $user4); set_config('passtype', 'plaintext', 'auth_db'); $auth->config->passtype = 'plaintext'; $iuser4 = create_user_record('u4', 'heslo', 'db'); $this->assertNotEmpty($iuser4); $this->assertSame($user4->name, $iuser4->username); $this->assertSame($user4->email, $iuser4->email); $this->assertSame('db', $iuser4->auth); $this->assertSame($CFG->mnet_localhost_id, $iuser4->mnethostid); $user4b = clone($user4); $user4b->email = 'u4b@example.com'; $DB->update_record('auth_db_users', $user4b); set_config('field_updatelocal_email', 'oncreate', 'auth_db'); $auth->config->field_updatelocal_email = 'oncreate'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4->email, $iuser4->email); set_config('field_updatelocal_email', 'onlogin', 'auth_db'); $auth->config->field_updatelocal_email = 'onlogin'; update_user_record('u4'); $iuser4 = $DB->get_record('user', array('id'=>$iuser4->id)); $this->assertSame($user4b->email, $iuser4->email); // Test user_exists() $this->assertTrue($auth->user_exists('u1')); $this->assertTrue($auth->user_exists('admin')); $this->assertFalse($auth->user_exists('u3')); $this->assertTrue($auth->user_exists('u4')); $this->cleanup_auth_database(); } /** * Testing the function _colonscope() from ADOdb. */ public function test_adodb_colonscope(): void { global $CFG; require_once($CFG->libdir.'/adodb/adodb.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-odbc.inc.php'); require_once($CFG->libdir.'/adodb/drivers/adodb-db2ora.inc.php'); $this->resetAfterTest(false); $sql = "select * from table WHERE column=:1 AND anothercolumn > :0"; $arr = array('b', 1); list($sqlout, $arrout) = _colonscope($sql,$arr); $this->assertEquals("select * from table WHERE column=? AND anothercolumn > ?", $sqlout); $this->assertEquals(array(1, 'b'), $arrout); } /** * Testing the clean_data() method. */ public function test_clean_data(): void { global $DB; $this->resetAfterTest(false); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Create users on external table. $extdbuser1 = (object)array('name'=>'u1', 'pass'=>'heslo', 'email'=>'u1@example.com'); $extdbuser1->id = $DB->insert_record('auth_db_users', $extdbuser1); // User with malicious data on the name (won't be imported). $extdbuser2 = (object)array('name'=>'userxss', 'pass'=>'heslo', 'email'=>'xssuser@example.com'); $extdbuser2->id = $DB->insert_record('auth_db_users', $extdbuser2); $extdbuser3 = (object)array('name'=>'u3', 'pass'=>'heslo', 'email'=>'u3@example.com', 'lastname' => 'userxss'); $extdbuser3->id = $DB->insert_record('auth_db_users', $extdbuser3); $trace = new \null_progress_trace(); // Let's test user sync make sure still works as expected.. $auth->sync_users($trace, true); $this->assertDebuggingCalled("The property 'lastname' has invalid data and has been cleaned."); // User with correct data, should be equal to external db. $user1 = $DB->get_record('user', array('email'=> $extdbuser1->email, 'auth'=>'db')); $this->assertEquals($extdbuser1->name, $user1->username); $this->assertEquals($extdbuser1->email, $user1->email); // Get the user on moodle user table. $user2 = $DB->get_record('user', array('email'=> $extdbuser2->email, 'auth'=>'db')); $user3 = $DB->get_record('user', array('email'=> $extdbuser3->email, 'auth'=>'db')); $this->assertEmpty($user2); $this->assertEquals($extdbuser3->name, $user3->username); $this->assertEquals('useralert(1);xss', $user3->lastname); $this->cleanup_auth_database(); } /** * Testing the deletion of a user when there are many users in the external DB. */ public function test_deleting_with_many_users(): void { global $DB; $this->resetAfterTest(true); $this->preventResetByRollback(); $this->init_auth_database(); $auth = get_auth_plugin('db'); $auth->db_init(); // Set to delete from moodle when missing from DB. set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_db'); $auth->config->removeuser = AUTH_REMOVEUSER_FULLDELETE; // Create users. $users = []; for ($i = 0; $i < $this->largedeletionsetsize; $i++) { $user = (object)array('username' => "u$i", 'name' => "u$i", 'pass' => 'heslo', 'email' => "u$i@example.com"); $user->id = $DB->insert_record('auth_db_users', $user); $users[] = $user; } // Sync to moodle. $trace = new \null_progress_trace(); $auth->sync_users($trace, true); // Check user is there. $user = array_shift($users); $moodleuser = $DB->get_record('user', array('email' => $user->email, 'auth' => 'db')); $this->assertNotNull($moodleuser); $this->assertEquals($user->username, $moodleuser->username); // Delete a user. $DB->delete_records('auth_db_users', array('id' => $user->id)); // Sync again. $auth->sync_users($trace, true); // Check user is no longer there. $moodleuser = $DB->get_record('user', array('id' => $moodleuser->id)); $this->assertFalse($auth->user_login($user->username, 'heslo')); $this->assertEquals(1, $moodleuser->deleted); // Make sure it was the only user deleted. $numberdeleted = $DB->count_records('user', array('deleted' => 1, 'auth' => 'db')); $this->assertEquals(1, $numberdeleted); $this->cleanup_auth_database(); } } PK&9\1%Đtt db/auth.phpnu[. /** * Authentication Plugin: External Database Authentication * * Checks against an external database. * * @package auth_db * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * External database authentication plugin. */ class auth_plugin_db extends auth_plugin_base { /** * Constructor. */ function __construct() { global $CFG; require_once($CFG->libdir.'/adodb/adodb.inc.php'); $this->authtype = 'db'; $this->config = get_config('auth_db'); $this->errorlogtag = '[AUTH DB] '; if (empty($this->config->extencoding)) { $this->config->extencoding = 'utf-8'; } } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { global $CFG, $DB; if ($this->is_configured() === false) { debugging(get_string('auth_notconfigured', 'auth', $this->authtype)); return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $extpassword = core_text::convert($password, 'utf-8', $this->config->extencoding); if ($this->is_internal()) { // Lookup username externally, but resolve // password locally -- to support backend that // don't track passwords. if (isset($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_KEEP) { // No need to connect to external database in this case because users are never removed and we verify password locally. if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype))) { return validate_internal_user_password($user, $password); } else { return false; } } $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"); if (!$rs) { $authdb->Close(); debugging(get_string('auth_dbcantconnect','auth_db')); return false; } if (!$rs->EOF) { $rs->Close(); $authdb->Close(); // User exists externally - check username/password internally. if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype))) { return validate_internal_user_password($user, $password); } } else { $rs->Close(); $authdb->Close(); // User does not exist externally. return false; } } else { // Normal case: use external db for both usernames and passwords. $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT {$this->config->fieldpass} FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"); if (!$rs) { $authdb->Close(); debugging(get_string('auth_dbcantconnect','auth_db')); return false; } if ($rs->EOF) { $authdb->Close(); return false; } $fields = array_change_key_case($rs->fields, CASE_LOWER); $fromdb = $fields[strtolower($this->config->fieldpass)]; $rs->Close(); $authdb->Close(); if ($this->config->passtype === 'plaintext') { return ($fromdb === $extpassword); } else if ($this->config->passtype === 'md5') { return (strtolower($fromdb) === md5($extpassword)); } else if ($this->config->passtype === 'sha1') { return (strtolower($fromdb) === sha1($extpassword)); } else if ($this->config->passtype === 'saltedcrypt') { return password_verify($extpassword, $fromdb); } else { return false; } } } /** * Connect to external database. * * @return ADOConnection * @throws moodle_exception */ function db_init() { if ($this->is_configured() === false) { throw new moodle_exception('auth_dbcantconnect', 'auth_db'); } // Connect to the external database (forcing new connection). $authdb = ADONewConnection($this->config->type); if (!empty($this->config->debugauthdb)) { $authdb->debug = true; ob_start(); //Start output buffer to allow later use of the page headers. } $authdb->Connect($this->config->host, $this->config->user, $this->config->pass, $this->config->name, true); $authdb->SetFetchMode(ADODB_FETCH_ASSOC); if (!empty($this->config->setupsql)) { $authdb->Execute($this->config->setupsql); } return $authdb; } /** * Returns user attribute mappings between moodle and the external database. * * @return array */ function db_attributes() { $moodleattributes = array(); // If we have custom fields then merge them with user fields. $customfields = $this->get_custom_user_profile_fields(); if (!empty($customfields) && !empty($this->userfields)) { $userfields = array_merge($this->userfields, $customfields); } else { $userfields = $this->userfields; } foreach ($userfields as $field) { if (!empty($this->config->{"field_map_$field"})) { $moodleattributes[$field] = $this->config->{"field_map_$field"}; } } $moodleattributes['username'] = $this->config->fielduser; return $moodleattributes; } /** * Reads any other information for a user from external database, * then returns it in an array. * * @param string $username * @return array */ function get_userinfo($username) { global $CFG; $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); // Array to map local fieldnames we want, to external fieldnames. $selectfields = $this->db_attributes(); $result = array(); // If at least one field is mapped from external db, get that mapped data. if ($selectfields) { $select = array(); $fieldcount = 0; foreach ($selectfields as $localname=>$externalname) { // Without aliasing, multiple occurrences of the same external // name can coalesce in only occurrence in the result. $select[] = "$externalname AS F".$fieldcount; $fieldcount++; } $select = implode(', ', $select); $sql = "SELECT $select FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."'"; if ($rs = $authdb->Execute($sql)) { if (!$rs->EOF) { $fields = $rs->FetchRow(); // Convert the associative array to an array of its values so we don't have to worry about the case of its keys. $fields = array_values($fields); foreach (array_keys($selectfields) as $index => $localname) { $value = $fields[$index]; $result[$localname] = core_text::convert($value, $this->config->extencoding, 'utf-8'); } } $rs->Close(); } } $authdb->Close(); return $result; } /** * Change a user's password. * * @param stdClass $user User table object * @param string $newpassword Plaintext password * @return bool True on success */ function user_update_password($user, $newpassword) { global $DB; if ($this->is_internal()) { $puser = $DB->get_record('user', array('id'=>$user->id), '*', MUST_EXIST); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). if (update_internal_user_password($puser, $newpassword)) { $user->password = $puser->password; return true; } else { return false; } } else { // We should have never been called! return false; } } /** * Synchronizes user from external db to moodle user table. * * Sync should be done by using idnumber attribute, not username. * You need to pass firstsync parameter to function to fill in * idnumbers if they don't exists in moodle user table. * * Syncing users removes (disables) users that don't exists anymore in external db. * Creates new users and updates coursecreator status of users. * * This implementation is simpler but less scalable than the one found in the LDAP module. * * @param progress_trace $trace * @param bool $do_updates Optional: set to true to force an update of existing accounts * @return int 0 means success, 1 means failure */ function sync_users(progress_trace $trace, $do_updates=false) { global $CFG, $DB; require_once($CFG->dirroot . '/user/lib.php'); // List external users. $userlist = $this->get_userlist(); // Delete obsolete internal users. if (!empty($this->config->removeuser)) { $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } // Find obsolete users. if (count($userlist)) { $removeusers = array(); $params['authtype'] = $this->authtype; $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect"; $params['mnethostid'] = $CFG->mnet_localhost_id; $internalusersrs = $DB->get_recordset_sql($sql, $params); $usernamelist = array_flip($userlist); foreach ($internalusersrs as $internaluser) { if (!array_key_exists($internaluser->username, $usernamelist)) { $removeusers[] = $internaluser; } } $internalusersrs->close(); } else { $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid $suspendselect"; $params = array(); $params['authtype'] = $this->authtype; $params['mnethostid'] = $CFG->mnet_localhost_id; $removeusers = $DB->get_records_sql($sql, $params); } if (!empty($removeusers)) { $trace->output(get_string('auth_dbuserstoremove', 'auth_db', count($removeusers))); foreach ($removeusers as $user) { if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { delete_user($user); $trace->output(get_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } } } unset($removeusers); } if (!count($userlist)) { // Exit right here, nothing else to do. $trace->finished(); return 0; } // Update existing accounts. if ($do_updates) { // Narrow down what fields we need to update. $all_keys = array_keys(get_object_vars($this->config)); $updatekeys = array(); foreach ($all_keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/',$key, $match)) { if ($this->config->{$key} === 'onlogin') { array_push($updatekeys, $match[1]); // The actual key name. } } } unset($all_keys); unset($key); // Only go ahead if we actually have fields to update locally. if (!empty($updatekeys)) { $update_users = array(); // All the drivers can cope with chunks of 10,000. See line 4491 of lib/dml/tests/dml_est.php $userlistchunks = array_chunk($userlist , 10000); foreach($userlistchunks as $userlistchunk) { list($in_sql, $params) = $DB->get_in_or_equal($userlistchunk, SQL_PARAMS_NAMED, 'u', true); $params['authtype'] = $this->authtype; $params['mnethostid'] = $CFG->mnet_localhost_id; $sql = "SELECT u.id, u.username, u.suspended FROM {user} u WHERE u.auth = :authtype AND u.deleted = 0 AND u.mnethostid = :mnethostid AND u.username {$in_sql}"; $update_users = $update_users + $DB->get_records_sql($sql, $params); } if ($update_users) { $trace->output("User entries to update: ".count($update_users)); foreach ($update_users as $user) { if ($this->update_user_record($user->username, $updatekeys, false, (bool) $user->suspended)) { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)), 1); } else { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id))." - ".get_string('skipped'), 1); } } unset($update_users); } } } // Create missing accounts. // NOTE: this is very memory intensive and generally inefficient. $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } $sql = "SELECT u.id, u.username FROM {user} u WHERE u.auth=:authtype AND u.deleted='0' AND mnethostid=:mnethostid $suspendselect"; $users = $DB->get_records_sql($sql, array('authtype'=>$this->authtype, 'mnethostid'=>$CFG->mnet_localhost_id)); // Simplify down to usernames. $usernames = array(); if (!empty($users)) { foreach ($users as $user) { array_push($usernames, $user->username); } unset($users); } $add_users = array_diff($userlist, $usernames); unset($usernames); if (!empty($add_users)) { $trace->output(get_string('auth_dbuserstoadd','auth_db',count($add_users))); // Do not use transactions around this foreach, we want to skip problematic users, not revert everything. foreach($add_users as $user) { $username = $user; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { if ($olduser = $DB->get_record('user', array('username' => $username, 'deleted' => 0, 'suspended' => 1, 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => $this->authtype))) { $updateuser = new stdClass(); $updateuser->id = $olduser->id; $updateuser->suspended = 0; user_update_user($updateuser); $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username, 'id' => $olduser->id)), 1); continue; } } // Do not try to undelete users here, instead select suspending if you ever expect users will reappear. // Prep a few params. $user = $this->get_userinfo_asobj($user); $user->username = $username; $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username'=>$user->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'auth'=>$this->authtype), 'id,username,auth')) { $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username'=>$user->username, 'auth'=>$collision->auth)), 1); continue; } try { $id = user_create_user($user, false, false); // It is truly a new user. $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)), 1); } catch (moodle_exception $e) { $trace->output(get_string('auth_dbinsertusererror', 'auth_db', $user->username), 1); continue; } // If relevant, tag for password generation. if ($this->is_internal()) { set_user_preference('auth_forcepasswordchange', 1, $id); set_user_preference('create_password', 1, $id); } // Save custom profile fields here. require_once($CFG->dirroot . '/user/profile/lib.php'); $user->id = $id; profile_save_data($user); // Make sure user context is present. context_user::instance($id); \core\event\user_created::create_from_userid($id)->trigger(); } unset($add_users); } $trace->finished(); return 0; } function user_exists($username) { // Init result value. $result = false; $extusername = core_text::convert($username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); $rs = $authdb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} = '".$this->ext_addslashes($extusername)."' "); if (!$rs) { throw new \moodle_exception('auth_dbcantconnect', 'auth_db'); } else if (!$rs->EOF) { // User exists externally. $result = true; } $authdb->Close(); return $result; } function get_userlist() { // Init result value. $result = array(); $authdb = $this->db_init(); // Fetch userlist. $rs = $authdb->Execute("SELECT {$this->config->fielduser} FROM {$this->config->table} "); if (!$rs) { throw new \moodle_exception('auth_dbcantconnect', 'auth_db'); } else if (!$rs->EOF) { while ($rec = $rs->FetchRow()) { $rec = array_change_key_case((array)$rec, CASE_LOWER); array_push($result, $rec[strtolower($this->config->fielduser)]); } } $authdb->Close(); return $result; } /** * Reads user information from DB and return it in an object. * * @param string $username username * @return stdClass */ function get_userinfo_asobj($username) { $user_array = truncate_userinfo($this->get_userinfo($username)); $user = new stdClass(); foreach($user_array as $key=>$value) { $user->{$key} = $value; } return $user; } /** * Called when the user record is updated. * Modifies user in external database. It takes olduser (before changes) and newuser (after changes) * compares information saved modified information to external db. * * @param stdClass $olduser Userobject before modifications * @param stdClass $newuser Userobject new modified userobject * @return boolean result * */ function user_update($olduser, $newuser) { if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { error_log("ERROR:User renaming not allowed in ext db"); return false; } if (isset($olduser->auth) and $olduser->auth != $this->authtype) { return true; // Just change auth and skip update. } $curruser = $this->get_userinfo($olduser->username); if (empty($curruser)) { error_log("ERROR:User $olduser->username found in ext db"); return false; } $extusername = core_text::convert($olduser->username, 'utf-8', $this->config->extencoding); $authdb = $this->db_init(); $update = array(); foreach($curruser as $key=>$value) { if ($key == 'username') { continue; // Skip this. } if (empty($this->config->{"field_updateremote_$key"})) { continue; // Remote update not requested. } if (!isset($newuser->$key)) { continue; } $nuvalue = $newuser->$key; // Support for textarea fields. if (isset($nuvalue['text'])) { $nuvalue = $nuvalue['text']; } if ($nuvalue != $value) { $update[] = $this->config->{"field_map_$key"}."='".$this->ext_addslashes(core_text::convert($nuvalue, 'utf-8', $this->config->extencoding))."'"; } } if (!empty($update)) { $sql = "UPDATE {$this->config->table} SET ".implode(',', $update)." WHERE {$this->config->fielduser} = ?"; if (!$authdb->Execute($sql, array($this->ext_addslashes($extusername)))) { throw new \moodle_exception('auth_dbupdateerror', 'auth_db'); } } $authdb->Close(); return true; } function prevent_local_passwords() { return !$this->is_internal(); } /** * Returns true if this authentication plugin is "internal". * * Internal plugins use password hashes from Moodle user table for authentication. * * @return bool */ function is_internal() { if (!isset($this->config->passtype)) { return true; } return ($this->config->passtype === 'internal'); } /** * Returns false if this plugin is enabled but not configured. * * @return bool */ public function is_configured() { if (!empty($this->config->type)) { return true; } 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 */ function is_synchronised_with_external() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return ($this->is_internal() or !empty($this->config->changepasswordurl)); } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { if ($this->is_internal() || empty($this->config->changepasswordurl)) { // Standard form. return null; } else { // Use admin defined custom url. return new moodle_url($this->config->changepasswordurl); } } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return $this->is_internal(); } /** * Add slashes, we can not use placeholders or system functions. * * @param string $text * @return string */ function ext_addslashes($text) { if (empty($this->config->sybasequoting)) { $text = str_replace('\\', '\\\\', $text); $text = str_replace(array('\'', '"', "\0"), array('\\\'', '\\"', '\\0'), $text); } else { $text = str_replace("'", "''", $text); } return $text; } /** * Test if settings are ok, print info to output. * @private */ public function test_settings() { global $CFG, $OUTPUT; // NOTE: this is not localised intentionally, admins are supposed to understand English at least a bit... raise_memory_limit(MEMORY_HUGE); if (empty($this->config->table)) { echo $OUTPUT->notification(get_string('auth_dbnoexttable', 'auth_db'), 'notifyproblem'); return; } if (empty($this->config->fielduser)) { echo $OUTPUT->notification(get_string('auth_dbnouserfield', 'auth_db'), 'notifyproblem'); return; } $olddebug = $CFG->debug; $olddisplay = ini_get('display_errors'); ini_set('display_errors', '1'); $CFG->debug = DEBUG_DEVELOPER; $olddebugauthdb = $this->config->debugauthdb; $this->config->debugauthdb = 1; error_reporting($CFG->debug); $adodb = $this->db_init(); if (!$adodb or !$adodb->IsConnected()) { $this->config->debugauthdb = $olddebugauthdb; $CFG->debug = $olddebug; ini_set('display_errors', $olddisplay); error_reporting($CFG->debug); ob_end_flush(); echo $OUTPUT->notification(get_string('auth_dbcannotconnect', 'auth_db'), 'notifyproblem'); return; } $rs = $adodb->Execute("SELECT * FROM {$this->config->table} WHERE {$this->config->fielduser} <> 'random_unlikely_username'"); // Any unlikely name is ok here. if (!$rs) { echo $OUTPUT->notification(get_string('auth_dbcannotreadtable', 'auth_db'), 'notifyproblem'); } else if ($rs->EOF) { echo $OUTPUT->notification(get_string('auth_dbtableempty', 'auth_db'), 'notifyproblem'); $rs->close(); } else { $columns = array_keys($rs->fetchRow()); echo $OUTPUT->notification(get_string('auth_dbcolumnlist', 'auth_db', implode(', ', $columns)), 'notifysuccess'); $rs->close(); } $adodb->Close(); $this->config->debugauthdb = $olddebugauthdb; $CFG->debug = $olddebug; ini_set('display_errors', $olddisplay); error_reporting($CFG->debug); ob_end_flush(); } /** * Clean the user data that comes from an external database. * @deprecated since 3.1, please use core_user::clean_data() instead. * @param array $user the user data to be validated against properties definition. * @return stdClass $user the cleaned user data. */ public function clean_data($user) { debugging('The method clean_data() has been deprecated, please use core_user::clean_data() instead.', DEBUG_DEVELOPER); return core_user::clean_data($user); } } PK&9\?rdb/settings.phpnu[. /** * Admin settings and defaults. * * @package auth_db * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/db/classes/admin_setting_special_auth_configtext.php'); // Needed for constants. require_once($CFG->libdir.'/authlib.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_db/pluginname', '', new lang_string('auth_dbdescription', 'auth_db'))); // Host. $settings->add(new admin_setting_configtext('auth_db/host', get_string('auth_dbhost_key', 'auth_db'), get_string('auth_dbhost', 'auth_db') . ' ' .get_string('auth_multiplehosts', 'auth'), '127.0.0.1', PARAM_RAW)); // Type. $dboptions = array(); $dbtypes = array("access", "ado_access", "ado", "ado_mssql", "borland_ibase", "csv", "db2", "fbsql", "firebird", "ibase", "informix72", "informix", "mssql", "mssql_n", "mssqlnative", "mysql", "mysqli", "mysqlt", "oci805", "oci8", "oci8po", "odbc", "odbc_mssql", "odbc_oracle", "oracle", "pdo", "postgres64", "postgres7", "postgres", "proxy", "sqlanywhere", "sybase", "vfp"); foreach ($dbtypes as $dbtype) { $dboptions[$dbtype] = $dbtype; } $settings->add(new admin_setting_configselect('auth_db/type', new lang_string('auth_dbtype_key', 'auth_db'), new lang_string('auth_dbtype', 'auth_db'), 'mysqli', $dboptions)); // Sybase quotes. $yesno = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_db/sybasequoting', new lang_string('auth_dbsybasequoting', 'auth_db'), new lang_string('auth_dbsybasequotinghelp', 'auth_db'), 0, $yesno)); // DB Name. $settings->add(new admin_setting_configtext('auth_db/name', get_string('auth_dbname_key', 'auth_db'), get_string('auth_dbname', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB Username. $settings->add(new admin_setting_configtext('auth_db/user', get_string('auth_dbuser_key', 'auth_db'), get_string('auth_dbuser', 'auth_db'), '', PARAM_RAW_TRIMMED)); // Password. $settings->add(new admin_setting_configpasswordunmask('auth_db/pass', get_string('auth_dbpass_key', 'auth_db'), get_string('auth_dbpass', 'auth_db'), '')); // DB Table. $settings->add(new admin_setting_configtext('auth_db/table', get_string('auth_dbtable_key', 'auth_db'), get_string('auth_dbtable', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB User field. $settings->add(new admin_setting_configtext('auth_db/fielduser', get_string('auth_dbfielduser_key', 'auth_db'), get_string('auth_dbfielduser', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB User password. $settings->add(new admin_setting_configtext('auth_db/fieldpass', get_string('auth_dbfieldpass_key', 'auth_db'), get_string('auth_dbfieldpass', 'auth_db'), '', PARAM_RAW_TRIMMED)); // DB Password Type. $passtype = array(); $passtype["plaintext"] = get_string("plaintext", "auth"); $passtype["md5"] = get_string("md5", "auth"); $passtype["sha1"] = get_string("sha1", "auth"); $passtype["saltedcrypt"] = get_string("auth_dbsaltedcrypt", "auth_db"); $passtype["internal"] = get_string("internal", "auth"); $settings->add(new admin_setting_configselect('auth_db/passtype', new lang_string('auth_dbpasstype_key', 'auth_db'), new lang_string('auth_dbpasstype', 'auth_db'), 'plaintext', $passtype)); // Encoding. $settings->add(new admin_setting_configtext('auth_db/extencoding', get_string('auth_dbextencoding', 'auth_db'), get_string('auth_dbextencodinghelp', 'auth_db'), 'utf-8', PARAM_RAW_TRIMMED)); // DB SQL SETUP. $settings->add(new admin_setting_configtext('auth_db/setupsql', get_string('auth_dbsetupsql', 'auth_db'), get_string('auth_dbsetupsqlhelp', 'auth_db'), '', PARAM_RAW_TRIMMED)); // Debug ADOOB. $settings->add(new admin_setting_configselect('auth_db/debugauthdb', new lang_string('auth_dbdebugauthdb', 'auth_db'), new lang_string('auth_dbdebugauthdbhelp', 'auth_db'), 0, $yesno)); // Password change URL. $settings->add(new auth_db_admin_setting_special_auth_configtext('auth_db/changepasswordurl', get_string('auth_dbchangepasswordurl_key', 'auth_db'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Label and Sync Options. $settings->add(new admin_setting_heading('auth_db/usersync', new lang_string('auth_sync_script', 'auth'), '')); // Sync Options. $deleteopt = array(); $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); $settings->add(new admin_setting_configselect('auth_db/removeuser', new lang_string('auth_remove_user_key', 'auth'), new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); // Update users. $settings->add(new admin_setting_configselect('auth_db/updateusers', new lang_string('auth_dbupdateusers', 'auth_db'), new lang_string('auth_dbupdateusers_description', 'auth_db'), 0, $yesno)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('db'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_dbextrafields', 'auth_db'), true, true, $authplugin->get_custom_user_profile_fields()); } PK&9\ݟ@kklti/db/upgrade.phpnu[. /** * LTI authentication plugin upgrade code * * @package auth_lti * @copyright 2021 Jake Dallimore * @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_lti_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; } PK&9\pI&&lti/db/install.xmlnu[
PK&9\Nlllti/db/events.phpnu[. /** * LTI Auth plugin event handler definition. * * @package auth_lti * @category event * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $observers = [ [ 'eventname' => '\core\event\user_loggedin', 'callback' => '\auth_lti\local\ltiadvantage\event\event_handler::handle_user_loggedin', ], ]; PK&9\i i lti/lang/en/auth_lti.phpnu[. /** * Strings for component 'auth_lti', language 'en'. * * @package auth_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['accountcreatedsuccess'] = 'Your account has been created and is now ready to use.'; $string['accountlinkedsuccess'] = 'Your existing account has been successfully linked.'; $string['auth_ltidescription'] = 'The LTI authentication plugin, together with the \'Publish as LTI tool\' enrolment plugin, allows remote users to access selected courses and activities. In other words, Moodle functions as an LTI tool provider.'; $string['cannotcreateaccounts'] = 'Account creation is currently prohibited on this site.'; $string['createaccount'] = 'Create account'; $string['createaccountforme'] = 'Create an account for me'; $string['createnewaccount'] = 'I\'d like to create a new account'; $string['currentlyloggedinas'] = 'You are currently logged in as:'; $string['firstlaunchnotice'] = 'It looks like this is your first time here. Please select from one of the following account options.'; $string['getstartedwithnewaccount'] = 'Get started with a new account'; $string['haveexistingaccount'] = 'I have an existing account'; $string['linkthisaccount'] = 'Link this account'; $string['mustbeloggedin'] = 'Log in to link your existing account.'; $string['pluginname'] = 'LTI'; $string['privacy:metadata:auth_lti'] = 'LTI authentication'; $string['privacy:metadata:auth_lti:authsubsystem'] = 'This plugin is connected to the authentication subsystem.'; $string['privacy:metadata:auth_lti:issuer'] = 'The issuer URL identifying the platform to which the linked user belongs.'; $string['privacy:metadata:auth_lti:issuer256'] = 'The SHA256 hash of the issuer URL.'; $string['privacy:metadata:auth_lti:sub'] = 'The subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:sub256'] = 'The SHA256 hash of the subject string identifying the user on the issuer.'; $string['privacy:metadata:auth_lti:tableexplanation'] = 'LTI accounts linked to a user\'s Moodle account.'; $string['privacy:metadata:auth_lti:timecreated'] = 'The timestamp when the user account was linked to the LTI login.'; $string['privacy:metadata:auth_lti:timemodified'] = 'The timestamp when this record was modified.'; $string['privacy:metadata:auth_lti:userid'] = 'The ID of the user account which the LTI login is linked to'; $string['provisioningmodeauto'] = 'New accounts only (automatic)'; $string['provisioningmodenewexisting'] = 'Existing and new accounts (prompt)'; $string['provisioningmodeexistingonly'] = 'Existing accounts only (prompt)'; $string['useexistingaccount'] = 'Use existing account'; $string['welcome'] = 'Welcome!'; PK&9\Zn lti/login.phpnu[. /** * Page allowing a platform user, identified by their {iss, sub} tuple, to be bound to a new or existing Moodle account. * * This is an LTI Advantage specific login feature. * * The auth flow defined in auth_lti\auth::complete_login() redirects here when a launching user does not have an * account binding yet. This page prompts the user to select between: * a) An auto provisioned account. * An account with auth type 'lti' is created for the user. This account is bound to the launch credentials. * Or * b) Use an existing account * The standard Moodle auth flow is leveraged to get an existing user account. This account is then bound to the launch * credentials. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\event\user_login_failed; use core\output\notification; require_once(__DIR__ . '/../../config.php'); global $OUTPUT, $PAGE, $SESSION; // Form fields dealing with the user's choice about account types (new, existing). $newaccount = optional_param('new_account', false, PARAM_BOOL); $existingaccount = optional_param('existing_account', false, PARAM_BOOL); if (empty($SESSION->auth_lti) || empty($SESSION->auth_lti->launchdata)) { throw new coding_exception('Missing LTI launch credentials.'); } if (empty($SESSION->auth_lti->returnurl)) { throw new coding_exception('Missing return URL.'); } if ($newaccount) { require_sesskey(); $launchdata = $SESSION->auth_lti->launchdata; $returnurl = $SESSION->auth_lti->returnurl; unset($SESSION->auth_lti); if (!empty($CFG->authpreventaccountcreation)) { // If 'authpreventaccountcreation' is enabled, the option to create a new account isn't presented to users in the form. // This just ensures no action is taken were the 'newaccount' value to be present in the submitted data. // Trigger login failed event. $failurereason = AUTH_LOGIN_UNAUTHORISED; $event = user_login_failed::create(['other' => ['reason' => $failurereason]]); $event->trigger(); // Site settings prevent creating new accounts. $errormsg = get_string('cannotcreateaccounts', 'auth_lti'); $SESSION->loginerrormsg = $errormsg; redirect(new moodle_url('/login/index.php')); } else { // Create a new account and link it, logging the user in. $auth = get_auth_plugin('lti'); $newuser = $auth->find_or_create_user_from_launch($launchdata); complete_user_login($newuser); $auth->update_user_account($newuser, $launchdata, $launchdata['iss']); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); echo $renderer->render_account_binding_complete( new notification(get_string('accountcreatedsuccess', 'auth_lti'), notification::NOTIFY_SUCCESS, false), $returnurl ); echo $OUTPUT->footer(); exit(); } } else if ($existingaccount) { // Only when authenticated can an account be bound, allowing the user to continue to the original launch action. require_login(null, false); require_sesskey(); $launchdata = $SESSION->auth_lti->launchdata; $returnurl = $SESSION->auth_lti->returnurl; unset($SESSION->auth_lti); global $USER; $auth = get_auth_plugin('lti'); $auth->create_user_binding($launchdata['iss'], $launchdata['sub'], $USER->id); $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); echo $renderer->render_account_binding_complete( new notification(get_string('accountlinkedsuccess', 'auth_lti'), notification::NOTIFY_SUCCESS, false), $returnurl ); echo $OUTPUT->footer(); exit(); } // Render the relevant account provisioning page, based on the provisioningmode set in the calling code. $PAGE->set_context(context_system::instance()); $PAGE->set_url(new moodle_url('/auth/lti/login.php')); $PAGE->set_pagelayout('popup'); $renderer = $PAGE->get_renderer('auth_lti'); echo $OUTPUT->header(); require_once($CFG->dirroot . '/auth/lti/auth.php'); echo $renderer->render_account_binding_options_page($SESSION->auth_lti->provisioningmode); echo $OUTPUT->footer(); PK&9\~ lti/lib.phpnu[. /** * Callbacks for auth_lti. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Callback to remove linked logins for deleted users. * * @param stdClass $user the user being deleted. */ function auth_lti_pre_user_delete($user) { global $DB; $DB->delete_records('auth_lti_linked_login', ['userid' => $user->id]); } PK&9\Zlti/version.phpnu[. /** * LTI authentication plugin version information * * @package auth_lti * @copyright 2016 Mark Nelson * @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_lti'; // Full name of the plugin (used for diagnostics). PK&9\ d lti/classes/privacy/provider.phpnu[. namespace auth_lti\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\context; use core_privacy\local\request\contextlist; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; /** * Privacy Subsystem for auth_lti implementing null_provider. * * @copyright 2018 Carlos Escobedo * @package auth_lti * @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\plugin\provider, \core_privacy\local\request\core_userlist_provider { /** * Get all contexts contain user information for the given user. * * @param int $userid the id of the user. * @return contextlist the list of contexts containing user information. */ public static function get_contexts_for_userid(int $userid): contextlist { $sql = "SELECT ctx.id FROM {auth_lti_linked_login} ll JOIN {context} ctx ON ctx.instanceid = ll.userid AND ctx.contextlevel = :contextlevel WHERE ll.userid = :userid"; $params = ['userid' => $userid, 'contextlevel' => CONTEXT_USER]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Export all user data for the user in the identified contexts. * * @param approved_contextlist $contextlist the list of approved contexts for the user. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; $user = $contextlist->get_user(); $linkedlogins = $DB->get_records('auth_lti_linked_login', ['userid' => $user->id], '', 'issuer, issuer256, sub, sub256, timecreated, timemodified'); foreach ($linkedlogins as $login) { $data = (object)[ 'timecreated' => transform::datetime($login->timecreated), 'timemodified' => transform::datetime($login->timemodified), 'issuer' => $login->issuer, 'issuer256' => $login->issuer256, 'sub' => $login->sub, 'sub256' => $login->sub256 ]; writer::with_context(\context_user::instance($user->id))->export_data([ get_string('privacy:metadata:auth_lti', 'auth_lti'), $login->issuer ], $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 user data in the list of given contexts. * * @param approved_contextlist $contextlist the list of contexts. */ 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) { static::delete_user_data($context->instanceid); } } } /** * 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_lti_linked_login} WHERE userid = ?"; $params = [$context->instanceid]; $userlist->add_from_sql('userid', $sql, $params); } /** * 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); } } /** * Description of the metadata stored for users in auth_lti. * * @param collection $collection a collection to add to. * @return collection the collection, with relevant metadata descriptions for auth_lti added. */ public static function get_metadata(collection $collection): collection { $authfields = [ 'userid' => 'privacy:metadata:auth_lti:userid', 'issuer' => 'privacy:metadata:auth_lti:issuer', 'issuer256' => 'privacy:metadata:auth_lti:issuer256', 'sub' => 'privacy:metadata:auth_lti:sub', 'sub256' => 'privacy:metadata:auth_lti:sub256', 'timecreated' => 'privacy:metadata:auth_lti:timecreated', 'timemodified' => 'privacy:metadata:auth_lti:timemodified' ]; $collection->add_database_table('auth_lti_linked_login', $authfields, 'privacy:metadata:auth_lti:tableexplanation'); $collection->link_subsystem('core_auth', 'privacy:metadata:auth_lti:authsubsystem'); return $collection; } /** * Delete user data for the user. * * @param int $userid The id of the user. */ 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_lti_linked_login', ['userid' => $userid]); } } PK&9\]!k>lti/classes/local/ltiadvantage/entity/user_migration_claim.phpnu[. namespace auth_lti\local\ltiadvantage\entity; /** * A simplified representation of a 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' migration claim. * * This serves the purpose of migrating a legacy user account only. Claim properties that do not relate to user migration are not * included or handled by this representation. * * See https://www.imsglobal.org/spec/lti/v1p3/migr#lti-1-1-migration-claim * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_migration_claim { /** @var string the LTI 1.1 consumer key */ private $consumerkey; /** @var string the LTI 1.1 user identifier. * This is only included in the claim if it differs to the value included in the LTI 1.3 'sub' claim. * If not included, the value will be taken from 'sub'. */ private $userid; /** * The migration_claim constructor. * * The signature of a migration claim must be verifiable. To achieve this, the constructor takes a list of secrets * corresponding to the 'oauth_consumer_key' provided in the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' * claim. How these secrets are determined is not the responsibility of this class. The constructor assumes these * correspond. * * @param array $jwt the array of claim data, as received in a resource link launch JWT. * @param array $consumersecrets a list of consumer secrets for the consumerkey included in the migration claim. * @throws \coding_exception if the claim data is invalid. */ public function __construct(array $jwt, array $consumersecrets) { // Can't get a claim instance without the claim data. if (empty($jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1'])) { throw new \coding_exception("Missing the 'https://purl.imsglobal.org/spec/lti/claim/lti1p1' JWT claim"); } $claim = $jwt['https://purl.imsglobal.org/spec/lti/claim/lti1p1']; // The oauth_consumer_key property MUST be sent. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key. if (empty($claim['oauth_consumer_key'])) { throw new \coding_exception("Missing 'oauth_consumer_key' property in lti1p1 migration claim."); } // The oauth_consumer_key_sign property MAY be sent. // For user migration to take place, however, this is deemed a required property since Moodle identified its // legacy users through a combination of consumerkey and userid. // See: https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key_sign. if (empty($claim['oauth_consumer_key_sign'])) { throw new \coding_exception("Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."); } if (!$this->verify_signature( $claim['oauth_consumer_key'], $claim['oauth_consumer_key_sign'], $jwt['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $jwt['iss'], $jwt['aud'], $jwt['exp'], $jwt['nonce'], $consumersecrets )) { throw new \coding_exception("Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim."); } $this->consumerkey = $claim['oauth_consumer_key']; $this->userid = $claim['user_id'] ?? $jwt['sub']; } /** * Verify the claim signature by recalculating it using the launch data and cross-checking consumer secrets. * * @param string $consumerkey the LTI 1.1 consumer key. * @param string $signature a signature of the LTI 1.1 consumer key and associated launch data. * @param string $deploymentid the deployment id included in the launch. * @param string $platform the platform included in the launch. * @param string $clientid the client id included in the launch. * @param string $exp the exp included in the launch. * @param string $nonce the nonce included in the launch. * @param array $consumersecrets the list of consumer secrets used with the given $consumerkey param * @return bool true if the signature was verified, false otherwise. */ private function verify_signature(string $consumerkey, string $signature, string $deploymentid, string $platform, string $clientid, string $exp, string $nonce, array $consumersecrets): bool { $base = [ $consumerkey, $deploymentid, $platform, $clientid, $exp, $nonce ]; $basestring = implode('&', $base); // Legacy enrol_lti code permits tools to share a consumer key but use different secrets. This results in // potentially many secrets per mapped tool consumer. As such, when generating the migration claim it's // impossible to know which secret the platform will use to sign the consumer key. The consumer key in the // migration claim is thus verified by trying all known secrets for the consumer, until either a match is found // or no signatures match. foreach ($consumersecrets as $consumersecret) { $calculatedsignature = base64_encode(hash_hmac('sha256', $basestring, $consumersecret)); if ($signature === $calculatedsignature) { return true; } } return false; } /** * Return the consumer key stored in the claim. * * @return string the consumer key included in the claim. */ public function get_consumer_key(): string { return $this->consumerkey; } /** * Return the LTI 1.1 user id stored in the claim. * * @return string the user id, or null if not provided in the claim. */ public function get_user_id(): string { return $this->userid; } } PK&9\UN6lti/classes/local/ltiadvantage/event/event_handler.phpnu[. namespace auth_lti\local\ltiadvantage\event; use auth_lti\local\ltiadvantage\utility\cookie_helper; use core\event\user_loggedin; /** * Event handler for auth_lti. * * @package auth_lti * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class event_handler { /** * Allows the plugin to augment Set-Cookie headers when the user_loggedin event is fired as part of complete_user_login() calls. * * @param user_loggedin $event the event * @return void */ public static function handle_user_loggedin(user_loggedin $event): void { // The event data isn't important here. The intent of this listener is to ensure that the MoodleSession cookie is set up // properly during LTI launches + login. This means two things: // i) it's set with SameSite=None; Secure; where possible (since OIDC needs HTTPS this will almost always be possible). // ii) it set with the 'Partitioned' attribute, when required. // The former ensures cross-site cookies are sent for embedded launches. The latter is an opt-in flag needed to use Chrome's // partitioning mechanism, CHIPS. if (cookie_helper::cookies_supported()) { cookie_helper::setup_session_cookie(); } } } PK&9\[Gv v 8lti/classes/local/ltiadvantage/utility/cookie_helper.phpnu[. namespace auth_lti\local\ltiadvantage\utility; use core\session\utility\cookie_helper as core_cookie_helper; /** * Helper class providing utils dealing with cookies in LTI, particularly 3rd party cookies. * * @package auth_lti * @copyright 2024 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class cookie_helper { /** @var int Cookies are not supported. */ public const COOKIE_METHOD_NOT_SUPPORTED = 0; /** @var int Cookies are supported without explicit partitioning. */ public const COOKIE_METHOD_NO_PARTITIONING = 1; /** @var int Cookies are supported via explicit partitioning. */ public const COOKIE_METHOD_EXPLICIT_PARTITIONING = 2; /** * Check whether cookies can be used with the current user agent and, if so, via what method they are set. * * Currently, this tries 2 modes of setting a test cookie: * 1. Setting a SameSite=None, Secure cookie. This will work in any first party context, and in 3rd party contexts for * any browsers supporting automatic partitioning of 3rd party cookies (E.g. Firefox, Brave). * 2. If 1 fails, setting a cookie with the Chrome 'Partitioned' attribute included, opting that cookie into CHIPS. This will * work for Chrome. * * Upon completion of the cookie check, the check sets a SESSION flag indicating the method used to set the cookie, and upgrades * the session cookie ('MoodleSession') using the respective method. This ensure the session cookie will continue to be sent. * * Then, the following methods can be used by client code to query whether the UA supports cookies, and how: * @see self::cookies_supported() - whether it could be set at all. * @see self::get_cookies_supported_mode() - if a cookie could be set, what mode was used to set it. * * This permits client code to make sure it's setting its cookies appropriately (via the advertised method), and allows it to * present notices - such as in the case where a given UA is found to be lacking the requisite cookie support. * E.g. * cookie_helper::do_cookie_check($mypageurl); * if (!cookie_helper::cookies_supported()) { * // Print a notice stating that cookie support is required. * } * // Elsewhere in other client code... * if (cookie_helper::get_cookies_supported_mode() === cookie_helper::COOKIE_METHOD_EXPLICIT_PARTITIONING) { * // Set a cookie, making sure to use the helper to also opt-in to partitioning. * setcookie('myauthcookie', 'myauthcookievalue', ['samesite' => 'None', 'secure' => true]); * cookie_helper::add_partitioning_to_cookie('myauthcookie'); * } * * @param \moodle_url $pageurl the URL of the page making the check, used to redirect back to after setting test cookies. * @return void */ public static function do_cookie_check(\moodle_url $pageurl): void { global $_COOKIE, $SESSION, $CFG; $cookiecheck1 = optional_param('cookiecheck1', null, PARAM_INT); $cookiecheck2 = optional_param('cookiecheck2', null, PARAM_INT); if (empty($cookiecheck1)) { // Start the cookie check. Set two test cookies - one samesite none, and one partitioned - and redirect. // Set cookiecheck to show the check has started. self::set_test_cookie('cookiecheck1', self::COOKIE_METHOD_NO_PARTITIONING); self::set_test_cookie('cookiecheck2', self::COOKIE_METHOD_EXPLICIT_PARTITIONING, true); $pageurl->params([ 'cookiecheck1' => self::COOKIE_METHOD_NO_PARTITIONING, 'cookiecheck2' => self::COOKIE_METHOD_EXPLICIT_PARTITIONING, ]); // LTI needs to guarantee the 'SameSite=None', 'Secure' (and sometimes 'Partitioned') attributes are set on the // MoodleSession cookie. This is done via manipulation of the outgoing headers after the cookie check redirect. To // guarantee these outgoing Set-Cookie headers will be created after the redirect, expire the current cookie. core_cookie_helper::expire_moodlesession(); redirect($pageurl); } else { // Have already started a cookie check, so check the result. $cookie1received = isset($_COOKIE['cookiecheck1']) && $_COOKIE['cookiecheck1'] == $cookiecheck1; $cookie2received = isset($_COOKIE['cookiecheck2']) && $_COOKIE['cookiecheck2'] == $cookiecheck2; if ($cookie1received || $cookie2received) { // The test cookie could be set and received. // Set a session flag storing the method used to set it, and make sure the session cookie uses this method. $cookiemethod = $cookie1received ? self::COOKIE_METHOD_NO_PARTITIONING : self::COOKIE_METHOD_EXPLICIT_PARTITIONING; $SESSION->auth_lti_cookie_method = $cookiemethod; self::setup_session_cookie(); } } } /** * If a cookie check has been made, returns whether cookies could be set or not. * * @return bool whether cookies are supported or not. */ public static function cookies_supported(): bool { return self::get_cookies_supported_method() !== self::COOKIE_METHOD_NOT_SUPPORTED; } /** * If a cookie check has been made, gets the method used to set a cookie, or self::COOKIE_METHOD_NOT_SUPPORTED if not supported. * * For cookie methods: * @see self::COOKIE_METHOD_NOT_SUPPORTED * @see self::COOKIE_METHOD_NO_PARTITIONING * @see self::COOKIE_METHOD_EXPLICIT_PARTITIONING * * @return int the constant representing the method by which the cookie was set, or not. */ public static function get_cookies_supported_method(): int { global $SESSION; return $SESSION->auth_lti_cookie_method ?? self::COOKIE_METHOD_NOT_SUPPORTED; } /** * Sets up the session cookie according to the method used in the cookie check, and with SameSite=None; Secure attributes. * * @return void */ public static function setup_session_cookie(): void { global $CFG; require_once($CFG->libdir . '/sessionlib.php'); if (is_moodle_cookie_secure()) { $atts = ['SameSite=None', 'Secure']; if (self::get_cookies_supported_method() == self::COOKIE_METHOD_EXPLICIT_PARTITIONING) { $atts[] = 'Partitioned'; } core_cookie_helper::add_attributes_to_cookie_response_header('MoodleSession' . $CFG->sessioncookie, $atts); } } /** * Set a test cookie, using SameSite=None; Secure; attributes if possible, and with or without partitioning opt-in. * * @param string $name cookie name * @param string $value cookie value * @param bool $partitioned whether to try to add partitioning opt-in, which requires secure cookies (https sites). * @return void */ private static function set_test_cookie(string $name, string $value, bool $partitioned = false): void { global $CFG; require_once($CFG->libdir . '/sessionlib.php'); $atts = ['expires' => time() + 30]; if (is_moodle_cookie_secure()) { $atts['samesite'] = 'none'; $atts['secure'] = true; } setcookie($name, $value, $atts); if (is_moodle_cookie_secure() && $partitioned) { core_cookie_helper::add_attributes_to_cookie_response_header($name, ['Partitioned']); } } } PK&9\i/lti/classes/output/renderer.phpnu[. namespace auth_lti\output; use core\output\notification; /** * Renderer class for auth_lti. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \plugin_renderer_base { /** * Render the account options view, displayed to instructors on first launch if no account binding exists. * * @param int $provisioningmode the desired account provisioning mode, see auth_plugin_lti constants for details. * @return string the html. */ public function render_account_binding_options_page(int $provisioningmode): string { $formaction = new \moodle_url('/auth/lti/login.php'); $notification = new notification(get_string('firstlaunchnotice', 'auth_lti'), \core\notification::INFO, false); $cancreateaccounts = !get_config('moodle', 'authpreventaccountcreation'); if ($provisioningmode == \auth_plugin_lti::PROVISIONING_MODE_PROMPT_EXISTING_ONLY) { $cancreateaccounts = false; } $accountinfo = []; if (isloggedin()) { global $USER; $accountinfo = [ 'firstname' => $USER->firstname, 'lastname' => $USER->lastname, 'email' => $USER->email, 'picturehtml' => $this->output->user_picture($USER, ['size' => 35, 'class' => 'round']), ]; } $context = [ 'isloggedin' => isloggedin(), 'info' => $notification->export_for_template($this), 'formaction' => $formaction->out(), 'sesskey' => sesskey(), 'accountinfo' => $accountinfo, 'cancreateaccounts' => $cancreateaccounts, ]; return parent::render_from_template('auth_lti/local/ltiadvantage/login', $context); } /** * Render the page displayed when the account binding is complete, letting the user continue to the launch. * * Callers can provide different messages depending on which type of binding took place. For example, a newly * provisioned account may require a slightly different message to an existing account being linked. * * The return URL is the page the user will be taken back to when they click 'Continue'. This is likely the launch * or deeplink launch endpoint but could be any calling code in LTI which wants to use the account binding workflow. * * @param notification $notification the notification containing the message describing the binding success. * @param \moodle_url $returnurl the URL to return to when the user clicks continue on the rendered page. * @return string the rendered HTML */ public function render_account_binding_complete(notification $notification, \moodle_url $returnurl): string { $context = (object) [ 'notification' => $notification->export_for_template($this), 'returnurl' => $returnurl->out() ]; return parent::render_from_template('auth_lti/local/ltiadvantage/account_binding_complete', $context); } } PK&9\Fb/lti/templates/local/ltiadvantage/login.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_lti/local/ltiadvantage/login Template which displays a choice screen for instructors on first launch, allowing them to select whether to use an existing account in the tool, or to auto provision a new one. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * formaction * sesskey * info - a notification describing the first launch options * cancreateaccounts - whether or not the user is allowed to create auth_lti accounts * accountinfo - information about the user, importantly whether they are logged in or not. Example context (json): { "formaction": "auth/lti/login.php", "sesskey": "1a2b3c4dfg", "info": { "message": "Looks like this is your first time here...", "extraclasses": "", "announce": false, "closebutton": false, "issuccess": true }, "cancreateaccounts": true, "isloggedin": true, "accountinfo": { "firstname": "John", "lastname": "Smith", "email": "john@example.com", "picturehtml": "\"\"" } } }}

{{#str}} welcome, auth_lti {{/str}}

{{#info}} {{> core/notification}} {{/info}}
{{#str}} haveexistingaccount, auth_lti {{/str}}

{{#str}} useexistingaccount, auth_lti {{/str}}

{{#isloggedin}} {{#accountinfo}}

{{#str}} currentlyloggedinas, auth_lti {{/str}}
{{{picturehtml}}} {{firstname}} {{lastname}} ({{email}})

{{/accountinfo}} {{/isloggedin}} {{^isloggedin}}

{{#str}} mustbeloggedin, auth_lti {{/str}}

{{/isloggedin}}
{{#cancreateaccounts}}
{{#str}} createnewaccount, auth_lti {{/str}}

{{#str}} createaccount, auth_lti {{/str}}

{{#str}} getstartedwithnewaccount, auth_lti {{/str}}

{{/cancreateaccounts}}
PK&9\ʄBlti/templates/local/ltiadvantage/account_binding_complete.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_lti/local/ltiadvantage/account_binding_complete Template which displays the confirmation after the user has either signed in and has their account linked, or has had an account automatically provisioned and linked. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * notification * returnurl Example context (json): { "notification": { "message": "Your account was successfully linked!", "extraclasses": "", "announce": true, "closebutton": false, "issuccess": true, "isinfo": false, "iswarning": false, "iserror": false }, "returnurl": "https://your.site/enrol/lti/launch_deeplink.php?id=123abc" } }}
{{#notification}} {{> core/notification}} {{/notification}} {{#str}}continue, core{{/str}}
PK&9\*F&&#lti/tests/privacy/provider_test.phpnu[. namespace auth_lti\privacy; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use core_privacy\tests\provider_testcase; use core_privacy\local\request\approved_userlist; /** * Test for the auth_lti privacy provider. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_lti\privacy\provider */ 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. * * @covers ::get_contexts_for_userid */ public function test_get_contexts_for_userid(): void { $user = $this->getDataGenerator()->create_user(); $this->assertEmpty(provider::get_contexts_for_userid($user->id)); $auth = get_auth_plugin('lti'); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); $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. * * @covers ::export_user_data */ public function test_export_user_data(): void { $user = $this->getDataGenerator()->create_user(); $auth = get_auth_plugin('lti'); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); $usercontext = \context_user::instance($user->id); $writer = writer::with_context($usercontext); $this->assertFalse($writer->has_any_data()); $approvedlist = new approved_contextlist($user, 'auth_lti', [$usercontext->id]); provider::export_user_data($approvedlist); $data = $writer->get_data([get_string('privacy:metadata:auth_lti', 'auth_lti'), 'https://lms.example.com']); $this->assertEquals('https://lms.example.com', $data->issuer); $this->assertEquals(hash('sha256', 'https://lms.example.com'), $data->issuer256); $this->assertEquals('abc123', $data->sub); $this->assertEquals(hash('sha256', 'abc123'), $data->sub256); } /** * Test deleting all user data for a specific context. * * @covers ::delete_data_for_all_users_in_context */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $auth = get_auth_plugin('lti'); $user1 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // Verify there are two linked logins. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(2, $ltiaccounts); // Delete everything for the first user context. provider::delete_data_for_all_users_in_context($user1context); // Get all LTI linked accounts match with user1. $ltiaccounts = $DB->get_records('auth_lti_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $ltiaccounts); // Verify there is now only one linked login. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(1, $ltiaccounts); } /** * This should work identical to the above test. * * @covers ::delete_data_for_user */ public function test_delete_data_for_user(): void { global $DB; $auth = get_auth_plugin('lti'); $user1 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $user1context = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // Verify there are two linked logins. $ltiaccounts = $DB->get_records('auth_lti_linked_login'); $this->assertCount(2, $ltiaccounts); // Delete everything for the first user. $approvedlist = new approved_contextlist($user1, 'auth_lti', [$user1context->id]); provider::delete_data_for_user($approvedlist); // Get all LTI accounts linked with user1. $ltiaccounts = $DB->get_records('auth_lti_linked_login', ['userid' => $user1->id]); $this->assertCount(0, $ltiaccounts); // Verify there is only one linked login now. $ltiaccounts = $DB->get_records('auth_lti_linked_login', array()); $this->assertCount(1, $ltiaccounts); } /** * Test that only users with a user context are fetched. * * @covers ::get_users_in_context */ public function test_get_users_in_context(): void { $auth = get_auth_plugin('lti'); $component = 'auth_lti'; $user = $this->getDataGenerator()->create_user(); $usercontext = \context_user::instance($user->id); // The list of users should not return anything yet (no linked login yet). $userlist = new userlist($usercontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); $auth->create_user_binding('https://lms.example.com', 'abc123', $user->id); // 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 userlist($systemcontext, $component); provider::get_users_in_context($userlist); $this->assertCount(0, $userlist); } /** * Test that data for users in approved userlist is deleted. * * @covers ::delete_data_for_users */ public function test_delete_data_for_users(): void { $auth = get_auth_plugin('lti'); $component = 'auth_lti'; $user1 = $this->getDataGenerator()->create_user(); $usercontext1 = \context_user::instance($user1->id); $user2 = $this->getDataGenerator()->create_user(); $usercontext2 = \context_user::instance($user2->id); $auth->create_user_binding('https://lms.example.com', 'abc123', $user1->id); $auth->create_user_binding('https://lms.example.com', 'def456', $user2->id); // The list of users for usercontext1 should return user1. $userlist1 = new 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 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 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 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 userlist($usercontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); } } PK&9\ulti/tests/auth_test.phpnu[. namespace auth_lti; /** * Tests for the auth_plugin_lti class. * * @package auth_lti * @copyright 2021 Jake Dallimore * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @coversDefaultClass \auth_plugin_lti */ final class auth_test extends \advanced_testcase { /** @var string issuer URL used for test cases. */ protected static string $issuer = 'https://lms.example.org'; /** @var int const representing cases where no PII is present. */ protected const PII_NONE = 0; /** @var int const representing cases where only names are included in PII. */ protected const PII_NAMES_ONLY = 1; /** @var int const representing cases where only email is included in PII. */ protected const PII_EMAILS_ONLY = 2; /** @var int const representing cases where both names and email are included in PII. */ protected const PII_ALL = 3; /** * Verify the user's profile picture has been set, which is useful to verify picture syncs. * * @param int $userid the id of the Moodle user. */ protected function verify_user_profile_image_updated(int $userid): void { global $CFG; $user = \core_user::get_user($userid); $usercontext = \context_user::instance($user->id); $expected = $CFG->wwwroot . '/pluginfile.php/' . $usercontext->id . '/user/icon/boost/f2?rev='. $user->picture; $page = new \moodle_page(); $page->set_url('/user/profile.php'); $page->set_context(\context_system::instance()); $renderer = $page->get_renderer('core'); $userpicture = new \user_picture($user); $this->assertEquals($expected, $userpicture->get_url($page, $renderer)->out(false)); } /** * Get a list of users ready for use with mock authentication requests by providing an array of user ids. * * @param array $ids the platform user_ids for the users. * @param string $role the LTI role to include in the user data. * @param bool $includenames whether to include the firstname and lastname of the user * @param bool $includeemail whether to include the email of the user * @param bool $includepicture whether to include a profile picture or not (slows tests, so defaults to false). * @return array the users list. */ protected static function get_mock_users_with_ids( array $ids, string $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', bool $includenames = true, bool $includeemail = true, bool $includepicture = false, ): array { $users = []; foreach ($ids as $id) { $user = [ 'user_id' => $id, 'given_name' => 'Firstname' . $id, 'family_name' => 'Surname' . $id, 'email' => "firstname.surname{$id}@lms.example.org", 'roles' => [$role] ]; if (!$includenames) { unset($user['given_name']); unset($user['family_name']); } if (!$includeemail) { unset($user['email']); } if ($includepicture) { $user['picture'] = self::getExternalTestFileUrl('/test.jpg'); } $users[] = $user; } return $users; } /** * Get a mock member structure based on a mock user and, optionally, a legacy user id. * * @param array $mockuser the user data * @param string $legacyuserid the id of the user in the platform in 1.1, if different from the id used in 1.3. * @return array */ protected function get_mock_member_data_for_user(array $mockuser, string $legacyuserid = ''): array { $data = [ 'user_id' => $mockuser['user_id'], 'roles' => $mockuser['roles'] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if (!empty($legacyuserid)) { $data['lti11_legacy_user_id'] = $legacyuserid; } return $data; } /** * Get mocked JWT data for the given user, including optionally the migration claim information if provided. * * @param array $mockuser the user data * @param array $mockmigration information needed to mock the migration claim * @return array the mock JWT data */ protected function get_mock_launchdata_for_user(array $mockuser, array $mockmigration = []): array { $data = [ 'iss' => self::$issuer, // Must match registration in create_test_environment. 'aud' => '123', // Must match registration in create_test_environment. 'sub' => $mockuser['user_id'], // User id on the platform site. 'exp' => time() + 60, 'nonce' => 'some-nonce-value-123', 'https://purl.imsglobal.org/spec/lti/claim/deployment_id' => '1', // Must match registration. 'https://purl.imsglobal.org/spec/lti/claim/roles' => $mockuser['roles'], 'https://purl.imsglobal.org/spec/lti/claim/resource_link' => [ 'title' => "Res link title", 'id' => 'res-link-id-123', ], "https://purl.imsglobal.org/spec/lti/claim/context" => [ "id" => "context-id-12345", "label" => "ITS 123", "title" => "ITS 123 Machine Learning", "type" => ["http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering"] ], 'https://purl.imsglobal.org/spec/lti/claim/target_link_uri' => 'https://this-moodle-tool.example.org/context/24/resource/14', 'https://purl.imsglobal.org/spec/lti/claim/custom' => [ 'id' => '1' ] ]; if (isset($mockuser['given_name'])) { $data['given_name'] = $mockuser['given_name']; } if (isset($mockuser['family_name'])) { $data['family_name'] = $mockuser['family_name']; } if (isset($mockuser['email'])) { $data['email'] = $mockuser['email']; } if (!empty($mockuser['picture'])) { $data['picture'] = $mockuser['picture']; } if ($mockmigration) { if (isset($mockmigration['consumer_key'])) { $base = [ $mockmigration['consumer_key'], $data['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], $data['iss'], $data['aud'], $data['exp'], $data['nonce'] ]; $basestring = implode('&', $base); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] = [ 'oauth_consumer_key' => $mockmigration['consumer_key'], ]; if (isset($mockmigration['signing_secret'])) { $sig = base64_encode(hash_hmac('sha256', $basestring, $mockmigration['signing_secret'])); $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['oauth_consumer_key_sign'] = $sig; } } if (isset($mockmigration['user_id'])) { $data['https://purl.imsglobal.org/spec/lti/claim/lti1p1']['user_id'] = $mockmigration['user_id']; } } return $data; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_launch() method. * * @dataProvider launchdata_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $launchdata data describing the launch, including user data and migration claim data. * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_launch */ public function test_find_or_create_user_from_launch(?array $legacydata, array $launchdata, array $expected = []): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($launchdata['has_authenticated_before'])) { $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user']); $firstauthuser = $auth->find_or_create_user_from_launch($mockjwtdata); } // Create legacy users and mocked tool secrets if desired. $legacysecrets = []; if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } // In a real usage, legacy tool secrets are only passed for a consumer, as indicated in the migration claim. if (!empty($launchdata['migration_claim'])) { $legacysecrets = array_column($legacydata['tools'], 'secret'); } } // Mock the launchdata. $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user'], $launchdata['migration_claim'] ?? []); // Authenticate the platform user. $sink = $this->redirectEvents(); $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_launch($mockjwtdata, $legacysecrets); if (!empty($expected['migration_debugging'])) { $this->assertDebuggingCalled(); } $countusersafter = $DB->count_records('user'); $events = $sink->get_events(); $sink->close(); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated'])) ? 0 : 1; $numnewusers = (!empty($launchdata['has_authenticated_before'])) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); if (!empty($expected['migrated'])) { // If migrated, verify the user account is reusing the legacy user account. $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); $this->assertEmpty($events); // No updates as part of this method. } else if (isset($firstauthuser)) { // If the user is authenticating a second time, confirm the same account is being returned. $this->assertEquals($firstauthuser->id, $user->id); $this->assertEmpty($events); // No updates as part of this method. } else { // The user wasn't migrated and hasn't launched before, so we expect a user_created event. $this->assertInstanceOf(\core\event\user_created::class, $events[0]); } } /** * Data provider for testing launch-based authentication. * * @return array the test case data. */ public static function launchdata_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding names, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding emails, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner excluding all PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, no migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null, ], 'expected' => [ 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration missing consumer_key' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration bad consumer_key' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_BAD', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration user not matched' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '234-bcd', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false ] ], 'New (unlinked) platform learner including PII, existing legacy user, valid migration claim secret2' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform learner including PII, existing legacy user, migration claim bad secret' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'bad_secret', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false, 'migration_debugging' => true, ] ], 'New (unlinked) platform learner including PII, no legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret2', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'New (unlinked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor including PII, existing legacy user, valid migration claim' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', 'tools' => [ ['secret' => 'toolsecret1'], ['secret' => 'toolsecret2'], ] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => [ 'consumer_key' => 'CONSUMER_1', 'signing_secret' => 'toolsecret1', 'user_id' => '123-abc', 'context_id' => 'd345b', 'tool_consumer_instance_guid' => '12345-123', 'resource_link_id' => '4b6fa' ] ], 'expected' => [ 'migrated' => true ] ], 'Existing (linked) platform learner including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'migration_claim' => null ], ], 'Existing (linked) platform learner excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], 'migration_claim' => null ], ], 'Existing (linked) platform instructor including PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], 'migration_claim' => null ], ], 'Existing (linked) platform instructor excluding PII, no legacy user, no migration claim' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], 'migration_claim' => null ], ], 'New (unlinked) platform instructor excluding PII, picture included' => [ 'legacydata' => null, 'launchdata' => [ 'has_authenticated_before' => false, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false, true )[0], 'migration_claim' => null ], ] ]; } /** * Test which verifies a user account can be created/found using the find_or_create_user_from_membership() method. * * @dataProvider membership_data_provider * @param array|null $legacydata legacy user and tool data, if testing migration cases. * @param array $memberdata data describing the membership data, including user data and legacy user id info. * @param string $iss the issuer URL string * @param string|null $legacyconsumerkey optional legacy consumer_key value for testing user migration * @param array $expected the test case expectations. * @covers ::find_or_create_user_from_membership */ public function test_find_or_create_user_from_membership(?array $legacydata, array $memberdata, string $iss, ?string $legacyconsumerkey, array $expected): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); // When testing platform users who have authenticated before, make that first auth call. if (!empty($memberdata['has_authenticated_before'])) { $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); $firstauthuser = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); } // Create legacy users and mocked tool secrets if desired. if ($legacydata) { $legacyusers = []; $generator = $this->getDataGenerator(); foreach ($legacydata['users'] as $legacyuser) { $username = 'enrol_lti' . sha1($legacydata['consumer_key'] . '::' . $legacydata['consumer_key'] . ':' . $legacyuser['user_id']); $legacyusers[] = $generator->create_user([ 'username' => $username, 'auth' => 'lti' ]); } } // Mock the membership data. $mockmemberdata = $this->get_mock_member_data_for_user($memberdata['user'], $memberdata['legacy_user_id'] ?? ''); // Authenticate the platform user. $sink = $this->redirectEvents(); $countusersbefore = $DB->count_records('user'); $user = $auth->find_or_create_user_from_membership($mockmemberdata, $iss, $legacyconsumerkey ?? ''); $countusersafter = $DB->count_records('user'); $events = $sink->get_events(); $sink->close(); // Verify user count is correct. i.e. no user is created when migration claim is correctly processed or when // the user has authenticated with the tool before. $numnewusers = (!empty($expected['migrated'])) ? 0 : 1; $numnewusers = (!empty($memberdata['has_authenticated_before'])) ? 0 : $numnewusers; $this->assertEquals($numnewusers, $countusersafter - $countusersbefore); // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $this->assertEquals($memberdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($memberdata['user']['given_name'], $user->firstname); $this->assertEquals($memberdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $this->assertEquals($memberdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($iss, $user->lastname); $this->assertEquals($mockmemberdata['user_id'], $user->firstname); $email = 'enrol_lti_13_' . sha1($iss . '_' . $mockmemberdata['user_id']) . "@example.com"; $this->assertEquals($email, $user->email); break; } if (!empty($expected['migrated'])) { // If migrated, verify the user account is reusing the legacy user account. $legacyuserids = array_column($legacyusers, 'id'); $this->assertContains($user->id, $legacyuserids); $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } else if (isset($firstauthuser)) { // If the user is authenticating a second time, confirm the same account is being returned. $this->assertEquals($firstauthuser->id, $user->id); $this->assertEmpty($events); // The user authenticated with the same data once before, so we don't expect an update. } else { // The user wasn't migrated and hasn't launched before, so we expect a user_created event. $this->assertInstanceOf(\core\event\user_created::class, $events[0]); } } /** * Data provider for testing membership-service-based authentication. * * @return array the test case data. */ public static function membership_data_provider(): array { return [ 'New (unlinked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ], 'New (unlinked) platform learner excluding names, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner excluding email, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NAMES_ONLY, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id sent' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, legacy user id omitted' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key bound, no change in user id' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => true ] ], 'New (unlinked) platform learner including PII, legacy user, unexpected consumer key bound, no change in user id' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['123-abc'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_ABCDEF', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false, ] ], 'New (unlinked) platform learner including PII, legacy user, consumer key not bound, legacy user id sent' => [ 'legacydata' => [ 'users' => [ ['user_id' => '123-abc'], ], 'consumer_key' => 'CONSUMER_1', ], 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform learner including PII, no legacy data, consumer key bound, legacy user id sent' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], 'legacy_user_id' => '123-abc' ], 'iss' => self::$issuer, 'legacyconsumerkey' => 'CONSUMER_1', 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], 'New (unlinked) platform instructor excluding PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', false, false )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_NONE, 'migrated' => false ] ], 'Existing (linked) platform learner including PII, no legacy data, no consumer key bound, no legacy id' => [ 'legacydata' => null, 'memberdata' => [ 'has_authenticated_before' => true, 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'iss' => self::$issuer, 'legacyconsumerkey' => null, 'expected' => [ 'PII' => self::PII_ALL, 'migrated' => false ] ], ]; } /** * Test the behaviour of create_user_binding(). * * @covers ::create_user_binding */ public function test_create_user_binding(): void { $this->resetAfterTest(); global $DB; $auth = get_auth_plugin('lti'); $user = $this->getDataGenerator()->create_user(); $mockiss = self::$issuer; $mocksub = '1'; // Create a binding and verify it exists. $this->assertFalse($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); $auth->create_user_binding($mockiss, $mocksub, $user->id); $this->assertTrue($DB->record_exists('auth_lti_linked_login', ['userid' => $user->id])); // Now, try to get an authenticated user USING that binding. Verify the bound user is returned. $numusersbefore = $DB->count_records('user'); $matcheduser = $auth->find_or_create_user_from_launch( $this->get_mock_launchdata_for_user( self::get_mock_users_with_ids([$mocksub])[0] ) ); $numusersafter = $DB->count_records('user'); $this->assertEquals($numusersafter, $numusersbefore); $this->assertEquals($user->id, $matcheduser->id); // Assert idempotency of the bind call. $this->assertNull($auth->create_user_binding($mockiss, $mocksub, $user->id)); } /** * Test updating a user account based on a given set of launchdata. * * @param array $firstlaunchdata the data from the first launch the user made. * @param array $launchdata the current launch data, which will dictate what data is updated. * @param array $expected array of test expectations * @dataProvider update_user_account_provider * @covers ::update_user_account */ public function test_update_user_account(array $firstlaunchdata, array $launchdata, array $expected): void { $this->resetAfterTest(); $auth = get_auth_plugin('lti'); // Mock the first authentication of the user. $firstmockjwtdata = $this->get_mock_launchdata_for_user($firstlaunchdata['user']); $user = $auth->find_or_create_user_from_launch($firstmockjwtdata); // Now, mock the recent authentication, confirming updates. $mockjwtdata = $this->get_mock_launchdata_for_user($launchdata['user']); $sink = $this->redirectEvents(); $auth->update_user_account($user, $mockjwtdata, $mockjwtdata['iss']); $user = \core_user::get_user($user->id); $events = $sink->get_events(); $sink->close(); if (!empty($expected['user_updated'])) { $this->assertInstanceOf(\core\event\user_updated::class, $events[0]); } else { $this->assertEmpty($events); } // Verify PII is updated appropriately. switch ($expected['PII']) { case self::PII_ALL: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $this->assertEquals($launchdata['user']['email'], $user->email); break; case self::PII_NAMES_ONLY: $this->assertEquals($launchdata['user']['given_name'], $user->firstname); $this->assertEquals($launchdata['user']['family_name'], $user->lastname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; case self::PII_EMAILS_ONLY: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $this->assertEquals($launchdata['user']['email'], $user->email); break; default: case self::PII_NONE: $this->assertEquals($mockjwtdata['iss'], $user->lastname); $this->assertEquals($mockjwtdata['sub'], $user->firstname); $email = 'enrol_lti_13_' . sha1($mockjwtdata['iss'] . '_' . $mockjwtdata['sub']) . "@example.com"; $this->assertEquals($email, $user->email); break; } // Verify picture sync occurs, if expected. if (!empty($expected['picture_updated'])) { $this->verify_user_profile_image_updated($user->id); } } /** * Data provider for testing user user_update_account. * * @return array the test case data. */ public static function update_user_account_provider(): array { return [ 'Full PII included in both auths, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => false, 'picture_updated' => false ] ], 'No PII included in both auths, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'expected' => [ 'PII' => self::PII_NONE, 'user_updated' => false, 'picture_updated' => false ] ], 'First auth no PII, second auth including PII, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth no PII, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false, false )[0], ], 'expected' => [ 'PII' => self::PII_NONE, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth emails only, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', false )[0], ], 'expected' => [ 'PII' => self::PII_EMAILS_ONLY, 'user_updated' => true, 'picture_updated' => false ] ], 'First auth full PII, second auth names only, no picture in either' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, false )[0], ], 'expected' => [ 'PII' => self::PII_NAMES_ONLY, 'user_updated' => true, 'picture_updated' => false ] ], 'Full PII included in both auths, picture included in the second auth' => [ 'firstlaunchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner' )[0] ], 'launchdata' => [ 'user' => self::get_mock_users_with_ids( ['1'], 'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner', true, true, true )[0], ], 'expected' => [ 'PII' => self::PII_ALL, 'user_updated' => false, 'picture_updated' => false ] ], ]; } } PK&9\,MwXwX lti/auth.phpnu[. use auth_lti\local\ltiadvantage\entity\user_migration_claim; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); require_once($CFG->libdir.'/accesslib.php'); /** * LTI Authentication plugin. * * @package auth_lti * @copyright 2016 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_plugin_lti extends \auth_plugin_base { /** * @var int constant representing the automatic account provisioning mode. * On first launch, for a previously unbound user, this mode dictates that a new Moodle account will be created automatically * for the user and bound to their platform credentials {iss, sub}. */ public const PROVISIONING_MODE_AUTO_ONLY = 1; /** * @var int constant representing the prompt for new or existing provisioning mode. * On first launch, for a previously unbound user, the mode dictates that the launch user will be presented with an options * view, allowing them to select either 'link an existing account' or 'create a new account for me'. */ public const PROVISIONING_MODE_PROMPT_NEW_EXISTING = 2; /** * @var int constant representing the prompt for existing only provisioning mode. * On first launch, for a previously unbound user, the mode dictates that the launch user will be presented with a view allowing * them to link an existing account only. This is useful for situations like deep linking, where an existing account is needed. */ public const PROVISIONING_MODE_PROMPT_EXISTING_ONLY = 3; /** * Constructor. */ public function __construct() { $this->authtype = 'lti'; } /** * Users can not log in via the traditional login form. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure */ public function user_login($username, $password) { return false; } /** * Authenticate the user based on the unique {iss, sub} tuple present in the OIDC JWT. * * This method ensures a Moodle user account has been found or is created, that the user is linked to the relevant * LTI Advantage credentials (iss, sub) and that the user account is logged in. * * Launch code can therefore rely on this method to get a session before doing things like calling require_login(). * * This method supports two workflows: * 1. Automatic account provisioning - where the complete_login() call will ALWAYS create/find a user and return to * calling code directly. No user interaction is required. * * 2. Manual account provisioning - where the complete_login() call will redirect ONLY ONCE to a login page, * where the user can decide whether to use an automatically provisioned account, or bind an existing user account. * When the decision has been made, the relevant account is bound and the user is redirected back to $returnurl. * Once an account has been bound via this selection process, subsequent calls to complete_login() will return to * calling code directly. Any calling code must provide its $returnurl to support the return from the account * selection process and must also take care to cache any JWT data appropriately, since the return will not inlude * that information. * * Which workflow is chosen depends on the roles present in the JWT. * For teachers/admins, manual provisioning will take place. These user type are likely to have existing accounts. * For learners, automatic provisioning will take place. * * Migration of legacy users is supported, however, only for the Learner role (automatic provisioning). Admins and * teachers are likely to have existing accounts and we want them to be able to select and bind these, rather than * binding an automatically provisioned legacy account which doesn't represent their real user account. * * The JWT data must be verified elsewhere. The code here assumes its integrity/authenticity. * * @param array $launchdata the JWT data provided in the link launch. * @param moodle_url $returnurl the local URL to return to if authentication workflows are required. * @param int $provisioningmode the desired account provisioning mode, which controls the auth flow for unbound users. * @param array $legacyconsumersecrets an array of secrets used by the legacy consumer if a migration claim exists. * @throws coding_exception if the specified provisioning mode is invalid. * @throws \core\exception\moodle_exception if user authentication fails. */ public function complete_login(array $launchdata, moodle_url $returnurl, int $provisioningmode, array $legacyconsumersecrets = []): void { // The platform user is already linked with a user account. if ($this->get_user_binding($launchdata['iss'], $launchdata['sub'])) { $user = $this->find_or_create_user_from_launch($launchdata); if ($user->suspended) { $failurereason = AUTH_LOGIN_SUSPENDED; $event = \core\event\user_login_failed::create([ 'userid' => $user->id, 'other' => [ 'username' => $user->username, 'reason' => $failurereason ] ]); $event->trigger(); throw new \core\exception\moodle_exception('invalidlogin', 'core'); } if (isloggedin()) { // If a different user is currently logged in, authenticate the linked user instead. global $USER; if ($USER->id !== $user->id) { complete_user_login($user); } // If the linked user is already logged in, skip the call to complete_user_login() because this affects deep linking // workflows on sites publishing and consuming resources on the same site, due to the regenerated sesskey. } else { complete_user_login($user); } // Always sync the PII, regardless of whether we're already authenticated as this user or not. $this->update_user_account($user, $launchdata, $launchdata['iss']); return; } // The platform user is not bound to a user account, check provisioning mode now. if (!$this->is_valid_provisioning_mode($provisioningmode)) { throw new coding_exception('Invalid account provisioning mode provided to complete_login().'); } switch ($provisioningmode) { case self::PROVISIONING_MODE_AUTO_ONLY: // Automatic provisioning - this will create/migrate a user account and log the user in. $user = $this->find_or_create_user_from_launch($launchdata, $legacyconsumersecrets); complete_user_login($user); $this->update_user_account($user, $launchdata, $launchdata['iss']); break; case self::PROVISIONING_MODE_PROMPT_NEW_EXISTING: case self::PROVISIONING_MODE_PROMPT_EXISTING_ONLY: default: // Allow linking an existing account or creation of a new account via an intermediate account options page. // Cache the relevant data and take the user to the account options page. // Note: This mode also depends on the global auth config 'authpreventaccountcreation'. If set, only existing // accounts can be bound in this provisioning mode. global $SESSION; $SESSION->auth_lti = (object)[ 'launchdata' => $launchdata, 'returnurl' => $returnurl, 'provisioningmode' => $provisioningmode ]; redirect(new moodle_url('/auth/lti/login.php', [ 'sesskey' => sesskey(), ])); break; } } /** * Get a Moodle user account for the LTI user based on the user details returned by a NRPS 2 membership call. * * This method expects a single member structure, in array format, as defined here: * See: https://www.imsglobal.org/spec/lti-nrps/v2p0#membership-container-media-type. * * This method supports migration of user accounts used in legacy launches, provided the legacy consumerkey corresponding to * to the legacy consumer is provided. Calling code will have verified the migration claim during initial launches and should * have the consumer key mapped to the deployment, ready to pass in. * * @param array $member the member data, in array format. * @param string $iss the issuer to which the member relates. * @param string $legacyconsumerkey optional consumer key mapped to the deployment to facilitate user migration. * @return stdClass a Moodle user record. */ public function find_or_create_user_from_membership(array $member, string $iss, string $legacyconsumerkey = ''): stdClass { // Picture is not synced with membership-based auths because sync tasks may wish to perform slow operations like this a // different way. unset($member['picture']); if ($binduser = $this->get_user_binding($iss, $member['user_id'])) { $user = \core_user::get_user($binduser); $this->update_user_account($user, $member, $iss); return \core_user::get_user($user->id); } else { if (!empty($legacyconsumerkey)) { // Consumer key is required to attempt user migration because legacy users were identified by a // username consisting of the consumer key and user id. // See the legacy \enrol_lti\helper::create_username() for details. $legacyuserid = $member['lti11_legacy_user_id'] ?? $member['user_id']; $username = 'enrol_lti' . sha1($legacyconsumerkey . '::' . $legacyconsumerkey . ':' . $legacyuserid); if ($user = \core_user::get_user_by_username($username)) { $this->create_user_binding($iss, $member['user_id'], $user->id); $this->update_user_account($user, $member, $iss); return \core_user::get_user($user->id); } } $user = $this->create_new_account($member, $iss); return \core_user::get_user($user->id); } } /** * Get a Moodle user account for the LTI user corresponding to the user defined in a link launch. * * This method supports migration of user accounts used in legacy launches, provided the legacy consumer secrets corresponding * to the legacy consumer are provided. If calling code wishes migration to be role-specific, it should check roles accordingly * itself and pass relevant data in - as auth_plugin_lti::complete_login() does. * * @param array $launchdata all data in the decoded JWT including iss and sub. * @param array $legacyconsumersecrets all secrets found for the legacy consumer, facilitating user migration. * @return stdClass the Moodle user who is mapped to the platform user identified in the JWT data. */ public function find_or_create_user_from_launch(array $launchdata, array $legacyconsumersecrets = []): stdClass { if ($binduser = $this->get_user_binding($launchdata['iss'], $launchdata['sub'])) { return \core_user::get_user($binduser); } else { // Is the intent to migrate a user account used in legacy launches? if (!empty($legacyconsumersecrets)) { try { // Validate the migration claim and try to find a legacy user. $usermigrationclaim = new user_migration_claim($launchdata, $legacyconsumersecrets); $username = 'enrol_lti' . sha1($usermigrationclaim->get_consumer_key() . '::' . $usermigrationclaim->get_consumer_key() . ':' . $usermigrationclaim->get_user_id()); if ($user = core_user::get_user_by_username($username)) { $this->create_user_binding($launchdata['iss'], $launchdata['sub'], $user->id); return core_user::get_user($user->id); } } catch (Exception $e) { // There was an issue validating the user migration claim. We don't want to fail auth entirely though. // Rather, we want to fall back to new account creation and log the attempt. debugging("There was a problem migrating the LTI user '{$launchdata['sub']}' on the platform " . "'{$launchdata['iss']}'. The migration claim could not be validated. A new account will be created."); } } // At the point of the creation, to ensure the user_created event correctly reflects the creating user of '0' (the user // performing the action), ensure any active session is terminated and an empty session initialised. $this->empty_session(); $user = $this->create_new_account($launchdata, $launchdata['iss']); return core_user::get_user($user->id); } } /** * Create a binding between the LTI user, as identified by {iss, sub} tuple and the user id. * * @param string $iss the issuer URL identifying the platform to which to user belongs. * @param string $sub the sub string identifying the user on the platform. * @param int $userid the id of the Moodle user account to bind. */ public function create_user_binding(string $iss, string $sub, int $userid): void { global $DB; $timenow = time(); $issuer256 = hash('sha256', $iss); $sub256 = hash('sha256', $sub); if ($DB->record_exists('auth_lti_linked_login', ['issuer256' => $issuer256, 'sub256' => $sub256])) { return; } $rec = [ 'userid' => $userid, 'issuer' => $iss, 'issuer256' => $issuer256, 'sub' => $sub, 'sub256' => $sub256, 'timecreated' => $timenow, 'timemodified' => $timenow ]; $DB->insert_record('auth_lti_linked_login', $rec); } /** * Gets the id of the linked Moodle user account for an LTI user, or null if not found. * * @param string $issuer the issuer to which the user belongs. * @param string $sub the sub string identifying the user on the issuer. * @return int|null the id of the corresponding Moodle user record, or null if not found. */ public function get_user_binding(string $issuer, string $sub): ?int { global $DB; $issuer256 = hash('sha256', $issuer); $sub256 = hash('sha256', $sub); try { $binduser = $DB->get_field('auth_lti_linked_login', 'userid', ['issuer256' => $issuer256, 'sub256' => $sub256], MUST_EXIST); } catch (\dml_exception $e) { $binduser = null; } return $binduser; } /** * If there's an existing session, inits an empty session. * * @return void */ protected function empty_session(): void { if (isloggedin()) { \core\session\manager::init_empty_session(); } } /** * Check whether a provisioning mode is valid or not. * * @param int $mode the mode * @return bool true if valid for use, false otherwise. */ protected function is_valid_provisioning_mode(int $mode): bool { $validmodes = [ self::PROVISIONING_MODE_AUTO_ONLY, self::PROVISIONING_MODE_PROMPT_NEW_EXISTING, self::PROVISIONING_MODE_PROMPT_EXISTING_ONLY ]; return in_array($mode, $validmodes); } /** * Create a new user account based on the user data either in the launch JWT or from a membership call. * * @param array $userdata the user data coming from either a launch or membership service call. * @param string $iss the issuer to which the user belongs. * @return stdClass a complete Moodle user record. */ protected function create_new_account(array $userdata, string $iss): stdClass { global $CFG; require_once($CFG->dirroot.'/user/lib.php'); // Launches and membership calls handle the user id differently. // Launch uses 'sub', whereas member uses 'user_id'. $userid = !empty($userdata['sub']) ? $userdata['sub'] : $userdata['user_id']; $user = new stdClass(); $user->username = 'enrol_lti_13_' . sha1($iss . '_' . $userid); // If the email was stripped/not set then fill it with a default one. // This stops the user from being redirected to edit their profile page. $email = !empty($userdata['email']) ? $userdata['email'] : 'enrol_lti_13_' . sha1($iss . '_' . $userid) . "@example.com"; $email = \core_user::clean_field($email, 'email'); $user->email = $email; $user->auth = 'lti'; $user->mnethostid = $CFG->mnet_localhost_id; $user->firstname = $userdata['given_name'] ?? $userid; $user->lastname = $userdata['family_name'] ?? $iss; $user->password = ''; $user->confirmed = 1; $user->id = user_create_user($user, false); // Link this user with the LTI credentials for future use. $this->create_user_binding($iss, $userid, $user->id); return (object) get_complete_user_data('id', $user->id); } /** * Update the personal fields of the user account, based on data present in either a launch of member sync call. * * @param stdClass $user the Moodle user account to update. * @param array $userdata the user data coming from either a launch or membership service call. * @param string $iss the issuer to which the user belongs. */ public function update_user_account(stdClass $user, array $userdata, string $iss): void { global $CFG; require_once($CFG->dirroot.'/user/lib.php'); if ($user->auth !== 'lti') { return; } // Launches and membership calls handle the user id differently. // Launch uses 'sub', whereas member uses 'user_id'. $platformuserid = !empty($userdata['sub']) ? $userdata['sub'] : $userdata['user_id']; $email = !empty($userdata['email']) ? $userdata['email'] : 'enrol_lti_13_' . sha1($iss . '_' . $platformuserid) . "@example.com"; $email = \core_user::clean_field($email, 'email'); $update = [ 'id' => $user->id, 'firstname' => $userdata['given_name'] ?? $platformuserid, 'lastname' => $userdata['family_name'] ?? $iss, 'email' => $email ]; $userfieldstocompare = array_intersect_key((array) $user, $update); if (!empty(array_diff($update, $userfieldstocompare))) { user_update_user($update); // Only update if there's a change. } if (!empty($userdata['picture'])) { try { $this->update_user_picture($user->id, $userdata['picture']); } catch (Exception $e) { debugging("Error syncing the profile picture for user '$user->id' during LTI authentication."); } } } /** * Update the user's picture with the image stored at $url. * * @param int $userid the id of the user to update. * @param string $url the string URL where the new image can be found. * @throws moodle_exception if there were any problems updating the picture. */ protected function update_user_picture(int $userid, string $url): void { global $CFG, $DB; require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/gdlib.php'); $fs = get_file_storage(); $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' => '/' ); $urlparams = array( 'calctimeout' => false, 'timeout' => 5, 'skipcertverify' => true, 'connecttimeout' => 5 ); try { $fs->create_file_from_url($filerecord, $url, $urlparams); } catch (\file_exception $e) { throw new moodle_exception(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'); throw new moodle_exception('There was a problem copying the profile picture to temp.'); } // 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. $DB->set_field('user', 'picture', $newpicture, array('id' => $userid)); } } PK&9\1KKshibboleth/db/install.phpnu[. /** * Shibboleth authentication plugin upgrade code * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_shibboleth. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_shibboleth_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; } PK&9\fyy&shibboleth/lang/en/auth_shibboleth.phpnu[. /** * Strings for component 'auth_shibboleth', language 'en'. * * @package auth_shibboleth * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_shib_auth_method'] = 'Authentication method name'; $string['auth_shib_auth_method_description'] = 'Provide a name for the Shibboleth authentication method that is familiar to your users. This could be the name of your Shibboleth federation, e.g. SWITCHaai Login or InCommon Login or similar.'; $string['auth_shib_auth_logo'] = 'Authentication method logo'; $string['auth_shib_auth_logo_description'] = 'Provide a logo for the Shibboleth authentication method that is familiar to your users. This could be the logo of your Shibboleth federation, e.g. SWITCHaai Login or InCommon Login or similar.'; $string['auth_shib_contact_administrator'] = 'In case you are not associated with the given organizations and you need access to a course on this server, please contact the Moodle Administrator.'; $string['auth_shibbolethdescription'] = 'Using this method users are created and authenticated using Shibboleth. For set-up details, see the Shibboleth README.'; $string['auth_shibboleth_errormsg'] = 'Please select the organization you are member of!'; $string['auth_shibboleth_login'] = 'Shibboleth login'; $string['auth_shibboleth_login_long'] = 'Log in via Shibboleth'; $string['auth_shibboleth_manual_login'] = 'Manual login'; $string['auth_shibboleth_select_member'] = 'I\'m a member of ...'; $string['auth_shibboleth_select_organization'] = 'For authentication via Shibboleth, please select your organisation from the drop-down menu:'; $string['auth_shib_convert_data'] = 'Data modification API'; $string['auth_shib_convert_data_description'] = 'You can use this API to further modify the data provided by Shibboleth. Read the README for further instructions.'; $string['auth_shib_convert_data_warning'] = 'The file does not exist or is not readable by the webserver process!'; $string['auth_shib_convert_data_filepath_warning'] = 'You cannot use a file that is located within the current site data directory ($CFG->dataroot) as the data modification API.'; $string['auth_shib_changepasswordurl'] = 'Password-change URL'; $string['auth_shib_idp_list'] = 'Identity providers'; $string['auth_shib_idp_list_description'] = 'Provide a list of Identity Provider entityIDs to let the user choose from on the login page.
On each line there must be a comma-separated tuple for entityID of the IdP (see the Shibboleth metadata file) and Name of IdP as it shall be displayed in the drop-down list.
As an optional third parameter you can add the location of a Shibboleth session initiator that shall be used in case your Moodle installation is part of a multi federation setup.'; $string['auth_shib_instructions'] = 'Use the Shibboleth login to get access via Shibboleth, if your institution supports it. Otherwise, use the normal login form shown here.'; $string['auth_shib_instructions_help'] = 'Here you should provide custom instructions for your users to explain Shibboleth. It will be shown on the login page in the instructions section. The instructions must include a link to "{$a}" that users click when they want to log in.'; $string['auth_shib_instructions_key'] = 'Login instructions'; $string['auth_shib_integrated_wayf'] = 'Moodle WAYF service'; $string['auth_shib_integrated_wayf_description'] = 'If you enable this, Moodle will use its own WAYF service instead of the one configured for Shibboleth. Moodle will display a drop-down list on this alternative login page where the user has to select his Identity Provider.'; $string['auth_shib_logout_return_url'] = 'Alternative logout return URL'; $string['auth_shib_logout_return_url_description'] = 'Provide the URL that Shibboleth users shall be redirected to after logging out.
If left empty, users will be redirected to the location that moodle will redirect users to'; $string['auth_shib_logout_url'] = 'Shibboleth Service Provider logout handler URL'; $string['auth_shib_logout_url_description'] = 'Provide the URL to the Shibboleth Service Provider logout handler. This typically is /Shibboleth.sso/Logout'; $string['auth_shib_no_organizations_warning'] = 'If you want to use the integrated WAYF service, you must provide a coma-separated list of Identity Provider entityIDs, their names and optionally a session initiator.'; $string['auth_shib_only'] = 'Shibboleth only'; $string['auth_shib_only_description'] = 'Check this option if a Shibboleth authentication shall be enforced'; $string['auth_shib_username_description'] = 'Name of the webserver Shibboleth environment variable that shall be used as Moodle username'; $string['shib_invalid_account_error'] = 'You seem to be Shibboleth authenticated but Moodle has no valid account for your username. Your account may not exist or it may have been suspended.'; $string['shib_no_attributes_error'] = 'You seem to be Shibboleth authenticated but Moodle didn\'t receive any user attributes. Please check that your Identity Provider releases the necessary attributes ({$a}) to the Service Provider Moodle is running on or inform the webmaster of this server.'; $string['shib_not_all_attributes_error'] = 'Moodle needs certain Shibboleth attributes which are not present in your case. The attributes are: {$a}
Please contact the webmaster of this server or your Identity Provider.'; $string['shib_not_set_up_error'] = 'Shibboleth authentication doesn\'t seem to be set up correctly because no Shibboleth environment variables are present for this page. Please consult the README for further instructions on how to set up Shibboleth authentication or contact the webmaster of this Moodle installation.'; $string['pluginname'] = 'Shibboleth'; $string['privacy:metadata'] = 'The Shibboleth authentication plugin does not store any personal data.'; PK&9\CtIshibboleth/login.phpnu[dirroot."/auth/shibboleth/auth.php"); $idp = optional_param('idp', null, PARAM_RAW); // Check for timed out sessions. if (!empty($SESSION->has_timed_out)) { $session_has_timed_out = true; $SESSION->has_timed_out = false; } else { $session_has_timed_out = false; } // Define variables used in page. $isvalid = true; $site = get_site(); $loginsite = get_string("loginsite"); $loginurl = (!empty($CFG->alternateloginurl)) ? $CFG->alternateloginurl : ''; $config = get_config('auth_shibboleth'); if (!empty($CFG->registerauth) or is_enabled_auth('none') or !empty($config->auth_instructions)) { $showinstructions = true; } else { $showinstructions = false; } $idplist = get_idp_list($config->organization_selection); if (isset($idp)) { if (isset($idplist[$idp])) { set_saml_cookie($idp); $targeturl = new moodle_url('/auth/shibboleth/index.php'); $idpinfo = $idplist[$idp]; // Redirect to SessionInitiator with entityID as argument. if (isset($idpinfo[1]) && !empty($idpinfo[1])) { $sso = $idpinfo[1]; } else { $sso = '/Shibboleth.sso'; } // For Shibboleth 1.x Service Providers. header('Location: ' . $sso . '?providerId=' . urlencode($idp) . '&target=' . urlencode($targeturl->out())); } else { $isvalid = false; } } $loginsite = get_string("loginsite"); $PAGE->set_url('/auth/shibboleth/login.php'); $PAGE->set_context(\core\context\system::instance()); $PAGE->navbar->add($loginsite); $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); $PAGE->set_pagelayout('login'); echo $OUTPUT->header(); if (isloggedin() and !isguestuser()) { // Prevent logging when already logged in, we do not want them to relogin by accident because sesskey would be changed. echo $OUTPUT->box_start(); $params = array('sesskey' => sesskey(), 'loginpage' => 1); $logout = new single_button(new moodle_url('/login/logout.php', $params), get_string('logout'), 'post'); $continue = new single_button(new moodle_url('/'), get_string('cancel'), 'get'); echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue); echo $OUTPUT->box_end(); } else { // Print login page. $selectedidp = '-'; if (isset($_COOKIE['_saml_idp'])) { $idpcookie = generate_cookie_array($_COOKIE['_saml_idp']); do { $selectedidp = array_pop($idpcookie); } while (!isset($idplist[$selectedidp]) && count($idpcookie) > 0); } $idps = []; foreach ($idplist as $value => $data) { $name = reset($data); $selected = $value === $selectedidp; $idps[] = (object)[ 'name' => $name, 'value' => $value, 'selected' => $selected ]; } // Whether the user can sign up. $cansignup = !empty($CFG->registerauth); // Default instructions. $instructions = format_text($config->auth_instructions); if (is_enabled_auth('none')) { $instructions = get_string('loginstepsnone'); } else if ($cansignup) { if ($CFG->registerauth === 'email' && empty($instructions)) { $instructions = get_string('loginsteps'); } } // Build the template context data. $templatedata = (object)[ 'adminemail' => get_admin()->email, 'cansignup' => $cansignup, 'guestlogin' => $CFG->guestloginbutton, 'guestloginurl' => new moodle_url('/login/index.php'), 'idps' => $idps, 'instructions' => $instructions, 'loginname' => format_string($config->login_name ?? '', options: ['context' => $PAGE->context]), 'logintoken' => \core\session\manager::get_login_token(), 'loginurl' => new moodle_url('/auth/shibboleth/login.php'), 'showinstructions' => $showinstructions, 'signupurl' => new moodle_url('/login/signup.php'), 'isvalid' => $isvalid ]; // Render the login form. echo $OUTPUT->render_from_template('auth_shibboleth/login_form', $templatedata); } echo $OUTPUT->footer(); PK&9\ b!!shibboleth/lib.phpnu[. /** * This file contains the hooks for the Shibboleth authentication module. * * @package auth_shibboleth * @copyright 2018 Fabrice Ménard * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Serves the logo file settings. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @param string $filearea file area * @param array $args extra arguments * @param bool $forcedownload whether or not force download * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - justsend the file */ function auth_shibboleth_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) { if ($context->contextlevel != CONTEXT_SYSTEM) { return false; } if ($filearea !== 'logo' ) { return false; } $itemid = 0; $filename = array_pop($args); if (!$args) { $filepath = '/'; } else { $filepath = '/'.implode('/', $args).'/'; } $fs = get_file_storage(); $file = $fs->get_file($context->id, 'auth_shibboleth', $filearea, $itemid, $filepath, $filename); if (!$file) { return false; } send_stored_file($file, null, 0, $forcedownload, $options); } PK&9\kSshibboleth/version.phpnu[. /** * Version information * * @package auth_shibboleth * @author Martin Dougiamas * @author Lukas Haemmerle * @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_shibboleth'; // Full name of the plugin (used for diagnostics) PK&9\³'shibboleth/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_shibboleth. * * @package auth_shibboleth * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_shibboleth\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_shibboleth implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\(R 8shibboleth/classes/admin_setting_special_wayf_select.phpnu[. /** * Special settings for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special settings for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_special_wayf_select extends admin_setting_configselect { /** * Calls parent::__construct with specific arguments. */ public function __construct() { $yesno = array(); $yesno['off'] = new lang_string('no'); $yesno['on'] = new lang_string('yes'); parent::__construct('auth_shibboleth/alt_login', new lang_string('auth_shib_integrated_wayf', 'auth_shibboleth'), new lang_string('auth_shib_integrated_wayf_description', 'auth_shibboleth'), 'off', $yesno); } /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { global $CFG; // Overwrite alternative login URL if integrated WAYF is used. if (isset($data) && $data == 'on') { set_config('alt_login', $data, 'auth_shibboleth'); set_config('alternateloginurl', $CFG->wwwroot.'/auth/shibboleth/login.php'); } else { // Check if integrated WAYF was enabled and is now turned off. // If it was and only then, reset the Moodle alternate URL. $oldsetting = get_config('auth_shibboleth', 'alt_login'); if (isset($oldsetting) and $oldsetting == 'on') { set_config('alt_login', 'off', 'auth_shibboleth'); set_config('alternateloginurl', ''); } $data = 'off'; } return parent::write_setting($data); } } PK&9\_}9k k Dshibboleth/classes/admin_setting_special_convert_data_configfile.phpnu[. /** * Special setting for auth_shibboleth convert_data. * * @package auth_shibboleth * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Admin settings class for the convert_data option. * * @package auth_shibboleth * @copyright 2020 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_convert_data extends admin_setting_configfile { /** * Constructor. * * @param string $name * @param string $visiblename * @param string $description * @param mixed $defaultdirectory */ public function __construct($name, $visiblename, $description, $defaultdirectory) { parent::__construct($name, $visiblename, $description, $defaultdirectory); } /** * Validate the file path (location). * * This method ensures that the file defined as a data modification API exists and is not located in the site * data directory ($CFG->dataroot). We should prohibit using files from the site data directory as this introduces * security vulnerabilities. * * @param string $filepath The path to the file. * @return mixed bool true for success or string:error on failure. */ public function validate($filepath) { global $CFG; if (empty($filepath)) { return true; } // Fail if the file does not exist or it is not readable by the webserver process. if (!is_readable($filepath)) { return get_string('auth_shib_convert_data_warning', 'auth_shibboleth'); } // Fail if the absolute file path matches the currently defined dataroot path. if (preg_match('/' . preg_quote($CFG->dataroot, '/') . '/', realpath($filepath))) { return get_string('auth_shib_convert_data_filepath_warning', 'auth_shibboleth'); } return true; } } PK&9\ ?shibboleth/classes/admin_setting_special_idp_configtextarea.phpnu[. /** * Special setting for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_shibboleth WAYF. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_shibboleth_admin_setting_special_idp_configtextarea extends admin_setting_configtextarea { /** * Calls parent::__construct with specific arguments. */ public function __construct() { $default = $orgdefault = "urn:mace:organization1:providerID, Example Organization 1 https://another.idp-id.com/shibboleth, Other Example Organization, /Shibboleth.sso/DS/SWITCHaai urn:mace:organization2:providerID, Example Organization 2, /Shibboleth.sso/WAYF/SWITCHaai"; parent::__construct('auth_shibboleth/organization_selection', get_string('auth_shib_idp_list', 'auth_shibboleth'), get_string('auth_shib_idp_list_description', 'auth_shibboleth'), $default, PARAM_RAW, '60', '8'); } /** * We need to overwrite the global "alternate login url" setting if wayf is enabled. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { global $CFG; $login = get_config('auth_shibboleth', 'alt_login'); if (isset($data) && !empty($data) && isset($login) && $login == 'on') { // Need to use the get_idp_list() function here. require_once($CFG->dirroot.'/auth/shibboleth/auth.php'); $idplist = get_idp_list($data); if (count($idplist) < 1) { return false; } $data = ''; foreach ($idplist as $idp => $value) { $data .= $idp.', '.$value[0]; if (isset($value[1])) { // Value[1] is optional. $data .= ', '.$value[1] . "\n"; } else { $data .= "\n"; } } } return parent::write_setting($data); } } PK&9\=shibboleth/classes/helper.phpnu[. /** * Contains a helper class for the Shibboleth authentication plugin. * * @package auth_shibboleth * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_shibboleth; defined('MOODLE_INTERNAL') || die(); /** * The helper class for the Shibboleth authentication plugin. * * @package auth_shibboleth * @copyright 2018 Mark Nelson * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Delete session of user using file sessions. * * @param string $spsessionid SP-provided Shibboleth Session ID * @return \SoapFault or void if everything was fine */ public static function logout_file_session($spsessionid) { global $CFG; if (!empty($CFG->session_file_save_path)) { $dir = $CFG->session_file_save_path; } else { $dir = $CFG->dataroot . '/sessions'; } if (is_dir($dir)) { if ($dh = opendir($dir)) { // Read all session files. while (($file = readdir($dh)) !== false) { // Check if it is a file. if (is_file($dir.'/'.$file)) { // Read session file data. $data = file($dir.'/'.$file); if (isset($data[0])) { $usersession = self::unserializesession($data[0]); // Check if we have found session that shall be deleted. if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) { // If there is a match, delete file. if ($usersession['SESSION']->shibboleth_session_id == $spsessionid) { // Delete session file. if (!unlink($dir.'/'.$file)) { return new SoapFault('LogoutError', 'Could not delete Moodle session file.'); } } } } } } closedir($dh); } } } /** * Delete session of user using DB sessions. * * @param string $spsessionid SP-provided Shibboleth Session ID */ public static function logout_db_session($spsessionid) { global $CFG, $DB; $sessions = $DB->get_records_sql( 'SELECT userid, sessdata FROM {sessions} WHERE timemodified > ?', array(time() - $CFG->sessiontimeout) ); foreach ($sessions as $session) { // Get user session from DB. $usersession = self::unserializesession(base64_decode($session->sessdata)); if (isset($usersession['SESSION']) && isset($usersession['SESSION']->shibboleth_session_id)) { // If there is a match, kill the session. if ($usersession['SESSION']->shibboleth_session_id == trim($spsessionid)) { // Delete this user's sessions. \core\session\manager::destroy_user_sessions($session->userid); } } } } /** * Unserialize a session string. * * @param string $serializedstring * @return array */ private static function unserializesession($serializedstring) { $variables = array(); $index = 0; // Find next delimiter after current index. It's key being the characters between those points. while ($delimiterpos = strpos($serializedstring, '|', $index)) { $key = substr($serializedstring, $index, $delimiterpos - $index); // Start unserializing immediately after the delimiter. PHP will read as much valid data as possible. $value = unserialize(substr($serializedstring, $delimiterpos + 1), ['allowed_classes' => ['stdClass']]); $variables[$key] = $value; // Advance index beyond the length of the previously captured serialized value. $index = $delimiterpos + 1 + strlen(serialize($value)); } return $variables; } } PK&9\#.9(shibboleth/templates/login_form.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_shibboleth/login_form Template for the Shibboleth authentication plugin's login form. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * adminemail String The Administrator's email address. * cansignup Boolean Whether a new user can sign up for an account. * guestlogin Boolean Whether to show the guest login section. * guestloginurl String The URL for guest login. * idps Array The list of identity providers for the Shibboleth authentication plugin in value-name pairs per IDP. * instructions String Signup instructions. * isvalid Boolean Whether form validation passes. * loginname String The custom login name. * logintoken String The login token. * loginurl String The login URL. * showinstructions Boolean Whether to show additional login instructions. * signupurl String The signup URL. Example context (json): { "loginurl": "#", "guestloginurl": "#", "guestlogin": true, "idps": [ { "value": 1, "name": "IDP 1" }, { "value": 2, "name": "IDP 2", "selected": true }, { "value": 3, "name": "IDP 3" } ], "showinstructions": true, "logintoken": "abcde", "adminemail": "admin@example.com", "loginname": "Shib auth", "cansignup": true, "signupurl": "#", "instructions": "Sign up here", "isvalid": false } }}

{{#loginname}}{{.}}{{/loginname}} {{^loginname}}{{#str}}auth_shibboleth_login_long, auth_shibboleth{{/str}}{{/loginname}}

{{#str}}auth_shibboleth_errormsg, auth_shibboleth{{/str}}

{{#str}}auth_shib_contact_administrator, auth_shibboleth, {{adminemail}}{{/str}}

{{#guestlogin}}

{{#str}}someallowguest, moodle{{/str}}

{{/guestlogin}}
{{#showinstructions}}

{{#str}}firsttime, moodle{{/str}}

{{{instructions}}}

{{#cansignup}}
{{/cansignup}}
{{/showinstructions}}
PK&9\j?shibboleth/logout.phpnu[dirroot."/auth/shibboleth/auth.php"); $action = optional_param('action', '', PARAM_ALPHA); $redirect = optional_param('return', '', PARAM_URL); // Find out whether host supports https $protocol = 'http://'; if (is_https()) { $protocol = 'https://'; } // If the shibboleth plugin is not enable, throw an exception. if (!is_enabled_auth('shibboleth')) { throw new moodle_exception(get_string('pluginnotenabled', 'auth', 'shibboleth')); } // Front channel logout. $inputstream = file_get_contents("php://input"); if ($action == 'logout' && !empty($redirect)) { if (isloggedin($USER) && $USER->auth == 'shibboleth') { // Logout user from application. require_logout(); } // Finally, send user to the return URL. redirect($redirect); } else if (!empty($inputstream)) { // Back channel logout. // Set SOAP header. $server = new SoapServer($protocol.$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'].'/LogoutNotification.wsdl'); $server->addFunction("LogoutNotification"); $server->handle(); } else { // Return WSDL. header('Content-Type: text/xml'); echo << WSDL; exit; } /******************************************************************************/ /** * Handles SOAP Back-channel logout notification * * @param string $spsessionid SP-provided Shibboleth Session ID * @return SoapFault or void if everything was fine */ function LogoutNotification($spsessionid) { $sessionclass = \core\session\manager::get_handler_class(); switch ($sessionclass) { case '\core\session\file': return \auth_shibboleth\helper::logout_file_session($spsessionid); case '\core\session\database': return \auth_shibboleth\helper::logout_db_session($spsessionid); default: throw new moodle_exception("Shibboleth logout not implemented for '$sessionclass'"); } // If no SoapFault was thrown, the function will return OK as the SP assumes. } PK&9\/@llshibboleth/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/shibboleth/*, information provided here is intended especially for developers. === 3.11 === * The 'Data modification API' (convert_data) setting can no longer be configured to use files located within the current site data directory ($CFG->dataroot), as it exposes the site to security risks. === 3.5.2 === * Moved the public function unserializesession in auth/shibboleth/logout.php to auth/shibboleth/classes/helper.php and made it private. This function should not have been used outside of this file. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/shibboleth' to 'auth_shibboleth'. PK&9\kz|shibboleth/index.phpnu[set_url('/auth/shibboleth/index.php'); $PAGE->set_context($context); // Support for WAYFless URLs. $target = optional_param('target', '', PARAM_LOCALURL); if (!empty($target) && empty($SESSION->wantsurl)) { $SESSION->wantsurl = $target; } if (isloggedin() && !isguestuser()) { // Nothing to do if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; /// Because it's an address in this site unset($SESSION->wantsurl); } else { $urltogo = $CFG->wwwroot.'/'; /// Go to the standard home page unset($SESSION->wantsurl); /// Just in case } redirect($urltogo); } $pluginconfig = get_config('auth_shibboleth'); $shibbolethauth = get_auth_plugin('shibboleth'); // Check whether Shibboleth is configured properly $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out(); if (empty($pluginconfig->user_attribute)) { throw new \moodle_exception('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl); } /// If we can find the Shibboleth attribute, save it in session and return to main login page if (!empty($_SERVER[$pluginconfig->user_attribute])) { // Shibboleth auto-login $frm = new stdClass(); $frm->username = strtolower($_SERVER[$pluginconfig->user_attribute]); // The password is never actually used, but needs to be passed to the functions 'user_login' and // 'authenticate_user_login'. Shibboleth returns true for the function 'prevent_local_password', which is // used when setting the password in 'update_internal_user_password'. When 'prevent_local_password' // returns true, the password is set to 'not cached' (AUTH_PASSWORD_NOT_CACHED) in the Moodle DB. However, // rather than setting the password to a hard-coded value, we will generate one each time, in case there are // changes to the Shibboleth plugin and it is actually used. $frm->password = generate_password(8); /// Check if the user has actually submitted login data to us $reason = null; if ($shibbolethauth->user_login($frm->username, $frm->password) && $user = authenticate_user_login($frm->username, $frm->password, false, $reason, false)) { complete_user_login($user); if (user_not_fully_set_up($USER, true)) { $urltogo = $CFG->wwwroot.'/user/edit.php?id='.$USER->id.'&course='.SITEID; // We don't delete $SESSION->wantsurl yet, so we get there later } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; /// Because it's an address in this site unset($SESSION->wantsurl); } else { $urltogo = $CFG->wwwroot.'/'; /// Go to the standard home page unset($SESSION->wantsurl); /// Just in case } /// Go to my-moodle page instead of homepage if defaulthomepage enabled if (!has_capability('moodle/site:config', context_system::instance()) and !empty($CFG->defaulthomepage) and !isguestuser()) { if ($urltogo == $CFG->wwwroot or $urltogo == $CFG->wwwroot.'/' or $urltogo == $CFG->wwwroot.'/index.php') { if ($CFG->defaulthomepage == HOMEPAGE_MY && !empty($CFG->enabledashboard)) { $urltogo = $CFG->wwwroot.'/my/'; } else if ($CFG->defaulthomepage == HOMEPAGE_MYCOURSES) { $urltogo = $CFG->wwwroot.'/my/courses.php'; } } } redirect($urltogo); exit; } else { // The Shibboleth user couldn't be mapped to a valid Moodle user throw new \moodle_exception('shib_invalid_account_error', 'auth_shibboleth'); } } // If we can find any (user independent) Shibboleth attributes but no user // attributes we probably didn't receive any user attributes elseif (!empty($_SERVER['HTTP_SHIB_APPLICATION_ID']) || !empty($_SERVER['Shib-Application-ID'])) { throw new \moodle_exception('shib_no_attributes_error', 'auth_shibboleth' , '', '\''.$pluginconfig->user_attribute.'\', \''.$pluginconfig->field_map_firstname.'\', \''. $pluginconfig->field_map_lastname.'\' and \''.$pluginconfig->field_map_email.'\''); } else { throw new \moodle_exception('shib_not_set_up_error', 'auth_shibboleth', '', $readmeurl); } PK&9\r55shibboleth/auth.phpnu[. /** * Authentication Plugin: Shibboleth Authentication * Authentication using Shibboleth. * * Distributed under GPL (c)Markus Hagman 2004-2006 * * @package auth_shibboleth * @author Martin Dougiamas * @author Lukas Haemmerle * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Shibboleth authentication plugin. */ class auth_plugin_shibboleth extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'shibboleth'; $this->config = get_config('auth_shibboleth'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_shibboleth() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * @return bool Authentication success or failure. */ function user_login($username, $password) { global $SESSION; // If we are in the shibboleth directory then we trust the server var if (!empty($_SERVER[$this->config->user_attribute])) { // Associate Shibboleth session with user for SLO preparation $sessionkey = ''; if (isset($_SERVER['Shib-Session-ID'])){ // This is only available for Shibboleth 2.x SPs $sessionkey = $_SERVER['Shib-Session-ID']; } else { // Try to find out using the user's cookie foreach ($_COOKIE as $name => $value){ if (preg_match('/_shibsession_/i', $name)){ $sessionkey = $value; } } } // Set shibboleth session ID for logout $SESSION->shibboleth_session_id = $sessionkey; return (strtolower($_SERVER[$this->config->user_attribute]) == strtolower($username)); } else { // If we are not, the user has used the manual login and the login name is // unknown, so we return false. return false; } } /** * Returns the user information for 'external' users. In this case the * attributes provided by Shibboleth * * @return array $result Associative array of user data */ function get_userinfo($username) { // reads user information from shibboleth attributes and return it in array() global $CFG; // Check whether we have got all the essential attributes if ( empty($_SERVER[$this->config->user_attribute]) ) { throw new \moodle_exception( 'shib_not_all_attributes_error', 'auth_shibboleth' , '', "'".$this->config->user_attribute."' ('".$_SERVER[$this->config->user_attribute]."'), '". $this->config->field_map_firstname."' ('".$_SERVER[$this->config->field_map_firstname]."'), '". $this->config->field_map_lastname."' ('".$_SERVER[$this->config->field_map_lastname]."') and '". $this->config->field_map_email."' ('".$_SERVER[$this->config->field_map_email]."')"); } $attrmap = $this->get_attributes(); $result = array(); $search_attribs = array(); foreach ($attrmap as $key=>$value) { // Check if attribute is present if (!isset($_SERVER[$value])){ $result[$key] = ''; continue; } // Make usename lowercase if ($key == 'username'){ $result[$key] = strtolower($this->get_first_string($_SERVER[$value])); } else { $result[$key] = $this->get_first_string($_SERVER[$value]); } } // Provide an API to modify the information to fit the Moodle internal // data representation if ( $this->config->convert_data && $this->config->convert_data != '' && is_readable($this->config->convert_data) ) { // Include a custom file outside the Moodle dir to // modify the variable $moodleattributes include($this->config->convert_data); } return $result; } /** * Returns array containg attribute mappings between Moodle and Shibboleth. * * @return array */ function get_attributes() { $configarray = (array) $this->config; $moodleattributes = array(); $userfields = array_merge($this->userfields, $this->get_custom_user_profile_fields()); foreach ($userfields as $field) { if (isset($configarray["field_map_$field"])) { $moodleattributes[$field] = $configarray["field_map_$field"]; } } $moodleattributes['username'] = $configarray["user_attribute"]; return $moodleattributes; } function prevent_local_passwords() { return true; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Whether shibboleth users can change their password or not. * * Shibboleth auth requires password to be changed on shibboleth server directly. * So it is required to have password change url set. * * @return bool true if there's a password url or false otherwise. */ function can_change_password() { if (!empty($this->config->changepasswordurl)) { return true; } else { return false; } } /** * Get password change url. * * @return moodle_url|null Returns URL to change password or null otherwise. */ function change_password_url() { if (!empty($this->config->changepasswordurl)) { return new moodle_url($this->config->changepasswordurl); } else { return null; } } /** * Hook for login page * */ function loginpage_hook() { global $SESSION, $CFG; // Prevent username from being shown on login page after logout $CFG->nolastloggedin = true; return; } /** * Hook for logout page * */ function logoutpage_hook() { global $SESSION, $redirect; // Only do this if logout handler is defined, and if the user is actually logged in via Shibboleth $logouthandlervalid = isset($this->config->logout_handler) && !empty($this->config->logout_handler); if (isset($SESSION->shibboleth_session_id) && $logouthandlervalid ) { // Check if there is an alternative logout return url defined if (isset($this->config->logout_return_url) && !empty($this->config->logout_return_url)) { // Set temp_redirect to alternative return url $temp_redirect = $this->config->logout_return_url; } else { // Backup old redirect url $temp_redirect = $redirect; } // Overwrite redirect in order to send user to Shibboleth logout page and let him return back $redirecturl = new moodle_url($this->config->logout_handler, array('return' => $temp_redirect)); $redirect = $redirecturl->out(); } } /** * Cleans and returns first of potential many values (multi-valued attributes) * * @param string $string Possibly multi-valued attribute from Shibboleth */ function get_first_string($string) { $list = explode( ';', $string); $clean_string = rtrim($list[0]); return $clean_string; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $OUTPUT; if (!isset($this->config->user_attribute) || empty($this->config->user_attribute)) { echo $OUTPUT->notification(get_string("shib_not_set_up_error", "auth_shibboleth", (new moodle_url('/auth/shibboleth/README.txt'))->out()), 'notifyproblem'); return; } if ($this->config->convert_data and $this->config->convert_data != '' and !is_readable($this->config->convert_data)) { echo $OUTPUT->notification(get_string("auth_shib_convert_data_warning", "auth_shibboleth"), 'notifyproblem'); return; } if (isset($this->config->organization_selection) && empty($this->config->organization_selection) && isset($this->config->alt_login) && $this->config->alt_login == 'on') { echo $OUTPUT->notification(get_string("auth_shib_no_organizations_warning", "auth_shibboleth"), 'notifyproblem'); return; } } /** * Return a list of identity providers to display on the login page. * * @param string $wantsurl The requested URL. * @return array List of arrays with keys url, iconurl and name. */ public function loginpage_idp_list($wantsurl) { $config = get_config('auth_shibboleth'); $result = []; // Before displaying the button check that Shibboleth is set-up correctly. if (empty($config->user_attribute)) { return $result; } $context = \core\context\system::instance(); if ($config->auth_logo) { $iconurl = moodle_url::make_pluginfile_url( $context->id, 'auth_shibboleth', 'logo', null, null, $config->auth_logo); } else { $iconurl = null; } $result[] = [ 'url' => new moodle_url('/auth/shibboleth/index.php'), 'iconurl' => $iconurl, 'name' => format_string($config->login_name ?? '', options: ['context' => $context]), ]; return $result; } } /** * Sets the standard SAML domain cookie that is also used to preselect * the right entry on the local way * * @param string $selectedIDP IDP identifier */ function set_saml_cookie($selectedIDP) { if (isset($_COOKIE['_saml_idp'])) { $IDPArray = generate_cookie_array($_COOKIE['_saml_idp']); } else { $IDPArray = array(); } $IDPArray = appendCookieValue($selectedIDP, $IDPArray); setcookie ('_saml_idp', generate_cookie_value($IDPArray), time() + (100*24*3600)); } /** * Generate array of IdPs from Moodle Shibboleth settings * * @param string Text containing tuble/triple of IdP entityId, name and (optionally) session initiator * @return array Identifier of IdPs and their name/session initiator */ function get_idp_list($organization_selection) { $idp_list = array(); $idp_raw_list = explode("\n", $organization_selection); foreach ($idp_raw_list as $idp_line){ $idp_data = explode(',', $idp_line); if (isset($idp_data[2])) { $idp_list[trim($idp_data[0])] = array(trim($idp_data[1]),trim($idp_data[2])); } elseif(isset($idp_data[1])) { $idp_list[trim($idp_data[0])] = array(trim($idp_data[1])); } } return $idp_list; } /** * Generates an array of IDPs using the cookie value * * @param string Value of SAML domain cookie * @return array Identifiers of IdPs */ function generate_cookie_array($value) { // Decodes and splits cookie value $CookieArray = explode(' ', $value); $CookieArray = array_map('base64_decode', $CookieArray); return $CookieArray; } /** * Generate the value that is stored in the cookie using the list of IDPs * * @param array IdP identifiers * @return string SAML domain cookie value */ function generate_cookie_value($CookieArray) { // Merges cookie content and encodes it $CookieArray = array_map('base64_encode', $CookieArray); $value = implode(' ', $CookieArray); return $value; } /** * Append a value to the array of IDPs * * @param string IdP identifier * @param array IdP identifiers * @return array IdP identifiers with appended IdP */ function appendCookieValue($value, $CookieArray) { array_push($CookieArray, $value); $CookieArray = array_reverse($CookieArray); $CookieArray = array_unique($CookieArray); $CookieArray = array_reverse($CookieArray); return $CookieArray; } PK&9\n[shibboleth/settings.phpnu[. /** * Admin settings and defaults. * * @package auth_shibboleth * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_wayf_select.php'); require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_idp_configtextarea.php'); require_once($CFG->dirroot.'/auth/shibboleth/classes/admin_setting_special_convert_data_configfile.php'); // Introductory explanation. $readmeurl = (new moodle_url('/auth/shibboleth/README.txt'))->out(); $settings->add(new admin_setting_heading('auth_shibboleth/pluginname', '', new lang_string('auth_shibbolethdescription', 'auth_shibboleth', $readmeurl))); // Username. $settings->add(new admin_setting_configtext('auth_shibboleth/user_attribute', get_string('username'), get_string('auth_shib_username_description', 'auth_shibboleth'), '', PARAM_RAW)); // Convert Data configuration file. $settings->add(new auth_shibboleth_admin_setting_convert_data('auth_shibboleth/convert_data', get_string('auth_shib_convert_data', 'auth_shibboleth'), get_string('auth_shib_convert_data_description', 'auth_shibboleth', $readmeurl), '')); // WAYF. $settings->add(new auth_shibboleth_admin_setting_special_wayf_select()); // Organization_selection. $settings->add(new auth_shibboleth_admin_setting_special_idp_configtextarea()); // Logout handler. $settings->add(new admin_setting_configtext('auth_shibboleth/logout_handler', get_string('auth_shib_logout_url', 'auth_shibboleth'), get_string('auth_shib_logout_url_description', 'auth_shibboleth'), '', PARAM_URL)); // Logout return URL. $settings->add(new admin_setting_configtext('auth_shibboleth/logout_return_url', get_string('auth_shib_logout_return_url', 'auth_shibboleth'), get_string('auth_shib_logout_return_url_description', 'auth_shibboleth'), '', PARAM_URL)); // Authentication method name. $settings->add(new admin_setting_configtext('auth_shibboleth/login_name', get_string('auth_shib_auth_method', 'auth_shibboleth'), get_string('auth_shib_auth_method_description', 'auth_shibboleth'), 'Shibboleth Login', PARAM_TEXT)); // Authentication method logo. $settings->add(new admin_setting_configstoredfile('auth_shibboleth/auth_logo', get_string('auth_shib_auth_logo', 'auth_shibboleth'), get_string('auth_shib_auth_logo_description', 'auth_shibboleth'), 'logo', 0, ['accepted_types' => ['image']])); // Login directions. $settings->add(new admin_setting_configtextarea('auth_shibboleth/auth_instructions', get_string('auth_shib_instructions_key', 'auth_shibboleth'), get_string('auth_shib_instructions_help', 'auth_shibboleth', $CFG->wwwroot.'/auth/shibboleth/index.php'), get_string('auth_shib_instructions', 'auth_shibboleth', $CFG->wwwroot.'/auth/shibboleth/index.php'), PARAM_RAW_TRIMMED)); // Password change URL. $settings->add(new admin_setting_configtext('auth_shibboleth/changepasswordurl', get_string('auth_shib_changepasswordurl', 'auth_shibboleth'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('shibboleth'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, '', true, false, $authplugin->get_custom_user_profile_fields()); } PK&9\s1AAshibboleth/README.txtnu[Shibboleth Authentication for Moodle ------------------------------------------------------------------------------- Requirements: - Shibboleth Service Provider 1.3 or newer. Recommended is 2.1 or newer. See documentation for your Shibboleth federation on how to set up Shibboleth. Changes: - 11. 2004: Created by Markus Hagman - 05. 2005: Modifications to login process by Martin Dougiamas - 05. 2005: Various extensions and fixes by Lukas Haemmerle - 06. 2005: Adaptions to new field locks and plugin config structures by Martin Langhoff and Lukas Haemmerle - 10. 2005: Added better error messages and moved text to language directories - 02. 2006: Simplified authentication so that authorization works properly Added instructions for IIS - 11. 2006: User capabilities are now loaded properly as of Moodle 1.7+ - 03. 2007: Adapted authentication method to Moodle 1.8 - 07. 2007: Fixed a but that caused problems with uppercase usernames - 10. 2007: Removed the requirement for email address, surname and given name attributes on request of Markus Hagman - 11. 2007: Integrated WAYF Service in Moodle - 12. 2008: Shibboleth 2.x and Single Logout support added - 1. 2008: Added logout hook and moved Shibboleth config strings to utf8 auth language files. - 3. 2009: Added various improvements and bug fixes reported by Ina M�ller from university Tuebingen and Peter Ellis of University of Washington - 4. 2009: Added another requirement for logout regarding the call back script - 6. 2009: Changed handler URL when integrated Discovery Service is used - 10. 2009: Fixed HTML entity preservation in Shibboleth settings Moodle Configuration with Dual login ------------------------------------------------------------------------------- 1. Protect the directory moodle/auth/shibboleth/index.php with Shibboleth. The page index.php in that directory actually logs in a Shibboleth user. For Apache you have to define a rule like the following in the Apache config: -- AuthType shibboleth ShibRequireSession On require valid-user -- To restrict access to Moodle, replace the access rule 'require valid-user' with something that fits your needs, e.g. 'require affiliation student'. For IIS you have protect the auth/shibboleth directory directly in the RequestMap of the Shibboleth configuration file (shibboleth.xml or shibboleth2.xml). -- ... -- Also see: https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPRequestMapper and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPAccessControl 2. As Moodle admin, go to the 'Administrations >> Users >> Authentication' and click on the the 'Shibboleth' settings. 3. Fill in the fields of the form. The fields 'Username', 'First name', 'Surname', etc. should contain the name of the environment variables of the Shibboleth attributes that you want to map onto the corresponding Moodle variable (e.g. 'Shib-Person-surname' for the person's last name, refer the Shibboleth documentation or the documentation of your Shibboleth federation for information on which attributes are available). Especially the 'Username' field is of great importance because this attribute is used for the Moodle authentication of Shibboleth users. ############################################################################# Shibboleth Attributes needed by Moodle: For Moodle to work properly Shibboleth should at least provide the attribute that is used as username in Moodle. It has to be unique for all Shibboleth Be aware that Moodle converts the username to lowercase. So, the overall behaviour of the username will be case-insensitive. All attributes used for moodle must obey a certain length, otherwise Moodle cuts off the ends. Consult the Moodle documentation for further information on the maximum lengths for each field in the user profile. ############################################################################# 4.a If you want Shibboleth as your only authentication method with an external Where Are You From (WAYF) Service , set the 'Alternate Login URL' in the 'Common settings' in 'Administrations >> Users >> Authentication Options' to the the URL of the file 'moodle/auth/shibboleth/index.php'. This will enforce Shibboleth login. 4.b If you want to use the Moodle integrated WAYF service, you have to activate it in the Moodle Shibboleth authentication settings by checking the 'Moodle WAYF Service' checkbox and providing a list of entity IDs in the 'Identity Providers' textarea together with a name and an optional SessionInitiator URL, which usually is an absolute or relative URL pointing to the same host. If no SessionInitiator URL is given, the default one '/Shibboleth.sso' (only works for Shibboleth 1.3.x) will be used. For Shibboleth 2.x you have to add '/Shibboleth.sso/DS' as a SessionInitiator. Also see https://wiki.shibboleth.net/confluence/display/SHIB/SessionInitiator and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSessionInitiator Important Note: If you upgraded from a previous version of Moodle and now want to use the integrated WAYF, you have to make sure that in step 1 only the index.php script in moodle/auth/shibboleth/ is protected but *not* the other scripts and especially not the login.php script. If you were using the integrated WAYF alread with Shibboleth 1.3, it could be that the integrated WAYF is not working anymore after you updated Moodle. The reason is that the implicitly set default SessionInitiator changed in Moodle as well as in Shibboleth. For Shibboleth 1.3 one therefore has to add /Shibboleth.sso as third parameter whereas this is /Shibboleth.sso/DS for Shibboleth 2.x. 5. Save the changes for the 'Shibboleth settings'. Important Note: If you went for 4.b (integrated WAYF service), saving the settings will overwrite the Moodle Alternate Login URL using the Moodle web root URL. 6. If you want to use Shibboleth in addition to another authentication method not using the integrated WAYF service from 4.b, change the 'Instructions' in 'Administrations >> Users >> Manage authentication' to contain a link to the moodle/auth/shibboleth/index.php file which is protected by Shibboleth (see step 1.) and causes the Shibboleth login procedure to start. You can also use HTML code in that field, e.g. to include an image as a Shibboleth login button. Note: As of now you cannot use dual login together with the integrated WAYF service provided by Moodle (4.b). 7. Save the authentication changes. How the Shibboleth authentication works -------------------------------------------------------------------------------- To get Shibboleth authenticated in Moodle a user basically must access the Shibboleth-protected page /auth/shibboleth/index.php. If Shibboleth is the only authentication method (see 4.a), this happens automatically when a user selects his home organization in the Moodle WAYF service or if the alternate login URL is configured to be the protected /auth/shibboleth/index.php Otherwise, the user has to click on the link on the dual login page you provided in step 5.b. Moodle basically checks whether the Shibboleth attribute that you mapped as the username is present. This attribute should only be present if a user is Shibboleth authenticated. If the user's Moodle account has not existed yet, it gets automatically created. To prevent that every Shibboleth user can access your Moodle site you have to adapt the 'require valid-user' line in your webserver's config (see step 1) to allow only specific users. If you defined some authorization rules in step 1, these are checked by Shibboleth itself. Only users who met these rules actually can access /auth/shibboleth/index.php and get logged in. You can use Shibboleth AND another authentication method (it was tested with manual login). So, if there are a few users that don't have a Shibboleth login, you could create manual accounts for them and they could use the manual login. For other authentication methods you first have to configure them and then set Shibboleth as your authentication method. Users can log in only via one authentication method unless they have two accounts in Moodle. Shibboleth dual login with custom login page -------------------------------------------------------------------------------- You can create a dual login page that better fits your needs. For this to work, you have to set up the two authentication methods (e.g. 'Manual Accounts' and 'Shibboleth') and specify an alternate login link to your own dual login page. On that page you basically need a link to the Shibboleth-protected page ('/auth/shibboleth/index.php') for the Shibboleth login and a form that sends 'username' and 'password' to moodle/login/index.php. Set this web page then als alternate login page. Consult the Moodle documentation for further instructions and requirements. How to customize the way the Shibboleth user data is used in Moodle -------------------------------------------------------------------------------- Among the Shibboleth settings in Moodle there is a field that should contain a path to a php file that can be used as data manipulation hook. You can use this if you want to further process the way your Shibboleth attributes are used in Moodle. Due to security reasons this file cannot be located within the current site data directory ($CFG->dataroot). Example 1: Your Shibboleth federation uses an attribute that specifies the user's preferred language, but the content of this attribute is not compatible with the Moodle data representation, e.g. the Shibboleth attribute contains 'German' but Moodle needs a two letter value like 'de'. Example 2: The country, city and street are provided in one Shibboleth attribute and you want these values to be used in the Moodle user profile. So You have to parse the corresponding attribute to fill the user fields. If you want to use this hook you have to be a skilled PHP programmer. It is strongly recommended that you take a look at the file moodle/auth/shibboleth/auth.php, especially the function 'get_userinfo' where this file is included. The context of the file is the same as within this login function. So you can directly edit the object $result. Example file: -- config->field_map_address] != '') { // $address contains something like 'SWITCH$Limmatquai 138$CH-8021 Zurich' // We want to split this up to get: // institution, street, zipcode, city and country $address = $_SERVER[$this->config->field_map_address]; list($institution, $street, $zip_city) = explode('$', $address); preg_match('/ (.+)/', $zip_city, $regs); $city = $regs[1]; preg_match('/(.+)-/',$zip_city, $regs); $country = $regs[1]; $result["address"] = $street; $result["city"] = $city; $result["country"] = $country; $result["department"] = $institution; $result["description"] = "I am a Shibboleth user"; } ?> -- How to upgrade your Service Provider to 2.x ------------------------------------------------------------------------------- In case your upgrade your Service Provider 1.3.x to 2.x, be aware of the fact that in version 2.0 the default behaviour regarding attribute propagation changed. While the Service Provider 1.3.x published the Shibboleth attributes to the web server environment as HTTP Request headers, the Service Provider 2.x publishes attributes as environment variables, which increases the security for some platforms. However, this change has the effect that the attribute names change. E.g. while the surname attribute was published as 'HTTP_SHIB_PERSON_SURNAME' with 1.3.x, this attribute will be available in $_SERVER['Shib-Person-surname'] or depending on your /etc/shibboleth/attribute-map.xml file just as $_SERVER['sn']. Because Moodle needs to know what Shibboleth attributes it shall map onto which Moodle user profile field, one has to make sure the mapping is updated as well after the Service Provider upgrade. ******************************************************************************** Because you risk locking yourself out of Moodle it is strongly recommended to use the following approach when upgrading the Service Provider: 1. Enable manual authentication before the upgrade. 2. Make sure that you have at least one manual account with administration privileges working before upgrading your Service Provider to 2.x. 3. After the SP upgrade, use this account to log into Moodle and adapt the attribute mapping in 'Site Administration -> Users -> Shibboleth' to reflect the changed attribute names. You find the attribute names in the file /etc/shibboleth/attribute-map.xml listed as the 'id' value of an attribute definition. 4. If you are using the integrated WAYF, you may have to set the third parameter of each entry to '/Shibboleth.sso/DS' 5. Test the login with a Shibboleth account 6. If all is working, disable manual authentication again ******************************************************************************** How to add logout support -------------------------------------------------------------------------------- In order make Moodle support Shibboleth logout, one has to make the Shibboleth Service Provider (SP) aware of the Moodle logout capability. Only then the SP can trigger Moodle's front or back channel logout handler. To make the SP aware of the Moodle logout, you have to add the following to the Shibboleth main configuration file shibboleth2.xml (usually in /etc/shibboleth/) just before the element. -- -- Then restart the Shibboleth daemon and check the log file for errors. If there were no errors, you can test the logout feature by accessing Moodle, authenticating via Shibboleth and the access the URL: #YOUR_MOODLE_HOSTNAME#/Shibboleth.sso/Logout (assuming you have a standard Shibboleth installation). If everything worked well, you should see a Shibboleth page saying that you were successfully logged out and if you go back to Moodle you also should be logged out from Moodle. Requirements: - PHP needs the Soap Extension, which maybe must installed manually: More information is available here http://ch.php.net/soap - Logout only works with Shibboleth Service Provider 2.1 or higher - /moodle/auth/shibboleth/logout.php *must not* be protected by Shibboleth! In case all of Moodle is protected with Shibboleth, you have to add something like this to your Apache configuration after all the other require rules -- AuthType shibboleth ShibRequireSession Off require shibboleth -- When using IIS, the same can be achieved by something like: -- -- in the shibboleth2.xml RequestMap. Limitations: Single Logout is only supported when SAML2 is used at the SP and the IdP. As of October 2009, the Shibboleth Identity Provider 2.1.4 does not yet support Single Logout (SLO). Therefore, the single logout feature cannot be used yet in a Shibboleth only setup but there may be other SAML2 products that could be used as Identity Provider, e.g. SimpleSAML PHP. One of the reasons why SLO isn't supported yet is because there aren't many applications yet that were adapted to support front and back channel logout. Hopefully, the Moodle logout helps to motivate the developers to implement SLO. On the other hand, the easiest and safest way to log out still is to tell users to quit their web browsers :) Also see https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues and https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPLogoutInitiator for some background information on this topic. -------------------------------------------------------------------------------- In case of problems and questions with Shibboleth authentication, contact Lukas Haemmerle or Markus Hagman PK&9\(rlloauth2/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 ) ), ]; PK&9\OVVoauth2/db/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; } PK&9\8oauth2/db/install.xmlnu[
PK&9\Loauth2/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', ], ]; PK&9\oauth2/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'; PK&9\gfnoauth2/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.'); } PK&9\S" " oauth2/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(); PK&9\@oauth2/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]); } PK&9\oauth2/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). PK&9\t>u oauth2/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/"); PK&9\199oauth2/classes/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; } } PK&9\MJ% #oauth2/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]); } } PK&9\Eloauth2/classes/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); } } PK&9\ 8 8 "oauth2/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); } } PK&9\=bhhoauth2/classes/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 ]; } } PK&9\Q)oauth2/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}}

PK&9\墺EE%oauth2/templates/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}}}
PK&9\9ppoauth2/linkedlogins.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(); PK&9\7b oauth2/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/"); PK&9\z'--&oauth2/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); } } PK&9\<oauth2/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'])); } } PK&9\?7((oauth2/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)); } } PK&9\P(oauth2/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" PK&9\X)-77oauth2/auth.phpnu[. /** * Open ID authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for oauth2 authentication. * * @package auth_oauth2 * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ class auth_plugin_oauth2 extends \auth_oauth2\auth { /** * Test the various configured Oauth2 providers. */ public function test_settings() { global $OUTPUT; $authplugin = get_auth_plugin('oauth2'); $idps = $authplugin->loginpage_idp_list(''); $templateidps = []; if (empty($idps)) { echo $OUTPUT->notification(get_string('noconfiguredidps', 'auth_oauth2'), 'notifyproblem'); return; } else { foreach ($idps as $idp) { $idpid = $idp['url']->get_param('id'); $sesskey = $idp['url']->get_param('sesskey'); $testurl = new moodle_url('/auth/oauth2/test.php', ['id' => $idpid, 'sesskey' => $sesskey]); $templateidps[] = ['name' => $idp['name'], 'url' => $testurl->out(), 'iconurl' => $idp['iconurl']]; } echo $OUTPUT->render_from_template('auth_oauth2/idps', ['idps' => $templateidps]); } } } PK&9\@+dGGoauth2/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&9\ nologin/lang/en/auth_nologin.phpnu[. /** * Strings for component 'auth_nologin', language 'en'. * * @package auth_nologin * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_nologindescription'] = 'Auxiliary plugin that prevents user to login into system and also discards any mail sent to the user. Can be used to suspend user accounts.'; $string['pluginname'] = 'No login'; $string['privacy:metadata'] = 'The No login authentication plugin does not store any personal data.'; PK&9\"nologin/version.phpnu[. /** * Version information * * @package auth_nologin * @copyright 2011 Petr Skoda (http://skodak.org) * @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_nologin'; // Full name of the plugin (used for diagnostics) PK&9\g$nologin/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_nologin. * * @package auth_nologin * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_nologin\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_nologin implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\0 nologin/auth.phpnu[. /** * Nologin authentication login - prevents user login. * * @package auth_nologin * @author Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for no authentication - disabled user. */ class auth_plugin_nologin extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'nologin'; } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_nologin() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Do not allow any login. * */ function user_login($username, $password) { return false; } /** * No password updates. */ function user_update_password($user, $newpassword) { return false; } function prevent_local_passwords() { // just in case, we do not want to loose the passwords return false; } /** * No external data sync. * * @return bool */ function is_internal() { //we do not know if it was internal or external originally return true; } /** * No changing of password. * * @return bool */ function can_change_password() { return false; } /** * No password resetting. */ function can_reset_password() { return false; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns information on how the specified user can change their password. * User accounts with authentication type set to nologin are disabled accounts. * They cannot change their password. * * @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('emailpasswordchangeinfodisabled', '', $data); $subject = get_string('emailpasswordchangeinfosubject', '', format_string($site->fullname)); return [ 'subject' => $subject, 'message' => $message ]; } } PK&9\cW:W:classes/external.phpnu[. /** * Auth external API * * @package core_auth * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/authlib.php'); /** * Auth external functions * * @package core_auth * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class core_auth_external extends external_api { /** * Describes the parameters for confirm_user. * * @return external_function_parameters * @since Moodle 3.2 */ public static function confirm_user_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'User name'), 'secret' => new external_value(core_user::get_property_type('secret'), 'Confirmation secret'), ) ); } /** * Confirm a user account. * * @param string $username user name * @param string $secret confirmation secret (random string) used for validating the confirm request * @return array warnings and success status (true if the user was confirmed, false if he was already confirmed) * @since Moodle 3.2 * @throws moodle_exception */ public static function confirm_user($username, $secret) { global $PAGE; $warnings = array(); $params = self::validate_parameters( self::confirm_user_parameters(), array( 'username' => $username, 'secret' => $secret, ) ); $context = context_system::instance(); $PAGE->set_context($context); if (!$authplugin = signup_get_user_confirmation_authplugin()) { throw new moodle_exception('confirmationnotenabled'); } $confirmed = $authplugin->user_confirm($username, $secret); if ($confirmed == AUTH_CONFIRM_ALREADY) { $success = false; $warnings[] = array( 'item' => 'user', 'itemid' => 0, 'warningcode' => 'alreadyconfirmed', 'message' => s(get_string('alreadyconfirmed')) ); } else if ($confirmed == AUTH_CONFIRM_OK) { $success = true; } else { throw new moodle_exception('invalidconfirmdata'); } $result = array( 'success' => $success, 'warnings' => $warnings, ); return $result; } /** * Describes the confirm_user return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function confirm_user_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'True if the user was confirmed, false if he was already confirmed'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for request_password_reset. * * @return external_function_parameters * @since Moodle 3.4 */ public static function request_password_reset_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'User name', VALUE_DEFAULT, ''), 'email' => new external_value(core_user::get_property_type('email'), 'User email', VALUE_DEFAULT, ''), ) ); } /** * Requests a password reset. * * @param string $username user name * @param string $email user email * @return array warnings and success status (including notices and errors while processing) * @since Moodle 3.4 * @throws moodle_exception */ public static function request_password_reset($username = '', $email = '') { global $CFG, $PAGE; require_once($CFG->dirroot . '/login/lib.php'); $warnings = array(); $params = self::validate_parameters( self::request_password_reset_parameters(), array( 'username' => $username, 'email' => $email, ) ); $context = context_system::instance(); $PAGE->set_context($context); // Needed by format_string calls. // Check if an alternate forgotten password method is set. if (!empty($CFG->forgottenpasswordurl)) { throw new moodle_exception('cannotmailconfirm'); } $errors = core_login_validate_forgot_password_data($params); if (!empty($errors)) { $status = 'dataerror'; $notice = ''; foreach ($errors as $itemname => $message) { $warnings[] = array( 'item' => $itemname, 'itemid' => 0, 'warningcode' => 'fielderror', 'message' => s($message) ); } } else { list($status, $notice, $url) = core_login_process_password_reset($params['username'], $params['email']); } return array( 'status' => $status, 'notice' => $notice, 'warnings' => $warnings, ); } /** * Describes the request_password_reset return value. * * @return external_single_structure * @since Moodle 3.4 */ public static function request_password_reset_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_ALPHANUMEXT, 'The returned status of the process: dataerror: Error in the sent data (username or email). More information in warnings field. emailpasswordconfirmmaybesent: Email sent or not (depends on user found in database). emailpasswordconfirmnotsent: Failure, user not found. emailpasswordconfirmnoemail: Failure, email not found. emailalreadysent: Email already sent. emailpasswordconfirmsent: User pending confirmation. emailresetconfirmsent: Email sent. '), 'notice' => new external_value(PARAM_RAW, 'Important information for the user about the process.'), 'warnings' => new external_warnings(), ) ); } /** * Describes the parameters for the digital minor check. * * @return external_function_parameters * @since Moodle 3.4 */ public static function is_minor_parameters() { return new external_function_parameters( array( 'age' => new external_value(PARAM_INT, 'Age'), 'country' => new external_value(PARAM_ALPHA, 'Country of residence'), ) ); } /** * Requests a check if a user is digital minor. * * @param int $age User age * @param string $country Country of residence * @return array status (true if the user is a minor, false otherwise) * @since Moodle 3.4 * @throws moodle_exception */ public static function is_minor($age, $country) { global $CFG, $PAGE; require_once($CFG->dirroot . '/login/lib.php'); $params = self::validate_parameters( self::is_minor_parameters(), array( 'age' => $age, 'country' => $country, ) ); if (!array_key_exists($params['country'], get_string_manager()->get_list_of_countries())) { throw new invalid_parameter_exception('Invalid value for country parameter (value: '. $params['country'] .')'); } $context = context_system::instance(); $PAGE->set_context($context); // Check if verification of age and location (minor check) is enabled. if (!\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { throw new moodle_exception('nopermissions', 'error', '', get_string('agelocationverificationdisabled', 'error')); } $status = \core_auth\digital_consent::is_minor($params['age'], $params['country']); return array( 'status' => $status ); } /** * Describes the is_minor return value. * * @return external_single_structure * @since Moodle 3.4 */ public static function is_minor_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the user is considered to be a digital minor, false if not') ) ); } /** * Describes the parameters for is_age_digital_consent_verification_enabled. * * @return external_function_parameters * @since Moodle 3.3 */ public static function is_age_digital_consent_verification_enabled_parameters() { return new external_function_parameters(array()); } /** * Checks if age digital consent verification is enabled. * * @return array status (true if digital consent verification is enabled, false otherwise.) * @since Moodle 3.3 * @throws moodle_exception */ public static function is_age_digital_consent_verification_enabled() { global $PAGE; $context = context_system::instance(); $PAGE->set_context($context); $status = false; // Check if verification is enabled. if (\core_auth\digital_consent::is_age_digital_consent_verification_enabled()) { $status = true; } return array( 'status' => $status ); } /** * Describes the is_age_digital_consent_verification_enabled return value. * * @return external_single_structure * @since Moodle 3.3 */ public static function is_age_digital_consent_verification_enabled_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if digital consent verification is enabled, false otherwise.') ) ); } /** * Describes the parameters for resend_confirmation_email. * * @return external_function_parameters * @since Moodle 3.6 */ public static function resend_confirmation_email_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'Username.'), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password.'), 'redirect' => new external_value(PARAM_LOCALURL, 'Redirect the user to this site url after confirmation.', VALUE_DEFAULT, ''), ) ); } /** * Requests resend the confirmation email. * * @param string $username user name * @param string $password plain text password * @param string $redirect redirect the user to this site url after confirmation * @return array warnings and success status * @since Moodle 3.6 * @throws moodle_exception */ public static function resend_confirmation_email($username, $password, $redirect = '') { global $PAGE, $CFG; $warnings = array(); $params = self::validate_parameters( self::resend_confirmation_email_parameters(), array( 'username' => $username, 'password' => $password, 'redirect' => $redirect, ) ); $context = context_system::instance(); $PAGE->set_context($context); // Need by internal APIs. $username = trim(core_text::strtolower($params['username'])); $password = $params['password']; $user = core_user::get_user_by_username($username); if (!empty($user) && $user->confirmed) { if (!empty($CFG->protectusernames)) { throw new moodle_exception('invalidlogin'); } throw new moodle_exception('alreadyconfirmed'); } if (is_restored_user($username)) { if (!empty($CFG->protectusernames)) { throw new moodle_exception('invalidlogin'); } throw new moodle_exception('restoredaccountresetpassword', 'webservice'); } $user = authenticate_user_login($username, $password); if (empty($user)) { throw new moodle_exception('invalidlogin'); } // Check if we should redirect the user once the user is confirmed. $confirmationurl = null; if (!empty($params['redirect'])) { // Pass via moodle_url to fix thinks like admin links. $redirect = new moodle_url($params['redirect']); $confirmationurl = new moodle_url('/login/confirm.php', array('redirect' => $redirect->out())); } $status = send_confirmation_email($user, $confirmationurl); return array( 'status' => $status, 'warnings' => $warnings, ); } /** * Describes the resend_confirmation_email return value. * * @return external_single_structure * @since Moodle 3.6 */ public static function resend_confirmation_email_returns() { return new external_single_structure( array( 'status' => new external_value(PARAM_BOOL, 'True if the confirmation email was sent, false otherwise.'), 'warnings' => new external_warnings(), ) ); } } PK&9\1uhh)classes/form/verify_age_location_form.phpnu[. /** * Age and location verification mform. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\form; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); use moodleform; /** * Age and location verification mform class. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class verify_age_location_form extends moodleform { /** * Defines the form fields. */ public function definition() { global $CFG; $mform = $this->_form; $mform->addElement('text', 'age', get_string('whatisyourage'), array('optional' => false)); $mform->setType('age', PARAM_RAW); $mform->addRule('age', null, 'required', null, 'client'); $mform->addRule('age', null, 'numeric', null, 'client'); $countries = get_string_manager()->get_list_of_countries(); $defaultcountry[''] = get_string('selectacountry'); $countries = array_merge($defaultcountry, $countries); $mform->addElement('select', 'country', get_string('wheredoyoulive'), $countries); $mform->addRule('country', null, 'required', null, 'client'); $mform->setDefault('country', $CFG->country); $this->add_action_buttons(true, get_string('proceed')); } } PK&9\:g<<classes/privacy/provider.phpnu[. /** * Data provider. * * @package core_auth * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\privacy; defined('MOODLE_INTERNAL') || die(); use context; use core_privacy\local\metadata\collection; use core_privacy\local\request\transform; use core_privacy\local\request\writer; /** * Data provider class. * * @package core_auth * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @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\user_preference_provider { /** * Returns metadata. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_user_preference('auth_forcepasswordchange', 'privacy:metadata:userpref:forcepasswordchange'); $collection->add_user_preference('create_password', 'privacy:metadata:userpref:createpassword'); $collection->add_user_preference('login_failed_count', 'privacy:metadata:userpref:loginfailedcount'); $collection->add_user_preference('login_failed_count_since_success', 'privacy:metadata:userpref:loginfailedcountsincesuccess'); $collection->add_user_preference('login_failed_last', 'privacy:metadata:userpref:loginfailedlast'); $collection->add_user_preference('login_lockout', 'privacy:metadata:userpref:loginlockout'); $collection->add_user_preference('login_lockout_ignored', 'privacy:metadata:userpref:loginlockoutignored'); $collection->add_user_preference('login_lockout_secret', 'privacy:metadata:userpref:loginlockoutsecret'); return $collection; } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $yesno = function($v) { return transform::yesno($v); }; $datetime = function($v) { return $v ? transform::datetime($v) : null; }; $prefs = [ ['auth_forcepasswordchange', 'forcepasswordchange', $yesno], ['create_password', 'createpassword', $yesno], ['login_failed_count', 'loginfailedcount', null], ['login_failed_count_since_success', 'loginfailedcountsincesuccess', null], ['login_failed_last', 'loginfailedlast', $datetime], ['login_lockout', 'loginlockout', $datetime], ['login_lockout_ignored', 'loginlockoutignored', $yesno], ['login_lockout_secret', 'loginlockoutsecret', null], ]; foreach ($prefs as $prefdata) { list($prefname, $langkey, $transformer) = $prefdata; $value = get_user_preferences($prefname, null, $userid); if ($value === null) { continue; } writer::export_user_preference('core_auth', $prefname, $transformer ? $transformer($value) : $value, get_string("privacy:metadata:userpref:{$langkey}", 'core_auth')); } } } PK&9\£  +classes/output/verify_age_location_page.phpnu[. /** * Age and location verification renderable. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; require_once($CFG->libdir.'/formslib.php'); /** * Age and location verification renderable class. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class verify_age_location_page implements renderable, templatable { /** @var \moodleform The form object */ protected $form; /** @var string Error message */ protected $errormessage; /** * Constructor * * @param \moodleform $form The form object * @param string $errormessage The error message. */ public function __construct($form, $errormessage = null) { $this->form = $form; $this->errormessage = $errormessage; } /** * Export the page data for the mustache template. * * @param renderer_base $output renderer to be used to render the page elements. * @return \stdClass */ public function export_for_template(renderer_base $output) { global $SITE; $sitename = format_string($SITE->fullname); $formhtml = $this->form->render(); $error = $this->errormessage; $context = [ 'sitename' => $sitename, 'formhtml' => $formhtml, 'error' => $error ]; return $context; } } PK&9\Q:_classes/output/login.phpnu[. /** * Login renderable. * * @package core_auth * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; use context_system; use help_icon; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; /** * Login renderable class. * * @package core_auth * @copyright 2016 Frédéric Massart - FMCorz.net * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class login implements renderable, templatable { /** @var bool Whether to auto focus the form fields. */ public $autofocusform; /** @var bool Whether we can login as guest. */ public $canloginasguest; /** @var bool Whether we can login by e-mail. */ public $canloginbyemail; /** @var bool Whether we can sign-up. */ public $cansignup; /** @var help_icon The cookies help icon. */ public $cookieshelpicon; /** @var string The error message, if any. */ public $error; /** @var string The info message, if any. */ public $info; /** @var moodle_url Forgot password URL. */ public $forgotpasswordurl; /** @var array Additional identify providers, contains the keys 'url', 'name' and 'icon'. */ public $identityproviders; /** @var string Login instructions, if any. */ public $instructions; /** @var moodle_url The form action login URL. */ public $loginurl; /** @var moodle_url The sign-up URL. */ public $signupurl; /** @var string The user name to pre-fill the form with. */ public $username; /** @var string The language selector menu. */ public $languagemenu; /** @var string The csrf token to limit login to requests that come from the login form. */ public $logintoken; /** @var string Maintenance message, if Maintenance is enabled. */ public $maintenance; /** @var string ReCaptcha element HTML. */ public $recaptcha; /** @var bool Toggle the password visibility icon. */ public $togglepassword; /** @var bool Toggle the password visibility icon for small screens only. */ public $smallscreensonly; /** * Constructor. * * @param array $authsequence The enabled sequence of authentication plugins. * @param string $username The username to display. */ public function __construct(array $authsequence, $username = '') { global $CFG, $OUTPUT, $PAGE; $this->username = $username; $languagedata = new \core\output\language_menu($PAGE); $this->languagemenu = $languagedata->export_for_action_menu($OUTPUT); $this->canloginasguest = $CFG->guestloginbutton && !isguestuser(); $this->canloginbyemail = !empty($CFG->authloginviaemail); $this->cansignup = $CFG->registerauth == 'email' || !empty($CFG->registerauth); if ($CFG->rememberusername == 0) { $this->cookieshelpicon = new help_icon('cookiesenabledonlysession', 'core'); } else { $this->cookieshelpicon = new help_icon('cookiesenabled', 'core'); } $this->autofocusform = !empty($CFG->loginpageautofocus); $this->forgotpasswordurl = new moodle_url('/login/forgot_password.php'); $this->loginurl = new moodle_url('/login/index.php'); $this->signupurl = new moodle_url('/login/signup.php'); // Authentication instructions. $this->instructions = $CFG->auth_instructions; if (is_enabled_auth('none')) { $this->instructions = get_string('loginstepsnone'); } else if ($CFG->registerauth == 'email' && empty($this->instructions)) { $this->instructions = get_string('loginsteps', 'core', 'signup.php'); } if ($CFG->maintenance_enabled == true) { if (!empty($CFG->maintenance_message)) { $this->maintenance = $CFG->maintenance_message; } else { $this->maintenance = get_string('sitemaintenance', 'admin'); } } // Identity providers. $this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence); $this->logintoken = \core\session\manager::get_login_token(); // ReCaptcha. if (login_captcha_enabled()) { require_once($CFG->libdir . '/recaptchalib_v2.php'); $this->recaptcha = recaptcha_get_challenge_html(RECAPTCHA_API_URL, $CFG->recaptchapublickey); } // Toggle password visibility icon. $this->togglepassword = get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_ENABLED || get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY; $this->smallscreensonly = get_config('core', 'loginpasswordtoggle') == TOGGLE_SENSITIVE_SMALL_SCREENS_ONLY; } /** * Set the error message. * * @param string $error The error message. */ public function set_error($error) { $this->error = $error; } /** * Set the info message. * * @param string $info The info message. */ public function set_info(string $info): void { $this->info = $info; } public function export_for_template(renderer_base $output) { $identityproviders = \auth_plugin_base::prepare_identity_providers_for_output($this->identityproviders, $output); $data = new stdClass(); $data->autofocusform = $this->autofocusform; $data->canloginasguest = $this->canloginasguest; $data->canloginbyemail = $this->canloginbyemail; $data->cansignup = $this->cansignup; $data->cookieshelpicon = $this->cookieshelpicon->export_for_template($output); $data->error = $this->error; $data->info = $this->info; $data->forgotpasswordurl = $this->forgotpasswordurl->out(false); $data->hasidentityproviders = !empty($this->identityproviders); $data->hasinstructions = !empty($this->instructions) || $this->cansignup; $data->identityproviders = $identityproviders; list($data->instructions, $data->instructionsformat) = \core_external\util::format_text($this->instructions, FORMAT_MOODLE, context_system::instance()->id); $data->loginurl = $this->loginurl->out(false); $data->signupurl = $this->signupurl->out(false); $data->username = $this->username; $data->logintoken = $this->logintoken; $data->maintenance = format_text($this->maintenance, FORMAT_MOODLE); $data->languagemenu = $this->languagemenu; $data->recaptcha = $this->recaptcha; $data->togglepassword = $this->togglepassword; $data->smallscreensonly = $this->smallscreensonly; $data->showloginform = get_config('core', 'showloginform') === false || get_config('core', 'showloginform'); return $data; } } PK&9\dhtt%classes/output/digital_minor_page.phpnu[. /** * Digital minor renderable. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\output; defined('MOODLE_INTERNAL') || die(); use renderable; use renderer_base; use templatable; /** * Digital minor renderable class. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class digital_minor_page implements renderable, templatable { /** * Export the page data for the mustache template. * * @param renderer_base $output renderer to be used to render the page elements. * @return stdClass */ public function export_for_template(renderer_base $output) { global $SITE, $CFG; $sitename = format_string($SITE->fullname); $supportname = $CFG->supportname; $supportemail = $CFG->supportemail ?? null; $context = [ 'sitename' => $sitename, 'supportname' => $supportname, 'supportemail' => $supportemail, 'homelink' => new \moodle_url('/') ]; return $context; } } PK&9\classes/digital_consent.phpnu[. /** * Contains helper class for digital consent. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth; defined('MOODLE_INTERNAL') || die(); /** * Helper class for digital consent. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class digital_consent { /** * Returns true if age and location verification is enabled in the site. * * @return bool */ public static function is_age_digital_consent_verification_enabled() { global $CFG; return !empty($CFG->agedigitalconsentverification); } /** * Checks if a user is a digital minor. * * @param int $age * @param string $country The country code (ISO 3166-2) * @return bool */ public static function is_minor($age, $country) { global $CFG; $ageconsentmap = $CFG->agedigitalconsentmap; $agedigitalconsentmap = self::parse_age_digital_consent_map($ageconsentmap); return array_key_exists($country, $agedigitalconsentmap) ? $age < $agedigitalconsentmap[$country] : $age < $agedigitalconsentmap['*']; } /** * Parse the agedigitalconsentmap setting into an array. * * @param string $ageconsentmap The value of the agedigitalconsentmap setting * @return array $ageconsentmapparsed */ public static function parse_age_digital_consent_map($ageconsentmap) { $ageconsentmapparsed = array(); $countries = get_string_manager()->get_list_of_countries(true); $isdefaultvaluepresent = false; $lines = preg_split('/\r|\n/', $ageconsentmap, -1, PREG_SPLIT_NO_EMPTY); foreach ($lines as $line) { $arr = explode(",", $line); // Handle if there is more or less than one comma separator. if (count($arr) != 2) { throw new \moodle_exception('agedigitalconsentmapinvalidcomma', 'error', '', $line); } $country = trim($arr[0]); $age = trim($arr[1]); // Check if default. if ($country == "*") { $isdefaultvaluepresent = true; } // Handle if the presented value for country is not valid. if ($country !== "*" && !array_key_exists($country, $countries)) { throw new \moodle_exception('agedigitalconsentmapinvalidcountry', 'error', '', $country); } // Handle if the presented value for age is not valid. if (!is_numeric($age)) { throw new \moodle_exception('agedigitalconsentmapinvalidage', 'error', '', $age); } $ageconsentmapparsed[$country] = $age; } // Handle if a default value does not exist. if (!$isdefaultvaluepresent) { throw new \moodle_exception('agedigitalconsentmapinvaliddefault'); } return $ageconsentmapparsed; } } PK&9\__ upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/* - plugins, information provided here is intended especially for developers. === 4.4 === * A sesskey is no longer passed to the auth/test_settings.php page so it can no longer be required in test_settings(). * Auth plugins can now attempt to identify users running CLI scripts using a new method find_cli_user() === 4.2 === * Support for configuration with the deprecated auth_config.php file has been removed. * The moodle-auth-passwordunmask YUI module has been removed. Any uses should be replaced with core_form/passwordunmask. === 3.9 === * The following functions, previously used (exclusively) by upgrade steps are not available anymore because of the upgrade cleanup performed for this version. See MDL-65809 for more info: - upgrade_fix_config_auth_plugin_names() - upgrade_fix_config_auth_plugin_defaults() === 3.7 === * get_password_change_info() method is added to the base class and returns an array containing the subject and body of the message to the user that contains instructions on how to change their password. Authentication plugins can override this method if needed. === 3.6 === * Login forms generated from Moodle must include a login token to protect automated logins. See \core\session\manager::get_login_token(). === 3.5 === * The auth_db and auth_ldap plugins' implementations of update_user_record() have been removed and both now call the new implementation added in the base class. * Self registration plugins should use core_privacy\local\sitepolicy\manager instead of directly checking $CFG->sitepolicy , especially in custom signup forms. === 3.3 === * Authentication plugins have been migrated to use the admin settings API. Plugins should use a settings.php file to manage configurations rather than using the old config.html files. See how the helper function upgrade_fix_config_auth_plugin_names() can be used to convert the legacy settings to the new ones. Another helper function upgrade_fix_config_auth_plugin_defaults() can be used to populate the settings with default values so that they are not falsely reported as newly added ones. * The function 'print_auth_lock_options' has been replaced by 'display_auth_lock_options' which uses the admin settings API. See auth_manual as an exmple of how it can be used. More information can be found in MDL-12689. * The list of supported identity providers (SSO IdP) returned by the 'loginpage_idp_list' method (used to render the login page and login block links) now supports a new key 'iconurl' which should be used instead of the legacy 'icon'. === 3.2 === * New auth hook - pre_user_login_hook() - available, triggered right after the user object is created. This can be used to modify the user object before any authentication errors are raised. * The block_login now displays the loginpage_idp_list() links as well as main login page. * The authentication plugin auth_radius has been moved to https://github.com/moodlehq/moodle-auth_radius * New auth_email::user_signup_with_confirmation() method has a new optional parameter $confirmationurl to provide a different confirmation URL. * New signup_is_enabled() function available in lib/authlib.php to safely check if sign-up is enabled in the site. === 3.0 === * login_signup_form::signup_captcha_enabled() now calls is_captcha_enabled() from the current auth plugin instead of from auth_email === 2.9 === * Do not update user->firstaccess from any auth plugin, the complete_user_login() does it automatically. * Add user_add_password_history() to user_signup() method. * New auth hook - pre_loginpage_hook() - available, triggered before redirecting to the login page. === 2.8 === * \core\session\manager::session_exists() now verifies the session is active instead of only checking the session data is present in low level session handler * MNet is no longer sending logs between the client and parent sites. auth_plugin_mnet::refresh_log() is now deprecated. There is no alternative. Please don't use this function. === 2.7 === * If you are returning a url in method change_password_url() from config, please make sure it is set before trying to use it. === 2.6 === * can_be_manually_set() - This function was introduced in the base class and returns false by default. If overriden by an authentication plugin to return true, the authentication plugin will be able to be manually set for users. For example, when bulk uploading users you will be able to select it as the authentication method they use. === 2.4 === required changes in code: * use role_get_name() or role_fix_names() if you need any role names, using role.name directly from database is not correct any more optional - no changes needed: * add support for custom user signup form - see auth_plugin_base::signup_form() function === 2.2 === required changes in code: * the correct sequence to set up global $USER is: $user = get_complete_user_data('username', $username); // or $user = authenticate_user_login() enrol_check_plugins($user); session_set_user($user); PK&9\ I.,,email/db/services.phpnu[. /** * Auth email webservice definitions. * * @package auth_email * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = array( 'auth_email_get_signup_settings' => array( 'classname' => 'auth_email_external', 'methodname' => 'get_signup_settings', 'description' => 'Get the signup required settings and profile fields.', 'type' => 'read', 'ajax' => true, 'loginrequired' => false, ), 'auth_email_signup_user' => array( 'classname' => 'auth_email_external', 'methodname' => 'signup_user', 'description' => 'Adds a new user (pendingto be confirmed) in the site.', 'type' => 'write', 'ajax' => true, 'loginrequired' => false, ), ); PK&9\OS__email/db/upgrade.phpnu[. /** * No authentication plugin upgrade code * * @package auth_email * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_email. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_email_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; } PK&9\sEZeDDemail/lang/en/auth_email.phpnu[. /** * Strings for component 'auth_email', language 'en'. * * @package auth_email * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_emaildescription'] = '

Email-based self-registration enables a user to create their own account via a \'Create new account\' button on the login page. The user then receives an email containing a secure link to a page where they can confirm their account. Future logins just check the username and password against the stored values in the Moodle database.

Note: In addition to enabling the plugin, email-based self-registration must also be selected from the self registration drop-down menu on the \'Manage authentication\' page.

'; $string['auth_emailnoemail'] = 'Tried to send you an email but failed!'; $string['auth_emailrecaptcha'] = 'Adds a visual/audio confirmation form element to the sign-up page for email self-registering users. This protects your site against spammers and contributes to a worthwhile cause. See https://www.google.com/recaptcha for more details.'; $string['auth_emailrecaptcha_key'] = 'Enable reCAPTCHA element'; $string['auth_emailsettings'] = 'Settings'; $string['pluginname'] = 'Email-based self-registration'; $string['privacy:metadata'] = 'The Email-based self-registration authentication plugin does not store any personal data.'; PK&9\Temail/version.phpnu[. /** * Version details * * @package auth_email * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @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_email'; // Full name of the plugin (used for diagnostics) PK&9\AFAFemail/classes/external.phpnu[. /** * Auth e-mail external API * * @package auth_email * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ use core_external\external_api; use core_external\external_format_value; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/authlib.php'); require_once($CFG->dirroot . '/user/editlib.php'); require_once($CFG->dirroot . '/user/profile/lib.php'); /** * Auth e-mail external functions * * @package auth_email * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ class auth_email_external extends external_api { /** * Check if registration is enabled in this site. * * @throws moodle_exception * @since Moodle 3.2 */ protected static function check_signup_enabled() { global $CFG; if (empty($CFG->registerauth) or $CFG->registerauth != 'email') { throw new moodle_exception('registrationdisabled', 'error'); } } /** * Describes the parameters for get_signup_settings. * * @return external_function_parameters * @since Moodle 3.2 */ public static function get_signup_settings_parameters() { return new external_function_parameters(array()); } /** * Get the signup required settings and profile fields. * * @return array settings and possible warnings * @since Moodle 3.2 * @throws moodle_exception */ public static function get_signup_settings() { global $CFG, $PAGE; $context = context_system::instance(); // We need this to make work the format text functions. $PAGE->set_context($context); self::check_signup_enabled(); $result = array(); $result['namefields'] = useredit_get_required_name_fields(); if (!empty($CFG->passwordpolicy)) { $result['passwordpolicy'] = print_password_policy(); } $manager = new \core_privacy\local\sitepolicy\manager(); if ($sitepolicy = $manager->get_embed_url()) { $result['sitepolicy'] = $sitepolicy->out(false); } if (!empty($CFG->sitepolicyhandler)) { $result['sitepolicyhandler'] = $CFG->sitepolicyhandler; } if (!empty($CFG->defaultcity)) { $result['defaultcity'] = $CFG->defaultcity; } if (!empty($CFG->country)) { $result['country'] = $CFG->country; } $result['extendedusernamechars'] = !empty($CFG->extendedusernamechars); if ($fields = profile_get_signup_fields()) { $result['profilefields'] = array(); foreach ($fields as $field) { $fielddata = $field->object->get_field_config_for_external(); $fielddata['categoryname'] = \core_external\util::format_string($field->categoryname, $context->id); $fielddata['name'] = \core_external\util::format_string($fielddata['name'], $context->id); list($fielddata['defaultdata'], $fielddata['defaultdataformat']) = \core_external\util::format_text($fielddata['defaultdata'], $fielddata['defaultdataformat'], $context->id); $result['profilefields'][] = $fielddata; } } if (signup_captcha_enabled()) { // With reCAPTCHA v2 the captcha will be rendered by the mobile client using just the publickey. $result['recaptchapublickey'] = $CFG->recaptchapublickey; } $result['warnings'] = array(); return $result; } /** * Describes the get_signup_settings return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function get_signup_settings_returns() { return new external_single_structure( [ 'namefields' => new external_multiple_structure( new external_value(PARAM_NOTAGS, 'The order of the name fields') ), 'passwordpolicy' => new external_value(PARAM_RAW, 'Password policy', VALUE_OPTIONAL), 'sitepolicy' => new external_value(PARAM_RAW, 'Site policy', VALUE_OPTIONAL), 'sitepolicyhandler' => new external_value(PARAM_PLUGIN, 'Site policy handler', VALUE_OPTIONAL), 'defaultcity' => new external_value(PARAM_NOTAGS, 'Default city', VALUE_OPTIONAL), 'country' => new external_value(PARAM_ALPHA, 'Default country', VALUE_OPTIONAL), 'extendedusernamechars' => new external_value( PARAM_BOOL, 'Extended characters in usernames or not', VALUE_OPTIONAL ), 'profilefields' => new external_multiple_structure( new external_single_structure( [ 'id' => new external_value(PARAM_INT, 'Profile field id', VALUE_OPTIONAL), 'shortname' => new external_value(PARAM_ALPHANUMEXT, 'Profile field shortname', VALUE_OPTIONAL), 'name' => new external_value(PARAM_RAW, 'Profield field name', VALUE_OPTIONAL), 'datatype' => new external_value(PARAM_ALPHANUMEXT, 'Profield field datatype', VALUE_OPTIONAL), 'description' => new external_value(PARAM_RAW, 'Profield field description', VALUE_OPTIONAL), 'descriptionformat' => new external_format_value('description'), 'categoryid' => new external_value(PARAM_INT, 'Profield field category id', VALUE_OPTIONAL), 'categoryname' => new external_value(PARAM_RAW, 'Profield field category name', VALUE_OPTIONAL), 'sortorder' => new external_value(PARAM_INT, 'Profield field sort order', VALUE_OPTIONAL), 'required' => new external_value(PARAM_INT, 'Profield field required', VALUE_OPTIONAL), 'locked' => new external_value(PARAM_INT, 'Profield field locked', VALUE_OPTIONAL), 'visible' => new external_value(PARAM_INT, 'Profield field visible', VALUE_OPTIONAL), 'forceunique' => new external_value(PARAM_INT, 'Profield field unique', VALUE_OPTIONAL), 'signup' => new external_value(PARAM_INT, 'Profield field in signup form', VALUE_OPTIONAL), 'defaultdata' => new external_value(PARAM_RAW, 'Profield field default data', VALUE_OPTIONAL), 'defaultdataformat' => new external_format_value('defaultdata'), 'param1' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param2' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param3' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param4' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), 'param5' => new external_value(PARAM_RAW, 'Profield field settings', VALUE_OPTIONAL), ] ), 'Required profile fields', VALUE_OPTIONAL ), 'recaptchapublickey' => new external_value(PARAM_RAW, 'Recaptcha public key', VALUE_OPTIONAL), 'recaptchachallengehash' => new external_value(PARAM_RAW, 'Recaptcha challenge hash', VALUE_OPTIONAL), 'recaptchachallengeimage' => new external_value(PARAM_URL, 'Recaptcha challenge noscript image', VALUE_OPTIONAL), 'recaptchachallengejs' => new external_value(PARAM_URL, 'Recaptcha challenge js url', VALUE_OPTIONAL), 'warnings' => new external_warnings(), ] ); } /** * Describes the parameters for signup_user. * * @return external_function_parameters * @since Moodle 3.2 */ public static function signup_user_parameters() { return new external_function_parameters( array( 'username' => new external_value(core_user::get_property_type('username'), 'Username'), 'password' => new external_value(core_user::get_property_type('password'), 'Plain text password'), 'firstname' => new external_value(core_user::get_property_type('firstname'), 'The first name(s) of the user'), 'lastname' => new external_value(core_user::get_property_type('lastname'), 'The family name of the user'), 'email' => new external_value(core_user::get_property_type('email'), 'A valid and unique email address'), 'city' => new external_value(core_user::get_property_type('city'), 'Home city of the user', VALUE_DEFAULT, ''), 'country' => new external_value(core_user::get_property_type('country'), 'Home country code', VALUE_DEFAULT, ''), 'recaptchachallengehash' => new external_value(PARAM_RAW, 'Recaptcha challenge hash', VALUE_DEFAULT, ''), 'recaptcharesponse' => new external_value(PARAM_NOTAGS, 'Recaptcha response', VALUE_DEFAULT, ''), 'customprofilefields' => new external_multiple_structure( new external_single_structure( array( 'type' => new external_value(PARAM_ALPHANUMEXT, 'The type of the custom field'), 'name' => new external_value(PARAM_ALPHANUMEXT, 'The name of the custom field'), 'value' => new external_value(PARAM_RAW, 'Custom field value, can be an encoded json if required') ) ), 'User custom fields (also known as user profile fields)', VALUE_DEFAULT, array() ), 'redirect' => new external_value(PARAM_LOCALURL, 'Redirect the user to this site url after confirmation.', VALUE_DEFAULT, ''), ) ); } /** * Get the signup required settings and profile fields. * * @param string $username username * @param string $password plain text password * @param string $firstname the first name(s) of the user * @param string $lastname the family name of the user * @param string $email a valid and unique email address * @param string $city home city of the user * @param string $country home country code * @param string $recaptchachallengehash recaptcha challenge hash * @param string $recaptcharesponse recaptcha response * @param array $customprofilefields user custom fields (also known as user profile fields) * @param string $redirect Site url to redirect the user after confirmation * @return array settings and possible warnings * @since Moodle 3.2 * @throws moodle_exception * @throws invalid_parameter_exception */ public static function signup_user($username, $password, $firstname, $lastname, $email, $city = '', $country = '', $recaptchachallengehash = '', $recaptcharesponse = '', $customprofilefields = array(), $redirect = '') { global $CFG, $PAGE; $warnings = array(); $params = self::validate_parameters( self::signup_user_parameters(), array( 'username' => $username, 'password' => $password, 'firstname' => $firstname, 'lastname' => $lastname, 'email' => $email, 'city' => $city, 'country' => $country, 'recaptchachallengehash' => $recaptchachallengehash, 'recaptcharesponse' => $recaptcharesponse, 'customprofilefields' => $customprofilefields, 'redirect' => $redirect, ) ); // We need this to make work the format text functions. $context = context_system::instance(); $PAGE->set_context($context); self::check_signup_enabled(); // Validate profile fields param types. $allowedfields = profile_get_signup_fields(); $fieldproperties = array(); $fieldsrequired = array(); foreach ($allowedfields as $field) { $fieldproperties[$field->object->inputname] = $field->object->get_field_properties(); if ($field->object->is_required()) { $fieldsrequired[$field->object->inputname] = true; } } foreach ($params['customprofilefields'] as $profilefield) { if (!array_key_exists($profilefield['name'], $fieldproperties)) { throw new invalid_parameter_exception('Invalid field' . $profilefield['name']); } list($type, $allownull) = $fieldproperties[$profilefield['name']]; validate_param($profilefield['value'], $type, $allownull); // Remove from the potential required list. if (isset($fieldsrequired[$profilefield['name']])) { unset($fieldsrequired[$profilefield['name']]); } } if (!empty($fieldsrequired)) { throw new invalid_parameter_exception('Missing required parameters: ' . implode(',', array_keys($fieldsrequired))); } // Validate the data sent. $data = $params; $data['email2'] = $data['email']; // Force policy agreed if a site policy is set. The client is responsible of implementing the interface check. $manager = new \core_privacy\local\sitepolicy\manager(); if ($manager->is_defined()) { $data['policyagreed'] = 1; } unset($data['recaptcharesponse']); unset($data['customprofilefields']); // Add profile fields data. foreach ($params['customprofilefields'] as $profilefield) { // First, check if the value is a json (some profile fields like text area uses an array for sending data). $datadecoded = json_decode($profilefield['value'], true); if (is_array($datadecoded) && (json_last_error() == JSON_ERROR_NONE)) { $data[$profilefield['name']] = $datadecoded; } else { $data[$profilefield['name']] = $profilefield['value']; } } $errors = signup_validate_data($data, array()); // Validate recaptcha. if (signup_captcha_enabled()) { require_once($CFG->libdir . '/recaptchalib_v2.php'); $response = recaptcha_check_response(RECAPTCHA_VERIFY_URL, $CFG->recaptchaprivatekey, getremoteaddr(), $params['recaptcharesponse']); if (!$response['isvalid']) { $errors['recaptcharesponse'] = $response['error']; } } if (!empty($errors)) { foreach ($errors as $itemname => $message) { $warnings[] = array( 'item' => $itemname, 'itemid' => 0, 'warningcode' => 'fielderror', 'message' => s($message) ); } $result = array( 'success' => false, 'warnings' => $warnings, ); } else { // Save the user. $user = signup_setup_new_user((object) $data); $authplugin = get_auth_plugin('email'); // Check if we should redirect the user once the user is confirmed. $confirmationurl = null; if (!empty($params['redirect'])) { // Pass via moodle_url to fix thinks like admin links. $redirect = new moodle_url($params['redirect']); $confirmationurl = new moodle_url('/login/confirm.php', array('redirect' => $redirect->out())); } $authplugin->user_signup_with_confirmation($user, false, $confirmationurl); $result = array( 'success' => true, 'warnings' => array(), ); } return $result; } /** * Describes the signup_user return value. * * @return external_single_structure * @since Moodle 3.2 */ public static function signup_user_returns() { return new external_single_structure( array( 'success' => new external_value(PARAM_BOOL, 'True if the user was created false otherwise'), 'warnings' => new external_warnings(), ) ); } } PK&9\c"email/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_email. * * @package auth_email * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_email\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_email implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\aG&&email/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/email/*, information provided here is intended especially for developers. === 4.4 === * External function auth_email_external::get_signup_settings() now returns the field "extendedusernamechars". === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/email' to 'auth_email'. PK&9\,f7&7&&email/tests/external/external_test.phpnu[. /** * Auth email external functions tests. * * @package auth_email * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ namespace auth_email\external; use auth_email_external; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External auth email API tests. * * @package auth_email * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ final class external_test extends externallib_advanced_testcase { /** @var int custom profile field1 ID. */ protected $field1; /** @var int custom profile field2 ID. */ protected $field2; /** * Set up for every test */ public function setUp(): void { global $CFG; parent::setUp(); $this->resetAfterTest(true); $CFG->registerauth = 'email'; $this->field1 = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'frogname', 'name' => 'Name of frog', 'datatype' => 'text', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 1))->id; $this->field2 = $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'sometext', 'name' => 'Some text in textarea', 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2))->id; } public function test_get_signup_settings(): void { global $CFG; $CFG->defaultcity = 'Bcn'; $CFG->country = 'ES'; $CFG->sitepolicy = 'https://moodle.org'; $result = auth_email_external::get_signup_settings(); $result = \core_external\external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result); // Check expected data. $this->assertEquals(array('firstname', 'lastname'), $result['namefields']); $this->assertEquals($CFG->defaultcity, $result['defaultcity']); $this->assertEquals($CFG->country, $result['country']); $this->assertEquals($CFG->sitepolicy, $result['sitepolicy']); $this->assertEquals(print_password_policy(), $result['passwordpolicy']); $this->assertNotContains('recaptchachallengehash', $result); $this->assertNotContains('recaptchachallengeimage', $result); // Check if the extended username chars is returning false when is not set. $this->assertFalse($result['extendedusernamechars']); // Whip up a array with named entries to easily check against. $namedarray = array(); foreach ($result['profilefields'] as $key => $value) { $namedarray[$value['shortname']] = array( 'datatype' => $value['datatype'] ); } // Just check if we have the fields from this test. If a plugin adds fields we'll let it slide. $this->assertArrayHasKey('frogname', $namedarray); $this->assertArrayHasKey('sometext', $namedarray); $this->assertEquals('text', $namedarray['frogname']['datatype']); $this->assertEquals('textarea', $namedarray['sometext']['datatype']); $CFG->extendedusernamechars = true; $result = auth_email_external::get_signup_settings(); $result = \core_external\external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result); $this->assertTrue($result['extendedusernamechars']); } /** * Test get_signup_settings with mathjax in a profile field. */ public function test_get_signup_settings_with_mathjax_in_profile_fields(): void { // Enable MathJax filter in content and headings. $this->configure_filters([ ['name' => 'mathjaxloader', 'state' => TEXTFILTER_ON, 'move' => -1, 'applytostrings' => true], ]); // Create category with MathJax and a new field with MathJax. $categoryname = 'Cat $$(a+b)=2$$'; $fieldname = 'Some text $$(a+b)=2$$'; $categoryid = $this->getDataGenerator()->create_custom_profile_field_category(['name' => $categoryname])->id; $this->getDataGenerator()->create_custom_profile_field(array( 'shortname' => 'mathjaxname', 'name' => $fieldname, 'categoryid' => $categoryid, 'datatype' => 'textarea', 'signup' => 1, 'visible' => 1, 'required' => 1, 'sortorder' => 2)); $result = auth_email_external::get_signup_settings(); $result = \core_external\external_api::clean_returnvalue(auth_email_external::get_signup_settings_returns(), $result); // Format the original data. $sitecontext = \context_system::instance(); $categoryname = \core_external\util::format_string($categoryname, $sitecontext->id); $fieldname = \core_external\util::format_string($fieldname, $sitecontext->id); // Whip up a array with named entries to easily check against. $namedarray = array(); foreach ($result['profilefields'] as $key => $value) { $namedarray[$value['shortname']] = $value; } // Check the new profile field. $this->assertArrayHasKey('mathjaxname', $namedarray); $this->assertStringContainsString('', $namedarray['mathjaxname']['categoryname']); $this->assertStringContainsString('', $namedarray['mathjaxname']['name']); $this->assertEquals($categoryname, $namedarray['mathjaxname']['categoryname']); $this->assertEquals($fieldname, $namedarray['mathjaxname']['name']); } public function test_signup_user(): void { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; $city = 'Bcn'; $country = 'ES'; $customprofilefields = array( array( 'type' => 'text', 'name' => 'profile_field_frogname', 'value' => 'random text', ), array( 'type' => 'textarea', 'name' => 'profile_field_sometext', 'value' => json_encode( array( 'text' => 'blah blah', 'format' => FORMAT_HTML ) ), ) ); // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country, '', '', $customprofilefields); $result = \core_external\external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $user = $DB->get_record('user', array('username' => $username)); $this->assertEquals($firstname, $user->firstname); $this->assertEquals($lastname, $user->lastname); $this->assertEquals($email, $user->email); $this->assertEquals($city, $user->city); $this->assertEquals($country, $user->country); $this->assertEquals(0, $user->confirmed); $this->assertEquals(current_language(), $user->lang); $this->assertEquals('email', $user->auth); $infofield = $DB->get_record('user_info_data', array('userid' => $user->id, 'fieldid' => $this->field1)); $this->assertEquals($customprofilefields[0]['value'], $infofield->data); $infofield = $DB->get_record('user_info_data', array('userid' => $user->id, 'fieldid' => $this->field2)); $this->assertEquals(json_decode($customprofilefields[1]['value'])->text, $infofield->data); // Try to create a user with the same username, email and password. We ommit also the profile fields. $password = 'abc'; $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country, '', '', $customprofilefields); $result = \core_external\external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertFalse($result['success']); $this->assertCount(3, $result['warnings']); $expectederrors = array('username', 'email', 'password'); $finalerrors = []; foreach ($result['warnings'] as $warning) { $finalerrors[] = $warning['item']; } $this->assertEquals($expectederrors, $finalerrors); // Do not pass the required profile fields. $this->expectException('invalid_parameter_exception'); $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email, $city, $country); } } PK&9\g$$&email/tests/behat/behat_auth_email.phpnu[. /** * Step definition for auth_email * * @package auth_email * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Step definition for auth_email. * * @package auth_email * @category test * @copyright 2018 Marina Glancy * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_auth_email extends behat_base { /** * Emulate clicking on confirmation link from the email * * @When /^I confirm email for "(?P(?:[^"]|\\")*)"$/ * * @param string $username */ public function i_confirm_email_for($username) { global $DB; $secret = $DB->get_field('user', 'secret', ['username' => $username], MUST_EXIST); $confirmationurl = new moodle_url('/login/confirm.php'); $confirmationpath = $confirmationurl->out_as_local_url(false); $url = $confirmationpath . '?' . 'data='. $secret .'/'. $username; $this->execute('behat_general::i_visit', [$url]); } } PK&9\*ex/22 email/tests/behat/signup.featurenu[@auth @auth_email Feature: User must accept policy when logging in and signing up In order to record user agreement to use the site As a user I need to be able to accept site policy during sign up Scenario: Accept policy on sign up, no site policy Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" Then I should not see "I understand and agree" And I set the following fields to these values: | Username | user1 | | Password | user1 | | Email address | student1@example.com | | Email (again) | student1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" And I should see "An email should have been sent to your address at student1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" And I open my profile in edit mode And the field "First name" matches value "User1" And I log out # Confirm that user can login and browse the site (edit their profile). And I log in as "user1" And I open my profile in edit mode And the field "First name" matches value "User1" Scenario: Accept policy on sign up, with site policy Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | | sitepolicy | https://moodle.org | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" Then the field "I understand and agree" matches value "0" And I set the following fields to these values: | Username | user1 | | Password | user1 | | Email address | user1@example.com | | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | | I understand and agree | 1 | And I press "Create my new account" And I should see "Confirm your account" And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" And I open my profile in edit mode And the field "First name" matches value "User1" And I log out # Confirm that user is not asked to agree to site policy again after the next login. And I log in as "user1" And I open my profile in edit mode And the field "First name" matches value "User1" Scenario Outline: Email validation during email registration Given the following config values are set as admin: | allowaccountssameemail | | | registerauth | email | | passwordpolicy | 0 | And the following "users" exist: | username | firstname | lastname | email | | s1 | John | Doe | s1@example.com | And I am on site homepage And I follow "Log in" When I click on "Create new account" "link" And I set the following fields to these values: | Username | s2 | | Password | test | | Email address | | | Email (again) | | | First name | Jane | | Last name | Doe | And I press "Create my new account" Then I should "This email address is already registered. Perhaps you created an account in the past?" And I should "Invalid email address" Examples: | allowsameemail | email1 | email2 | expect | expect2 | | 0 | s1@example.com | s1@example.com | see | not see | | 0 | S1@EXAMPLE.COM | S1@EXAMPLE.COM | see | not see | | 0 | s1@example.com | S1@EXAMPLE.COM | see | not see | | 0 | s2@example.com | s1@example.com | not see | see | | 1 | s1@example.com | s1@example.com | not see | not see | | 1 | S1@EXAMPLE.COM | S1@EXAMPLE.COM | not see | not see | | 1 | s1@example.com | S1@EXAMPLE.COM | not see | not see | | 1 | s1@example.com | s2@example.com | not see | see | PK&9\\$email/auth.phpnu[. /** * Authentication Plugin: Email Authentication * * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License * @package auth_email */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Email authentication plugin. */ class auth_plugin_email extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'email'; $this->config = get_config('auth_email'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_email() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { global $CFG, $DB; if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return validate_internal_user_password($user, $password); } return false; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object (with system magic quotes) * @param string $newpassword Plaintext password (with system magic quotes) * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function can_signup() { return true; } /** * Sign up a new user ready for confirmation. * Password is passed in plaintext. * * @param object $user new user object * @param boolean $notify print notice with link and terminate */ function user_signup($user, $notify=true) { // Standard signup, without custom confirmatinurl. return $this->user_signup_with_confirmation($user, $notify); } /** * Sign up a new user ready for confirmation. * * Password is passed in plaintext. * A custom confirmationurl could be used. * * @param object $user new user object * @param boolean $notify print notice with link and terminate * @param string $confirmationurl user confirmation URL * @return boolean true if everything well ok and $notify is set to true * @throws moodle_exception * @since Moodle 3.2 */ public function user_signup_with_confirmation($user, $notify=true, $confirmationurl = null) { global $CFG, $DB, $SESSION; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); $plainpassword = $user->password; $user->password = hash_internal_user_password($user->password); if (empty($user->calendartype)) { $user->calendartype = $CFG->calendartype; } $user->id = user_create_user($user, false, false); user_add_password_history($user->id, $plainpassword); // Save any custom profile field information. profile_save_data($user); // Save wantsurl against user's profile, so we can return them there upon confirmation. if (!empty($SESSION->wantsurl)) { set_user_preference('auth_email_wantsurl', $SESSION->wantsurl, $user); } // Trigger event. \core\event\user_created::create_from_userid($user->id)->trigger(); if (! send_confirmation_email($user, $confirmationurl)) { throw new \moodle_exception('auth_emailnoemail', 'auth_email'); } if ($notify) { global $CFG, $PAGE, $OUTPUT; $emailconfirm = get_string('emailconfirm'); $PAGE->navbar->add($emailconfirm); $PAGE->set_title($emailconfirm); $PAGE->set_heading($PAGE->course->fullname); echo $OUTPUT->header(); notice(get_string('emailconfirmsent', '', $user->email), "$CFG->wwwroot/index.php"); } else { return true; } } /** * Returns true if plugin allows confirming of new users. * * @return bool */ function can_confirm() { return true; } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret) { global $DB, $SESSION; $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)); if ($wantsurl = get_user_preferences('auth_email_wantsurl', false, $user)) { // Ensure user gets returned to page they were trying to access before signing up. $SESSION->wantsurl = $wantsurl; unset_user_preference('auth_email_wantsurl', $user); } return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; // use default internal method } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns whether or not the captcha element is enabled. * @return bool */ function is_captcha_enabled() { return get_config("auth_{$this->authtype}", 'recaptcha'); } } PK&9\4Jemail/settings.phpnu[. /** * Admin settings and defaults. * * @package auth_email * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_email/pluginname', '', new lang_string('auth_emaildescription', 'auth_email'))); $options = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_email/recaptcha', new lang_string('auth_emailrecaptcha_key', 'auth_email'), new lang_string('auth_emailrecaptcha', 'auth_email'), 0, $options)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('email'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } PK&9\S<&webservice/lang/en/auth_webservice.phpnu[. /** * Strings for component 'auth_webservice', language 'en'. * * @package auth_webservice * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_webservicedescription'] = 'This authentication method should be used for accounts that are exclusively for use by web service clients.'; $string['pluginname'] = 'Web services authentication'; $string['privacy:metadata'] = 'The Web services authentication plugin does not store any personal data.';PK&9\ޯŒwebservice/version.phpnu[. /** * Version information * * @package auth_webservice * @copyright 2011 Petr Skoda (http://skodak.org) * @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_webservice'; // Full name of the plugin (used for diagnostics) PK&9\D'webservice/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_webservice. * * @package auth_webservice * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_webservice\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_webservice implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\>webservice/auth.phpnu[. /** * Web service auth plugin, reserves username, prevents normal login. * TODO: add IP restrictions and some other features - MDL-17135 * * @package auth_webservice * @copyright 2008 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Web service auth plugin. */ class auth_plugin_webservice extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'webservice'; $this->config = get_config('auth_webservice'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_webservice() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (with system magic quotes) * @param string $password The password (with system magic quotes) * * @return bool Authentication success or failure. */ function user_login($username, $password) { // normla logins not allowed! return false; } /** * Custom auth hook for web services. * @param string $username * @param string $password * @return bool success */ function user_login_webservice($username, $password) { global $CFG, $DB; // special web service login $user = $DB->get_record('user', [ 'username' => $username, 'auth' => 'webservice', 'deleted' => '0', 'suspended' => '0', 'mnethostid' => $CFG->mnet_localhost_id, ]); if ($user) { return validate_internal_user_password($user, $password); } return false; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object (with system magic quotes) * @param string $newpassword Plaintext password (with system magic quotes) * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } /** * Returns true if this authentication plugin is 'internal'. * * Webserice auth doesn't use password fields, it uses only tokens. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ 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 */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return false; } /** * Confirm the new user as registered. This should normally not be used, * but it may be necessary if the user auth_method is changed to manual * before the user is confirmed. */ function user_confirm($username, $confirmsecret = null) { return AUTH_CONFIRM_ERROR; } } PK&9\  UPGRADING.mdnu[# core_auth (subsystem / plugintype) Upgrade notes ## 5.0.1 ### Added - A new method called `get_additional_upgrade_token_parameters` has been added to `oauth2_client` class. This will allow custom clients to override this one and add their extra parameters for upgrade token request. For more information see [MDL-80380](https://tracker.moodle.org/browse/MDL-80380) ## 5.0 ### Removed - Cas authentication is removed from core and added to the following git repository: https://github.com/moodlehq/moodle-auth_cas For more information see [MDL-78778](https://tracker.moodle.org/browse/MDL-78778) - Removed auth_mnet plugin from core For more information see [MDL-84307](https://tracker.moodle.org/browse/MDL-84307) PK&9\ index.htmlnu[PK&9\S3EEldap/db/install.phpnu[. /** * Definition of auth_ldap tasks. * * @package auth_ldap * @category task * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $tasks = array( array( 'classname' => 'auth_ldap\task\sync_roles', 'blocking' => 0, 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ), array( 'classname' => 'auth_ldap\task\sync_task', 'blocking' => 0, 'minute' => '0', 'hour' => '0', 'day' => '*', 'month' => '*', 'dayofweek' => '*', 'disabled' => 1 ) ); PK&9\_4]]ldap/db/upgrade.phpnu[. /** * LDAP authentication plugin upgrade code * * @package auth_ldap * @copyright 2013 Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_ldap. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_ldap_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; } PK&9\5BBldap/lang/en/auth_ldap.phpnu[. /** * Strings for component 'auth_ldap', language 'en'. * * @package auth_ldap * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_ldap_ad_create_req'] = 'Cannot create the new account in Active Directory. Make sure you meet all the requirements for this to work (LDAPS connection, bind user with adequate rights, etc.)'; $string['auth_ldap_attrcreators'] = 'List of groups or contexts whose members are allowed to create attributes. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_attrcreators_key'] = 'Attribute creators'; $string['auth_ldap_auth_user_create_key'] = 'Create users externally'; $string['auth_ldap_bind_dn'] = 'If you want to use bind-user to search users, specify it here. Something like \'cn=ldapuser,ou=public,o=org\''; $string['auth_ldap_bind_dn_key'] = 'Distinguished name'; $string['auth_ldap_bind_pw'] = 'Password for bind-user.'; $string['auth_ldap_bind_pw_key'] = 'Password'; $string['auth_ldap_bind_settings'] = 'Bind settings'; $string['auth_ldap_contexts'] = 'List of contexts where users are located. Separate different contexts with \';\'. For example: \'ou=users,o=org; ou=others,o=org\''; $string['auth_ldap_contexts_key'] = 'Contexts'; $string['auth_ldap_create_context'] = 'If you enable user creation with email confirmation, specify the context where users are created. This context should be different from other users to prevent security issues. You don\'t need to add this context to ldap_context-variable, Moodle will search for users from this context automatically.
Note! You have to modify the method user_create() in file auth/ldap/auth.php to make user creation work'; $string['auth_ldap_create_context_key'] = 'Context for new users'; $string['auth_ldap_create_error'] = 'Error creating user in LDAP.'; $string['auth_ldapdescription'] = 'This method provides authentication against an external LDAP server. If the given username and password are valid, Moodle creates a new user entry in its database. This plugin can read user attributes from LDAP and prefill wanted fields in Moodle. For following logins only the username and password are checked.'; $string['auth_ldap_expiration_desc'] = 'Select \'{$a->no}\' to disable expired password checking or \'{$a->ldapserver}\' to read the password expiry time directly from the LDAP server.'; $string['auth_ldap_expiration_key'] = 'Expiry'; $string['auth_ldap_expiration_warning_desc'] = 'Number of days before password expiry warning is issued.'; $string['auth_ldap_expiration_warning_key'] = 'Expiry warning'; $string['auth_ldap_expireattr_desc'] = 'Optional: Overrides the LDAP attribute that stores password expiry time.'; $string['auth_ldap_expireattr_key'] = 'Expiry attribute'; $string['auth_ldapextrafields'] = 'These fields are optional. You can choose to pre-fill some Moodle user fields with information from the LDAP fields that you specify here.

If you leave these fields blank, then nothing will be transferred from LDAP and Moodle defaults will be used instead.

In either case, the user will be able to edit all of these fields after they log in.

'; $string['auth_ldap_graceattr_desc'] = 'Optional: Overrides grace login attribute'; $string['auth_ldap_gracelogin_key'] = 'Grace login attribute'; $string['auth_ldap_gracelogins_desc'] = 'Enable LDAP grace login support. After password has expired, user can log in until grace login count is 0. Enabling this setting displays grace login message if password has expired.'; $string['auth_ldap_gracelogins_key'] = 'Grace logins'; $string['auth_ldap_groupecreators'] = 'List of groups or contexts whose members are allowed to create groups. Separate multiple groups with \';\'. Usually something like \'cn=teachers,ou=staff,o=myorg\''; $string['auth_ldap_groupecreators_key'] = 'Group creators'; $string['auth_ldap_host_url'] = 'Specify LDAP host in URL-form like \'ldap://ldap.myorg.com/\' or \'ldaps://ldap.myorg.com/\'. Separate multiple servers with \';\' to get failover support.'; $string['auth_ldap_host_url_key'] = 'Host URL'; $string['auth_ldap_changepasswordurl_key'] = 'Password-change URL'; $string['auth_ldap_ldap_encoding'] = 'Encoding used by the LDAP server, most likely utf-8. If LDAP v2 is selected, Active Directory uses its configured encoding, such as cp1252 or cp1250.'; $string['auth_ldap_ldap_encoding_key'] = 'LDAP encoding'; $string['auth_ldap_login_settings'] = 'Login settings'; $string['auth_ldap_memberattribute'] = 'Optional: Overrides user member attribute, when users belongs to a group. Usually \'member\''; $string['auth_ldap_memberattribute_isdn'] = 'Overrides handling of member attribute values'; $string['auth_ldap_memberattribute_isdn_key'] = 'Member attribute uses dn'; $string['auth_ldap_memberattribute_key'] = 'Member attribute'; $string['auth_ldap_noconnect'] = 'LDAP-module cannot connect to server: {$a}'; $string['auth_ldap_noconnect_all'] = 'LDAP-module cannot connect to any servers: {$a}'; $string['auth_ldap_noextension'] = 'The PHP LDAP module does not seem to be present. Please ensure it is installed and enabled if you want to use this authentication plugin.'; $string['auth_ldap_no_mbstring'] = 'You need the mbstring extension to create users in Active Directory.'; $string['auth_ldapnotinstalled'] = 'Cannot use LDAP authentication. The PHP LDAP module is not installed.'; $string['auth_ldap_objectclass'] = 'Optional: Overrides objectClass used to name/search users on ldap_user_type. Usually you don\'t need to change this.'; $string['auth_ldap_objectclass_key'] = 'Object class'; $string['auth_ldap_opt_deref'] = 'Determines how aliases are handled during search. Select one of the following values: "No" (LDAP_DEREF_NEVER) or "Yes" (LDAP_DEREF_ALWAYS)'; $string['auth_ldap_opt_deref_key'] = 'Dereference aliases'; $string['auth_ldap_passtype'] = 'Specify the format of new or changed passwords in LDAP server.'; $string['auth_ldap_passtype_key'] = 'Password format'; $string['auth_ldap_passwdexpire_settings'] = 'LDAP password expiry settings'; $string['auth_ldap_preventpassindb'] = 'Select yes to prevent passwords from being stored in Moodle\'s DB.'; $string['auth_ldap_preventpassindb_key'] = 'Prevent password caching'; $string['auth_ldap_rolecontext'] = '{$a->localname} context'; $string['auth_ldap_rolecontext_help'] = 'LDAP context used to select for {$a->localname} mapping. Separate multiple groups with \';\'. Usually something like "cn={$a->shortname},ou=first-ou-with-role-groups,o=myorg; cn={$a->shortname},ou=second-ou-with-role-groups,o=myorg".'; $string['auth_ldap_search_sub'] = 'Search users from subcontexts.'; $string['auth_ldap_search_sub_key'] = 'Search subcontexts'; $string['auth_ldap_server_settings'] = 'LDAP server settings'; $string['auth_ldap_unsupportedusertype'] = 'auth: ldap user_create() does not support selected usertype: {$a}'; $string['auth_ldap_update_userinfo'] = 'Update user information (firstname, lastname, address..) from LDAP to Moodle. Specify "Data mapping" settings as you need.'; $string['auth_ldap_user_attribute'] = 'Optional: Overrides the attribute used to name/search users. Usually \'cn\'.'; $string['auth_ldap_user_attribute_key'] = 'User attribute'; $string['auth_ldap_suspended_attribute'] = 'Optional: When provided this attribute will be used to enable/suspend the locally created user account.'; $string['auth_ldap_suspended_attribute_key'] = 'Suspended attribute'; $string['auth_ldap_user_exists'] = 'LDAP username already exists.'; $string['auth_ldap_user_settings'] = 'User lookup settings'; $string['auth_ldap_user_type'] = 'Select how users are stored in LDAP. This setting also specifies how login expiry, grace logins and user creation will work.'; $string['auth_ldap_user_type_key'] = 'User type'; $string['auth_ldap_usertypeundefined'] = 'config.user_type not defined or function ldap_expirationtime2unix does not support selected type!'; $string['auth_ldap_usertypeundefined2'] = 'config.user_type not defined or function ldap_unixi2expirationtime does not support selected type!'; $string['auth_ldap_version'] = 'The version of the LDAP protocol your server is using.'; $string['auth_ldap_version_key'] = 'Version'; $string['auth_ntlmsso'] = 'NTLM SSO'; $string['auth_ntlmsso_enabled'] = 'Set to yes to attempt Single Sign On with the NTLM domain. Note that this requires additional setup on the server to work. For further details, see the documentation NTLM authentication.'; $string['auth_ntlmsso_enabled_key'] = 'Enable'; $string['auth_ntlmsso_ie_fastpath'] = 'Set to enable the NTLM SSO fast path (bypasses certain steps if the client\'s browser is MS Internet Explorer).'; $string['auth_ntlmsso_ie_fastpath_key'] = 'MS IE fast path?'; $string['auth_ntlmsso_ie_fastpath_yesform'] = 'Yes, all other browsers use standard login form'; $string['auth_ntlmsso_ie_fastpath_yesattempt'] = 'Yes, attempt NTLM other browsers'; $string['auth_ntlmsso_ie_fastpath_attempt'] = 'Attempt NTLM with all browsers'; $string['auth_ntlmsso_maybeinvalidformat'] = 'Unable to extract the username from the REMOTE_USER header. Is the configured format right?'; $string['auth_ntlmsso_missing_username'] = 'You need to specify at least %username% in the remote username format'; $string['auth_ntlmsso_remoteuserformat_key'] = 'Remote username format'; $string['auth_ntlmsso_remoteuserformat'] = 'If you have chosen \'NTLM\' in \'Authentication type\', you can specify the remote username format here. If you leave this empty, the default DOMAIN\\username format will be used. You can use the optional %domain% placeholder to specify where the domain name appears, and the mandatory %username% placeholder to specify where the username appears.

Some widely used formats are %domain%\\%username% (MS Windows default), %domain%/%username%, %domain%+%username% and just %username% (if there is no domain part).'; $string['auth_ntlmsso_subnet'] = 'If set, it will only attempt SSO with clients in this subnet. Format: xxx.xxx.xxx.xxx/bitmask. Separate multiple subnets with \',\' (comma).'; $string['auth_ntlmsso_subnet_key'] = 'Subnet'; $string['auth_ntlmsso_type_key'] = 'Authentication type'; $string['auth_ntlmsso_type'] = 'The authentication method configured in the web server to authenticate the users (if in doubt, choose NTLM)'; $string['cannotmaprole'] = 'The role "{$a->rolename}" cannot be mapped because its short name "{$a->shortname}" is too long and/or contains hyphens. To allow it to be mapped, the short name needs to be reduced to a maximum of {$a->charlimit} characters and any hyphens removed. Edit the role'; $string['connectingldap'] = "Connecting to LDAP server...\n"; $string['connectingldapsuccess'] = "Connecting to your LDAP server was successful"; $string['creatingtemptable'] = "Creating temporary table {\$a}\n"; $string['didntfindexpiretime'] = 'password_expire() didn\'t find expiration time.'; $string['didntgetusersfromldap'] = "Did not get any users from LDAP -- error? -- exiting\n"; $string['gotcountrecordsfromldap'] = "Got {\$a} records from LDAP\n"; $string['invalidusererrors'] = "Warning: Skipped creation of {\$a} user accounts.\n\n"; $string['invaliduserexception'] = "\nError: Cannot create new user account. Details and reason:\n{\$a}\nSkipping this user.\n\n"; $string['ldapnotconfigured'] = 'The LDAP host url is currently not configured'; $string['morethanoneuser'] = 'More than one user record found in LDAP. Using only the first one.'; $string['needbcmath'] = 'You need the BCMath extension to use expired password checking with Active Directory.'; $string['needmbstring'] = 'You need the mbstring extension to change passwords in Active Directory'; $string['nodnforusername'] = 'Error in user_update_password(). No DN for: {$a->username}'; $string['noemail'] = 'Tried to send you an email but failed!'; $string['notcalledfromserver'] = 'Should not be called from the web server!'; $string['noupdatestobedone'] = "No updates to be done\n"; $string['nouserentriestoremove'] = "No user entries to be removed\n"; $string['nouserentriestorevive'] = "No user entries to be revived\n"; $string['nouserstobeadded'] = 'No user entries to be added'; $string['ntlmsso_attempting'] = 'Attempting Single Sign On via NTLM...'; $string['ntlmsso_failed'] = 'Auto-login failed, try the normal login page...'; $string['ntlmsso_isdisabled'] = 'NTLM SSO is disabled.'; $string['ntlmsso_unknowntype'] = 'Unknown ntlmsso type!'; $string['pagedresultsnotsupp'] = 'LDAP paged results not supported (either your PHP version lacks support, you have configured Moodle to use LDAP protocol version 2 or Moodle cannot contact your LDAP server to see if paged support is available.)'; $string['pagesize'] = 'Make sure this value is smaller than your LDAP server result set size limit (the maximum number of entries that can be returned in a single query)'; $string['pagesize_key'] = 'Page size'; $string['pluginname'] = 'LDAP server'; $string['pluginnotenabled'] = 'Plugin not enabled!'; $string['renamingnotallowed'] = 'User renaming not allowed in LDAP'; $string['rootdseerror'] = 'Error querying rootDSE for Active Directory'; $string['syncroles'] = 'Synchronise system roles from LDAP'; $string['synctask'] = 'LDAP users sync job'; $string['sync_updateuserchunk'] = 'Set this value to the number of users you want updated per transaction. Setting this to 0 will update all users in one transaction.'; $string['sync_updateuserchunk_key'] = 'Sync update users chunk size'; $string['systemrolemapping'] = 'System role mapping'; $string['start_tls'] = 'Use regular LDAP service (port 389) with TLS encryption'; $string['start_tls_key'] = 'Use TLS'; $string['updateremfail'] = 'Error updating LDAP record. Error code: {$a->errno}; Error string: {$a->errstring}
Key ({$a->key}) - old moodle value: \'{$a->ouvalue}\' new value: \'{$a->nuvalue}\''; $string['updateremfailamb'] = 'Failed to update LDAP with ambiguous field {$a->key}; old moodle value: \'{$a->ouvalue}\', new value: \'{$a->nuvalue}\''; $string['updatepasserror'] = 'Error in user_update_password(). Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpire'] = 'Error in user_update_password() when reading password expiry time. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updatepasserrorexpiregrace'] = 'Error in user_update_password() when modifying expiry time and/or grace logins. Error code: {$a->errno}; Error string: {$a->errstring}'; $string['updateusernotfound'] = 'Could not find user while updating externally. Details follow: search base: \'{$a->userdn}\'; search filter: \'(objectClass=*)\'; search attributes: {$a->attribs}'; $string['user_activatenotsupportusertype'] = 'auth: ldap user_activate() does not support selected usertype: {$a}'; $string['user_disablenotsupportusertype'] = 'auth: ldap user_disable() does not support selected usertype: {$a}'; $string['userentriestoadd'] = "User entries to be added: {\$a}\n"; $string['userentriestoremove'] = "User entries to be removed: {\$a}\n"; $string['userentriestorevive'] = "User entries to be revived: {\$a}\n"; $string['userentriestoupdate'] = "User entries to be updated: {\$a}\n"; $string['usernotfound'] = 'User not found in LDAP'; $string['useracctctrlerror'] = 'Error getting userAccountControl for {$a}'; $string['diag_genericerror'] = 'LDAP error {$a->code} reading {$a->subject}: {$a->message}.'; $string['diag_toooldversion'] = 'It is very unlikely a modern LDAP server uses LDAPv2 protocol. Wrong settings can corrupt values in user fields. Check with your LDAP administrator.'; $string['diag_emptycontext'] = 'Empty context found.'; $string['diag_contextnotfound'] = 'Context {$a} doesn\'t exist or can\'t be read by bind DN.'; $string['diag_rolegroupnotfound'] = 'Group {$a->group} for role {$a->localname} doesn\'t exist or can\'t be read by bind DN.'; $string['privacy:metadata'] = 'The LDAP server authentication plugin does not store any personal data.'; PK&9\)g0ldap/version.phpnu[. /** * Version details * * @package auth_ldap * @author Martin Dougiamas * @author Iñaki Arenaza * @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_ldap'; // Full name of the plugin (used for diagnostics) PK&9\|hgQ77:ldap/classes/admin_setting_special_contexts_configtext.phpnu[. /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that cleans up context values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_contexts_configtext extends admin_setting_configtext { /** * We need to remove duplicates on save to prevent issues in other areas of Moodle. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { // Try to remove duplicates before storing the contexts (to avoid problems in sync_users()). $data = explode(';', $data); $data = array_map(function($x) { return core_text::strtolower(trim($x)); }, $data); $data = implode(';', array_unique($data)); return parent::write_setting($data); } } PK&9\4YF!ldap/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_ldap. * * @package auth_ldap * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_ldap implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\z6ldap/classes/admin_setting_special_ntlm_configtext.phpnu[. /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special admin setting for auth_ldap that validates ntlm usernames. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_ntlm_configtext extends admin_setting_configtext { /** * We need to validate the username format when using NTLM. * * @param string $data Form data. * @return string Empty when no errors. */ public function validate($data) { if (get_config('auth_ldap', 'ntlmsso_type') === 'ntlm') { $format = trim($data); if (!empty($format) && !preg_match('/%username%/i', $format)) { return get_string('auth_ntlmsso_missing_username', 'auth_ldap'); } } return parent::validate($data); } } PK&9\8ldap/classes/task/sync_task.phpnu[. /** * A scheduled task for LDAP user sync. * * @package auth_ldap * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; /** * A scheduled task class for LDAP user sync. * * @copyright 2015 Vadim Dvorovenko * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_task extends \core\task\scheduled_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('synctask', 'auth_ldap'); } /** * Run users sync. */ public function execute() { if (is_enabled_auth('ldap')) { /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $count = 0; $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { $asynctask = new asynchronous_sync_task(); $asynctask->set_custom_data([ 'users' => $users, 'updatekeys' => $updatekeys, ]); \core\task\manager::queue_adhoc_task($asynctask); $count++; mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, $count)); sleep(1); }); } } } PK&9\E ldap/classes/task/sync_roles.phpnu[. /** * A scheduled task for LDAP roles sync. * * @package auth_ldap * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; defined('MOODLE_INTERNAL') || die(); /** * A scheduled task class for LDAP roles sync. * * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sync_roles extends \core\task\scheduled_task { /** * Get a descriptive name for this task (shown to admins). * * @return string */ public function get_name() { return get_string('syncroles', 'auth_ldap'); } /** * Synchronise role assignments from LDAP. */ public function execute() { global $DB; if (is_enabled_auth('ldap')) { $auth = get_auth_plugin('ldap'); $users = $DB->get_records('user', array('auth' => 'ldap')); foreach ($users as $user) { $auth->sync_roles($user); } } } } PK&9\*,ldap/classes/task/asynchronous_sync_task.phpnu[. /** * Adhoc task for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_ldap\task; use core\task\adhoc_task; /** * Adhoc task class for LDAP user sync. * * @package auth_ldap * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class asynchronous_sync_task extends adhoc_task { /** @var string Message prefix for mtrace */ protected const MTRACE_MSG = 'Synced ldap users'; /** * Constructor */ public function __construct() { $this->set_component('auth_ldap'); } /** * Run users sync. */ public function execute() { $data = $this->get_custom_data(); /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $auth->update_users($data->users, $data->updatekeys); mtrace(sprintf(" %s (%d)", self::MTRACE_MSG, count($data->users))); } } PK&9\^=Z^ldap/classes/adminpresets/adminpresets_auth_ldap_admin_setting_special_contexts_configtext.phpnu[. namespace auth_ldap\adminpresets; use core_adminpresets\local\setting\adminpresets_setting; /** * Basic text setting, cleans the param using the admin_setting paramtext attribute. * * @package auth_ldap * @copyright 2021 Pimenko * @author Jordan Kesraoui | Sylvain Revenu | Pimenko based on David Monllaó code * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class adminpresets_auth_ldap_admin_setting_special_contexts_configtext extends adminpresets_setting { /** * Validates the value using paramtype attribute * * @param string $value * @return boolean Returned value is always true, whenever the value has been successfully cleaned or not. */ protected function set_value($value): bool { $this->value = $value; if (empty($this->settingdata->paramtype)) { // For configfile, configpasswordunmask.... $this->settingdata->paramtype = 'RAW'; } $paramtype = 'PARAM_' . strtoupper($this->settingdata->paramtype); // Regexp. if (!defined($paramtype)) { $this->value = preg_replace($this->settingdata->paramtype, '', $this->value); // Standard moodle param type. } else { $this->value = clean_param($this->value, constant($paramtype)); } $this->set_visiblevalue(); return true; } } PK&9\;ldap/classes/admin_setting_special_lowercase_configtext.phpnu[. /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Special setting for auth_ldap that lowercases values on save.. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_ldap_admin_setting_special_lowercase_configtext extends admin_setting_configtext { /** * We need to convert the data to lowercase prior to save. * * @param string $data Form data. * @return string Empty when no errors. */ public function write_setting($data) { return parent::write_setting(core_text::strtolower($data)); } } PK&9\ldap/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in the auth_ldap code. === 3.4 === * The "auth_ldap/coursecreators" setting was replaced with dynamically generated "auth_ldap/context" settings, migrating any existing value to a new setting in this style. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/ldap' to 'auth_ldap'. === 2.9.1 === * auth_plugin_ldap::update_user_record() accepts an additional (optional) param to trigger update event. PK&9\ldap/locallib.phpnu[. /** * Internal library of functions for module auth_ldap * * @package auth_ldap * @author David Balch * @copyright 2017 The Chancellor Masters and Scholars of the University of Oxford {@link http://www.tall.ox.ac.uk/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Get a list of system roles assignable by the current or a specified user, including their localised names. * * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @return array $roles, each role as an array with id, shortname, localname, and settingname for the config value. */ function get_ldap_assignable_role_names($user = null) { $roles = array(); if ($assignableroles = get_assignable_roles(context_system::instance(), ROLENAME_SHORT, false, $user)) { $systemroles = role_fix_names(get_all_roles(), context_system::instance(), ROLENAME_ORIGINAL); foreach ($assignableroles as $shortname) { foreach ($systemroles as $systemrole) { if ($systemrole->shortname == $shortname) { $roles[] = array('id' => $systemrole->id, 'shortname' => $shortname, 'localname' => $systemrole->localname, 'settingname' => $shortname . 'context'); break; } } } } return $roles; } PK&9\QnfJJldap/README-LDAPnu[LDAP-module README Please read comments from auth.php in this directory. PK&9\zT} ldap/cli/sync_users.phpnu[. /** * CAS user sync script. * * This script is meant to be called from a cronjob to sync moodle with the LDAP * backend in those setups where the LDAP backend acts as 'master'. * * Notes: * - it is required to use the web server account when executing PHP CLI scripts * - you need to change the "www-data" to match the apache user account * - use "su" if "sudo" not available * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * - If you have a large number of users, you may want to raise the memory limits * by passing -d momory_limit=256M * - For debugging & better logging, you are encouraged to use in the command line: * -d log_errors=1 -d error_reporting=E_ALL -d display_errors=0 -d html_errors=0 * * Performance notes: * We have optimized it as best as we could for PostgreSQL and MySQL, with 27K students * we have seen this take 10 minutes. * * @package auth_ldap * @copyright 2004 Martin Langhoff * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.0 MDL-51824 - please do not use this CLI script any more, use scheduled task instead. * @todo MDL-50264 This will be deleted in Moodle 3.2. */ define('CLI_SCRIPT', true); require(__DIR__.'/../../../config.php'); // global moodle config file. require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->libdir.'/clilib.php'); // Ensure errors are well explained set_debugging(DEBUG_DEVELOPER, true); if (!is_enabled_auth('ldap')) { error_log('[AUTH LDAP] '.get_string('pluginnotenabled', 'auth_ldap')); die; } cli_problem('[AUTH LDAP] The users sync cron has been deprecated. Please use the scheduled task instead.'); // Abort execution of the CLI script if the auth_ldap\task\sync_task is enabled. $taskdisabled = \core\task\manager::get_scheduled_task('auth_ldap\task\sync_task'); if (!$taskdisabled->get_disabled()) { cli_error('[AUTH LDAP] The scheduled task sync_task is enabled, the cron execution has been aborted.'); } $ldapauth = get_auth_plugin('ldap'); $ldapauth->sync_users(true); PK&9\TfTfldap/tests/auth_ldap_test.phpnu[. namespace auth_ldap; /** * LDAP authentication plugin tests. * * NOTE: in order to execute this test you need to set up * OpenLDAP server with core, cosine, nis and internet schemas * and add configuration constants to config.php or phpunit.xml configuration file: * * define('TEST_AUTH_LDAP_HOST_URL', 'ldap://127.0.0.1'); * define('TEST_AUTH_LDAP_BIND_DN', 'cn=someuser,dc=example,dc=local'); * define('TEST_AUTH_LDAP_BIND_PW', 'somepassword'); * define('TEST_AUTH_LDAP_DOMAIN', 'dc=example,dc=local'); * * @package auth_ldap * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use auth_plugin_ldap; use auth_ldap\task\{ sync_task, asynchronous_sync_task, }; /** * LDAP authentication plugin tests. * * @package auth_ldap * @copyright 2013 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class auth_ldap_test extends \advanced_testcase { public static function setUpBeforeClass(): void { global $CFG; parent::setUpBeforeClass(); require_once($CFG->dirroot . '/auth/ldap/auth.php'); require_once($CFG->libdir . '/ldaplib.php'); } /** * Data provider for auth_ldap tests * * Used to ensure that all the paged stuff works properly, irrespectively * of the pagesize configured (that implies all the chunking and paging * built in the plugis is doing its work consistently). Both searching and * not searching within subcontexts. * * @return array[] */ public static function auth_ldap_provider(): array { $pagesizes = [1, 3, 5, 1000]; $subcontexts = [0, 1]; $combinations = []; foreach ($pagesizes as $pagesize) { foreach ($subcontexts as $subcontext) { $combinations["pagesize {$pagesize}, subcontexts {$subcontext}"] = [$pagesize, $subcontext]; } } return $combinations; } /** * General auth_ldap testcase * * @dataProvider auth_ldap_provider * @param int $pagesize Value to be configured in settings controlling page size. * @param int $subcontext Value to be configured in settings controlling searching in subcontexts. */ public function test_auth_ldap(int $pagesize, int $subcontext): void { global $DB; if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); $createdusers = array(); for ($i=1; $i<=5; $i++) { $this->create_ldap_user($connection, $topdn, $i); $createdusers[] = 'username' . $i; } // Set up creators group. $assignedroles = array('username1', 'username2'); $o = array(); $o['objectClass'] = array('posixGroup'); $o['cn'] = 'creators'; $o['gidNumber'] = 1; $o['memberUid'] = $assignedroles; ldap_add($connection, 'cn='.$o['cn'].','.$topdn, $o); $creatorrole = $DB->get_record('role', array('shortname'=>'coursecreator')); $this->assertNotEmpty($creatorrole); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', $pagesize, 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', $subcontext, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('coursecreatorcontext', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 5 users created with 2 users having roles. $this->assertCount(7, $events); foreach ($events as $index => $event) { $username = $DB->get_field('user', 'username', array('id' => $event->relateduserid)); // Get username. if ($event->eventname === '\core\event\user_created') { $this->assertContains($username, $createdusers); unset($events[$index]); // Remove matching event. } else if ($event->eventname === '\core\event\role_assigned') { $this->assertContains($username, $assignedroles); unset($events[$index]); // Remove matching event. } else { $this->fail('Unexpected event found: ' . $event->eventname); } } // If all the user_created and role_assigned events have matched // then the $events array should be now empty. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); for ($i=1; $i<=5; $i++) { $this->assertTrue($DB->record_exists('user', array('username'=>'username'.$i, 'email'=>'user'.$i.'@example.com', 'firstname'=>'Firstname'.$i, 'lastname'=>'Lastname'.$i))); } $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, no new event. $this->assertCount(0, $events); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_SUSPEND, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('auth'=>'nologin', 'username'=>'username1'))); $this->assertEquals(1, $DB->count_records('user', array('auth'=>'ldap', 'suspended'=>'1', 'username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $DB->set_field('user', 'auth', 'nologin', array('username'=>'username1')); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 1 user got updated. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_updated', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(0, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); set_config('removeuser', AUTH_REMOVEUSER_FULLDELETE, 'auth_ldap'); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $this->delete_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_unassigned and user_deleted. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_deleted', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_unassigned', $event); $this->assertEquals(5, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(0, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(1, $DB->count_records('role_assignments')); $this->assertEquals(1, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); $this->create_ldap_user($connection, $topdn, 1); ob_start(); $sink = $this->redirectEvents(); $auth->sync_users(true); $events = $sink->get_events(); $sink->close(); ob_end_clean(); // Check events, 2 events role_assigned and user_created. $this->assertCount(2, $events); $event = array_pop($events); $this->assertInstanceOf('\core\event\role_assigned', $event); $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals(6, $DB->count_records('user', array('auth'=>'ldap'))); $this->assertEquals(1, $DB->count_records('user', array('username'=>'username1'))); $this->assertEquals(0, $DB->count_records('user', array('suspended'=>1))); $this->assertEquals(1, $DB->count_records('user', array('deleted'=>1))); $this->assertEquals(2, $DB->count_records('role_assignments')); $this->assertEquals(2, $DB->count_records('role_assignments', array('roleid'=>$creatorrole->id))); // Let's test syncing users in chunks of '1'. set_config('field_updatelocal_email', 'onlogin', 'auth_ldap'); set_config('sync_updateuserchunk', 1, 'auth_ldap'); /** @var auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $count = 0; ob_start(); $auth->sync_users_update_callback(function ($users, $updatekeys) use (&$count) { $count++; }); ob_end_clean(); // After updating in chunks of '1', we should have counted more than one update. $this->assertGreaterThan(1, $count); ob_start(); \core\cron::setup_user(); $cron = new sync_task(); $cron->execute(); $this->runAdhocTasks('\auth_ldap\task\asynchronous_sync_task'); $output = ob_get_contents(); ob_end_clean(); // Use Reflection to make protected constants available. $rp = new \ReflectionClassConstant(sync_task::class, 'MTRACE_MSG'); $synctaskmsg = $rp->getValue(); $rp = new \ReflectionClassConstant(asynchronous_sync_task::class, 'MTRACE_MSG'); $asynctaskmsg = $rp->getValue(); $this->assertMatchesRegularExpression( sprintf('/%s.*%s/s', $synctaskmsg, $asynctaskmsg), $output ); $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); ldap_close($connection); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_loggedin_event(): void { global $CFG, $DB, $USER; $this->resetAfterTest(); $this->assertFalse(isloggedin()); $user = $DB->get_record('user', array('username'=>'admin')); // Note: we are just going to trigger the function that calls the event, // not actually perform a LDAP login, for the sake of sanity. $ldap = new \auth_plugin_ldap(); // Set the key for the cache flag we want to set which is used by LDAP. set_cache_flag($ldap->pluginconfig . '/ntlmsess', sesskey(), $user->username, AUTH_NTLMTIMEOUT); // We are going to need to set the sesskey as the user's password in order for the LDAP log in to work. update_internal_user_password($user, sesskey()); // The function ntlmsso_finish is responsible for triggering the event, so call it directly and catch the event. $sink = $this->redirectEvents(); // We need to supress this function call, or else we will get the message "session_regenerate_id(): Cannot // regenerate session id - headers already sent" as the ntlmsso_finish function calls complete_user_login @$ldap->ntlmsso_finish(); $events = $sink->get_events(); $sink->close(); // Check that the event is valid. $this->assertCount(1, $events); $event = reset($events); $this->assertInstanceOf('\core\event\user_loggedin', $event); $this->assertEquals('user', $event->objecttable); $this->assertEquals('2', $event->objectid); $this->assertEquals(\context_system::instance()->id, $event->contextid); } /** * Test logging in via LDAP calls a user_loggedin event. */ public function test_ldap_user_signup(): void { global $CFG, $DB; // User to create. $user = array( 'username' => 'usersignuptest1', 'password' => 'Moodle2014!', 'idnumber' => 'idsignuptest1', 'firstname' => 'First Name User Test 1', 'lastname' => 'Last Name User Test 1', 'middlename' => 'Middle Name User Test 1', 'lastnamephonetic' => '最後のお名前のテスト一号', 'firstnamephonetic' => 'お名前のテスト一号', 'alternatename' => 'Alternate Name User Test 1', 'email' => 'usersignuptest1@example.com', 'description' => 'This is a description for user 1', 'city' => 'Perth', 'country' => 'AU', 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => 'ldap' ); if (!extension_loaded('ldap')) { $this->markTestSkipped('LDAP extension is not loaded.'); } $this->resetAfterTest(); if (!defined('TEST_AUTH_LDAP_HOST_URL') or !defined('TEST_AUTH_LDAP_BIND_DN') or !defined('TEST_AUTH_LDAP_BIND_PW') or !defined('TEST_AUTH_LDAP_DOMAIN')) { $this->markTestSkipped('External LDAP test server not configured.'); } // Make sure we can connect the server. $debuginfo = ''; if (!$connection = ldap_connect_moodle(TEST_AUTH_LDAP_HOST_URL, 3, 'rfc2307', TEST_AUTH_LDAP_BIND_DN, TEST_AUTH_LDAP_BIND_PW, LDAP_DEREF_NEVER, $debuginfo, false)) { $this->markTestSkipped('Can not connect to LDAP test server: '.$debuginfo); } $this->enable_plugin(); // Create new empty test container. $topdn = 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN; $this->recursive_delete($connection, TEST_AUTH_LDAP_DOMAIN, 'dc=moodletest'); $o = array(); $o['objectClass'] = array('dcObject', 'organizationalUnit'); $o['dc'] = 'moodletest'; $o['ou'] = 'MOODLETEST'; if (!ldap_add($connection, 'dc=moodletest,'.TEST_AUTH_LDAP_DOMAIN, $o)) { $this->markTestSkipped('Can not create test LDAP container.'); } // Create a few users. $o = array(); $o['objectClass'] = array('organizationalUnit'); $o['ou'] = 'users'; ldap_add($connection, 'ou='.$o['ou'].','.$topdn, $o); // Configure the plugin a bit. set_config('host_url', TEST_AUTH_LDAP_HOST_URL, 'auth_ldap'); set_config('start_tls', 0, 'auth_ldap'); set_config('ldap_version', 3, 'auth_ldap'); set_config('ldapencoding', 'utf-8', 'auth_ldap'); set_config('pagesize', '2', 'auth_ldap'); set_config('bind_dn', TEST_AUTH_LDAP_BIND_DN, 'auth_ldap'); set_config('bind_pw', TEST_AUTH_LDAP_BIND_PW, 'auth_ldap'); set_config('user_type', 'rfc2307', 'auth_ldap'); set_config('contexts', 'ou=users,'.$topdn, 'auth_ldap'); set_config('search_sub', 0, 'auth_ldap'); set_config('opt_deref', LDAP_DEREF_NEVER, 'auth_ldap'); set_config('user_attribute', 'cn', 'auth_ldap'); set_config('memberattribute', 'memberuid', 'auth_ldap'); set_config('memberattribute_isdn', 0, 'auth_ldap'); set_config('creators', 'cn=creators,'.$topdn, 'auth_ldap'); set_config('removeuser', AUTH_REMOVEUSER_KEEP, 'auth_ldap'); set_config('field_map_email', 'mail', 'auth_ldap'); set_config('field_updatelocal_email', 'oncreate', 'auth_ldap'); set_config('field_updateremote_email', '0', 'auth_ldap'); set_config('field_lock_email', 'unlocked', 'auth_ldap'); set_config('field_map_firstname', 'givenName', 'auth_ldap'); set_config('field_updatelocal_firstname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_firstname', '0', 'auth_ldap'); set_config('field_lock_firstname', 'unlocked', 'auth_ldap'); set_config('field_map_lastname', 'sn', 'auth_ldap'); set_config('field_updatelocal_lastname', 'oncreate', 'auth_ldap'); set_config('field_updateremote_lastname', '0', 'auth_ldap'); set_config('field_lock_lastname', 'unlocked', 'auth_ldap'); set_config('passtype', 'md5', 'auth_ldap'); set_config('create_context', 'ou=users,'.$topdn, 'auth_ldap'); $this->assertEquals(2, $DB->count_records('user')); $this->assertEquals(0, $DB->count_records('role_assignments')); /** @var \auth_plugin_ldap $auth */ $auth = get_auth_plugin('ldap'); $sink = $this->redirectEvents(); $mailsink = $this->redirectEmails(); $auth->user_signup((object)$user, false); $this->assertEquals(1, $mailsink->count()); $events = $sink->get_events(); $sink->close(); // Verify 2 events get generated. $this->assertCount(2, $events); // Get record from db. $dbuser = $DB->get_record('user', array('username' => $user['username'])); $user['id'] = $dbuser->id; // Last event is user_created. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_created', $event); $this->assertEquals($user['id'], $event->objectid); $this->assertEquals(\context_user::instance($user['id']), $event->get_context()); // First event is user_password_updated. $event = array_pop($events); $this->assertInstanceOf('\core\event\user_password_updated', $event); $this->assertEventContextNotUsed($event); // Delete user which we just created. ldap_delete($connection, 'cn='.$user['username'].',ou=users,'.$topdn); } protected function create_ldap_user($connection, $topdn, $i) { $o = array(); $o['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'posixAccount'); $o['cn'] = 'username'.$i; $o['sn'] = 'Lastname'.$i; $o['givenName'] = 'Firstname'.$i; $o['uid'] = $o['cn']; $o['uidnumber'] = 2000+$i; $o['gidNumber'] = 1000+$i; $o['homeDirectory'] = '/'; $o['mail'] = 'user'.$i.'@example.com'; $o['userPassword'] = 'pass'.$i; ldap_add($connection, 'cn='.$o['cn'].',ou=users,'.$topdn, $o); } protected function delete_ldap_user($connection, $topdn, $i) { ldap_delete($connection, 'cn=username'.$i.',ou=users,'.$topdn); } protected function enable_plugin() { $auths = get_enabled_auth_plugins(); if (!in_array('ldap', $auths)) { $auths[] = 'ldap'; } set_config('auth', implode(',', $auths)); } protected function recursive_delete($connection, $dn, $filter) { if ($res = ldap_list($connection, $dn, $filter, array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); if ($info['count'] > 0) { if ($res = ldap_search($connection, "$filter,$dn", 'cn=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn'])) { ldap_delete($connection, $i['dn']); } } } if ($res = ldap_search($connection, "$filter,$dn", 'ou=*', array('dn'))) { $info = ldap_get_entries($connection, $res); ldap_free_result($res); foreach ($info as $i) { if (isset($i['dn']) and $info[0]['dn'] != $i['dn']) { ldap_delete($connection, $i['dn']); } } } ldap_delete($connection, "$filter,$dn"); } } } } PK&9\#YCldap/ntlmsso_attempt.phpnu[set_url('/auth/ldap/ntlmsso_attempt.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = sesskey(); // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs) which in turn prevents a bug in both IE 6.x // and FF 3.x (Windows version at least) where javascript timers fire up even // when we've already left the page that set the timer. $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); $msg = '

'.get_string('ntlmsso_attempting', 'auth_ldap').'

' . ''; redirect($CFG->wwwroot . '/auth/ldap/ntlmsso_finish.php', $msg, 3); PK&9\ƫÆldap/ntlmsso_finish.phpnu[set_url('/auth/ldap/ntlmsso_finish.php'); $PAGE->set_context(context_system::instance()); // Define variables used in page $site = get_site(); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } // If ntlmsso_finish() succeeds, then the code never returns, // so we only worry about failure. if (!$authplugin->ntlmsso_finish()) { // Redirect to login, saying "don't try again!" // Display the page header. This makes redirect respect the timeout we specify // here (and not add 3 more secs). $loginsite = get_string("loginsite"); $PAGE->navbar->add($loginsite); $PAGE->set_title($loginsite); $PAGE->set_heading($site->fullname); echo $OUTPUT->header(); redirect($CFG->wwwroot . '/login/index.php?authldap_skipntlmsso=1', get_string('ntlmsso_failed','auth_ldap'), 3); } PK&9\`|| ldap/auth.phpnu[. /** * Authentication Plugin: LDAP Authentication * Authentication using LDAP (Lightweight Directory Access Protocol). * * @package auth_ldap * @author Martin Dougiamas * @author Iñaki Arenaza * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); // See http://support.microsoft.com/kb/305144 to interprete these values. if (!defined('AUTH_AD_ACCOUNTDISABLE')) { define('AUTH_AD_ACCOUNTDISABLE', 0x0002); } if (!defined('AUTH_AD_NORMAL_ACCOUNT')) { define('AUTH_AD_NORMAL_ACCOUNT', 0x0200); } if (!defined('AUTH_NTLMTIMEOUT')) { // timewindow for the NTLM SSO process, in secs... define('AUTH_NTLMTIMEOUT', 10); } // UF_DONT_EXPIRE_PASSWD value taken from MSDN directly if (!defined('UF_DONT_EXPIRE_PASSWD')) { define ('UF_DONT_EXPIRE_PASSWD', 0x00010000); } // The Posix uid and gid of the 'nobody' account and 'nogroup' group. if (!defined('AUTH_UID_NOBODY')) { define('AUTH_UID_NOBODY', -2); } if (!defined('AUTH_GID_NOGROUP')) { define('AUTH_GID_NOGROUP', -2); } // Regular expressions for a valid NTLM username and domain name. if (!defined('AUTH_NTLM_VALID_USERNAME')) { define('AUTH_NTLM_VALID_USERNAME', '[^/\\\\\\\\\[\]:;|=,+*?<>@"]+'); } if (!defined('AUTH_NTLM_VALID_DOMAINNAME')) { define('AUTH_NTLM_VALID_DOMAINNAME', '[^\\\\\\\\\/:*?"<>|]+'); } // Default format for remote users if using NTLM SSO if (!defined('AUTH_NTLM_DEFAULT_FORMAT')) { define('AUTH_NTLM_DEFAULT_FORMAT', '%domain%\\%username%'); } if (!defined('AUTH_NTLM_FASTPATH_ATTEMPT')) { define('AUTH_NTLM_FASTPATH_ATTEMPT', 0); } if (!defined('AUTH_NTLM_FASTPATH_YESFORM')) { define('AUTH_NTLM_FASTPATH_YESFORM', 1); } if (!defined('AUTH_NTLM_FASTPATH_YESATTEMPT')) { define('AUTH_NTLM_FASTPATH_YESATTEMPT', 2); } // Allows us to retrieve a diagnostic message in case of LDAP operation error if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) { define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032); } require_once($CFG->libdir.'/authlib.php'); require_once($CFG->libdir.'/ldaplib.php'); require_once($CFG->dirroot.'/user/lib.php'); require_once($CFG->dirroot.'/auth/ldap/locallib.php'); /** * LDAP authentication plugin. */ class auth_plugin_ldap extends auth_plugin_base { /** @var string */ protected $roleauth; /** @var string */ public $pluginconfig; /** @var LDAP\Connection LDAP connection. */ protected $ldapconnection; /** @var int */ protected $ldapconns = 0; /** * Init plugin config from database settings depending on the plugin auth type. */ function init_plugin($authtype) { $this->pluginconfig = 'auth_'.$authtype; $this->config = get_config($this->pluginconfig); if (empty($this->config->ldapencoding)) { $this->config->ldapencoding = 'utf-8'; } if (empty($this->config->user_type)) { $this->config->user_type = 'default'; } $ldap_usertypes = ldap_supported_usertypes(); $this->config->user_type_name = $ldap_usertypes[$this->config->user_type]; unset($ldap_usertypes); $default = ldap_getdefaults(); // Use defaults if values not given foreach ($default as $key => $value) { // watch out - 0, false are correct values too if (!isset($this->config->{$key}) or $this->config->{$key} == '') { $this->config->{$key} = $value[$this->config->user_type]; } } // Hack prefix to objectclass $this->config->objectclass = ldap_normalise_objectclass($this->config->objectclass); } /** * Constructor with initialisation. */ public function __construct() { $this->authtype = 'ldap'; $this->roleauth = 'auth_ldap'; $this->errorlogtag = '[AUTH LDAP] '; $this->init_plugin($this->authtype); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_ldap() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. * * @param string $username The username (without system magic quotes) * @param string $password The password (without system magic quotes) * * @return bool Authentication success or failure. */ function user_login($username, $password) { if (! function_exists('ldap_bind')) { throw new \moodle_exception('auth_ldapnotinstalled', 'auth_ldap'); return false; } if (!$username or !$password) { // Don't allow blank usernames or passwords return false; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($password, 'utf-8', $this->config->ldapencoding); // Before we connect to LDAP, check if this is an AD SSO login // if we succeed in this block, we'll return success early. // $key = sesskey(); if (!empty($this->config->ntlmsso_enabled) && $key === $password) { $sessusername = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); // We only get the cache flag if we retrieve it before // it expires (AUTH_NTLMTIMEOUT seconds). if (empty($sessusername)) { return false; } if ($username === $sessusername) { unset($sessusername); // Check that the user is inside one of the configured LDAP contexts $validuser = false; $ldapconnection = $this->ldap_connect(); // if the user is not inside the configured contexts, // ldap_find_userdn returns false. if ($this->ldap_find_userdn($ldapconnection, $extusername)) { $validuser = true; } $this->ldap_close(); // Shortcut here - SSO confirmed return $validuser; } } // End SSO processing unset($key); $ldapconnection = $this->ldap_connect(); $ldap_user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); // If ldap_user_dn is empty, user does not exist if (!$ldap_user_dn) { $this->ldap_close(); return false; } // Try to bind with current username and password $ldap_login = @ldap_bind($ldapconnection, $ldap_user_dn, $extpassword); // If login fails and we are using MS Active Directory, retrieve the diagnostic // message to see if this is due to an expired password, or that the user is forced to // change the password on first login. If it is, only proceed if we can change // password from Moodle (otherwise we'll get stuck later in the login process). if (!$ldap_login && ($this->config->user_type == 'ad') && $this->can_change_password() && (!empty($this->config->expiration) and ($this->config->expiration == 1))) { // We need to get the diagnostic message right after the call to ldap_bind(), // before any other LDAP operation. ldap_get_option($ldapconnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagmsg); if ($this->ldap_ad_pwdexpired_from_diagmsg($diagmsg)) { // If login failed because user must change the password now or the // password has expired, let the user in. We'll catch this later in the // login process when we explicitly check for expired passwords. $ldap_login = true; } } $this->ldap_close(); return $ldap_login; } /** * Reads user information from ldap and returns it in array() * * Function should return all information available. If you are saving * this information to moodle user-table you should honor syncronization flags * * @param string $username username * * @return mixed array with no magic quotes or false on error */ function get_userinfo($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extusername))) { $this->ldap_close(); return false; } $search_attribs = array(); $attrmap = $this->ldap_attributes(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if (!$user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs)) { $this->ldap_close(); return false; // error! } $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $this->ldap_close(); return false; // entry not found } $result = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } $ldapval = NULL; foreach ($values as $value) { $entry = $user_entry[0]; if (($value == 'dn') || ($value == 'distinguishedname')) { $result[$key] = $user_dn; continue; } if (!array_key_exists($value, $entry)) { continue; // wrong data mapping! } if (is_array($entry[$value])) { $newval = core_text::convert($entry[$value][0], $this->config->ldapencoding, 'utf-8'); } else { $newval = core_text::convert($entry[$value], $this->config->ldapencoding, 'utf-8'); } if (!empty($newval)) { // favour ldap entries that are set $ldapval = $newval; } } if (!is_null($ldapval)) { $result[$key] = $ldapval; } } $this->ldap_close(); return $result; } /** * Reads user information from ldap and returns it in an object * * @param string $username username (with system magic quotes) * @return mixed object or false on error */ function get_userinfo_asobj($username) { $user_array = $this->get_userinfo($username); if ($user_array == false) { return false; //error or not found } $user_array = truncate_userinfo($user_array); $user = new stdClass(); foreach ($user_array as $key=>$value) { $user->{$key} = $value; } return $user; } /** * Returns all usernames from LDAP * * get_userlist returns all usernames from LDAP * * @return array */ function get_userlist() { return $this->ldap_get_userlist("({$this->config->user_attribute}=*)"); } /** * Checks if user exists on LDAP * * @param string $username */ function user_exists($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); // Returns true if given username exists on ldap $users = $this->ldap_get_userlist('('.$this->config->user_attribute.'='.ldap_filter_addslashes($extusername).')'); return count($users); } /** * Creates a new user on LDAP. * By using information in userobject * Use user_exists to prevent duplicate usernames * * @param mixed $userobject Moodle userobject * @param mixed $plainpass Plaintext password */ function user_create($userobject, $plainpass) { $extusername = core_text::convert($userobject->username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($plainpass, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $attrmap = $this->ldap_attributes(); $newuser = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!empty($userobject->$key) ) { $newuser[$value] = core_text::convert($userobject->$key, 'utf-8', $this->config->ldapencoding); } } } //Following sets all mandatory and other forced attribute values //User should be creted as login disabled untill email confirmation is processed //Feel free to add your user type and send patches to paca@sci.fi to add them //Moodle distribution switch ($this->config->user_type) { case 'edir': $newuser['objectClass'] = array('inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['uniqueId'] = $extusername; $newuser['logindisabled'] = 'TRUE'; $newuser['userpassword'] = $extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'rfc2307': case 'rfc2307bis': // posixAccount object class forces us to specify a uidNumber // and a gidNumber. That is quite complicated to generate from // Moodle without colliding with existing numbers and without // race conditions. As this user is supposed to be only used // with Moodle (otherwise the user would exist beforehand) and // doesn't need to login into a operating system, we assign the // user the uid of user 'nobody' and gid of group 'nogroup'. In // addition to that, we need to specify a home directory. We // use the root directory ('/') as the home directory, as this // is the only one can always be sure exists. Finally, even if // it's not mandatory, we specify '/bin/false' as the login // shell, to prevent the user from login in at the operating // system level (Moodle ignores this). $newuser['objectClass'] = array('posixAccount', 'inetOrgPerson', 'organizationalPerson', 'person', 'top'); $newuser['cn'] = $extusername; $newuser['uid'] = $extusername; $newuser['uidNumber'] = AUTH_UID_NOBODY; $newuser['gidNumber'] = AUTH_GID_NOGROUP; $newuser['homeDirectory'] = '/'; $newuser['loginShell'] = '/bin/false'; // IMPORTANT: // We have to create the account locked, but posixAccount has // no attribute to achive this reliably. So we are going to // modify the password in a reversable way that we can later // revert in user_activate(). // // Beware that this can be defeated by the user if we are not // using MD5 or SHA-1 passwords. After all, the source code of // Moodle is available, and the user can see the kind of // modification we are doing and 'undo' it by hand (but only // if we are using plain text passwords). // // Also bear in mind that you need to use a binding user that // can create accounts and has read/write privileges on the // 'userPassword' attribute for this to work. $newuser['userPassword'] = '*'.$extpassword; $uadd = ldap_add($ldapconnection, $this->config->user_attribute.'='.ldap_addslashes($extusername).','.$this->config->create_context, $newuser); break; case 'ad': // User account creation is a two step process with AD. First you // create the user object, then you set the password. If you try // to set the password while creating the user, the operation // fails. // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { throw new \moodle_exception('auth_ldap_no_mbstring', 'auth_ldap'); } // Check for invalid sAMAccountName characters. if (preg_match('#[/\\[\]:;|=,+*?<>@"]#', $extusername)) { throw new \moodle_exception ('auth_ldap_ad_invalidchars', 'auth_ldap'); } // First create the user account, and mark it as disabled. $newuser['objectClass'] = array('top', 'person', 'user', 'organizationalPerson'); $newuser['sAMAccountName'] = $extusername; $newuser['userAccountControl'] = AUTH_AD_NORMAL_ACCOUNT | AUTH_AD_ACCOUNTDISABLE; $userdn = 'cn='.ldap_addslashes($extusername).','.$this->config->create_context; if (!ldap_add($ldapconnection, $userdn, $newuser)) { throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } // Now set the password unset($newuser); $newuser['unicodePwd'] = mb_convert_encoding('"' . $extpassword . '"', 'UCS-2LE', 'UTF-8'); if(!ldap_modify($ldapconnection, $userdn, $newuser)) { // Something went wrong: delete the user account and error out ldap_delete ($ldapconnection, $userdn); throw new \moodle_exception('auth_ldap_ad_create_req', 'auth_ldap'); } $uadd = true; break; default: throw new \moodle_exception('auth_ldap_unsupportedusertype', 'auth_ldap', '', $this->config->user_type_name); } $this->ldap_close(); return $uadd; } /** * Returns true if plugin allows resetting of password from moodle. * * @return bool */ function can_reset_password() { return !empty($this->config->stdchangepassword); } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Returns true if plugin allows signup and user creation. * * @return bool */ function can_signup() { return (!empty($this->config->auth_user_create) and !empty($this->config->create_context)); } /** * Sign up a new user ready for confirmation. * Password is passed in plaintext. * * @param object $user new user object * @param boolean $notify print notice with link and terminate * @return boolean success */ function user_signup($user, $notify=true) { global $CFG, $DB, $PAGE, $OUTPUT; require_once($CFG->dirroot.'/user/profile/lib.php'); require_once($CFG->dirroot.'/user/lib.php'); if ($this->user_exists($user->username)) { throw new \moodle_exception('auth_ldap_user_exists', 'auth_ldap'); } $plainslashedpassword = $user->password; unset($user->password); if (! $this->user_create($user, $plainslashedpassword)) { throw new \moodle_exception('auth_ldap_create_error', 'auth_ldap'); } $user->id = user_create_user($user, false, false); user_add_password_history($user->id, $plainslashedpassword); // Save any custom profile field information profile_save_data($user); $userinfo = $this->get_userinfo($user->username); $this->update_user_record($user->username, false, false, $this->is_user_suspended((object) $userinfo)); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). update_internal_user_password($user, $plainslashedpassword); $user = $DB->get_record('user', array('id'=>$user->id)); \core\event\user_created::create_from_userid($user->id)->trigger(); if (! send_confirmation_email($user)) { throw new \moodle_exception('noemail', 'auth_ldap'); } if ($notify) { $emailconfirm = get_string('emailconfirm'); $PAGE->set_url('/auth/ldap/auth.php'); $PAGE->navbar->add($emailconfirm); $PAGE->set_title($emailconfirm); $PAGE->set_heading($emailconfirm); echo $OUTPUT->header(); notice(get_string('emailconfirmsent', '', $user->email), "{$CFG->wwwroot}/index.php"); } else { return true; } } /** * Returns true if plugin allows confirming of new users. * * @return bool */ function can_confirm() { return $this->can_signup(); } /** * Confirm the new user as registered. * * @param string $username * @param string $confirmsecret */ 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 if (!$this->user_activate($username)) { return AUTH_CONFIRM_FAIL; } $user->confirmed = 1; user_update_user($user, false); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } /** * Return number of days to user password expires * * If userpassword does not expire it should return 0. If password is already expired * it should return negative value. * * @param mixed $username username * @return integer */ function password_expire($username) { $result = 0; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); $search_attribs = array($this->config->expireattr); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $info = ldap_get_entries_moodle($ldapconnection, $sr); if (!empty ($info)) { $info = $info[0]; if (isset($info[$this->config->expireattr][0])) { $expiretime = $this->ldap_expirationtime2unix($info[$this->config->expireattr][0], $ldapconnection, $user_dn); if ($expiretime != 0) { $now = time(); if ($expiretime > $now) { $result = ceil(($expiretime - $now) / DAYSECS); } else { $result = floor(($expiretime - $now) / DAYSECS); } } } } } else { error_log($this->errorlogtag.get_string('didtfindexpiretime', 'auth_ldap')); } return $result; } /** * Synchronise users from the external LDAP server to Moodle's user table. * * Calls sync_users_update_callback() with default callback if appropriate. * * @param bool $doupdates will do pull in data updates from LDAP if relevant * @return bool success */ public function sync_users($doupdates = true) { return $this->sync_users_update_callback($doupdates ? [$this, 'update_users'] : null); } /** * Synchronise users from the external LDAP server to Moodle's user table (callback). * * Sync is now using username attribute. * * Syncing users removes or suspends users that dont exists anymore in external LDAP. * Creates new users and updates coursecreator status of users. * * @param callable|null $updatecallback will do pull in data updates from LDAP if relevant * @return bool success */ public function sync_users_update_callback(?callable $updatecallback = null): bool { global $CFG, $DB; require_once($CFG->dirroot . '/user/profile/lib.php'); print_string('connectingldap', 'auth_ldap'); $ldapconnection = $this->ldap_connect(); $dbman = $DB->get_manager(); /// Define table user to be created $table = new xmldb_table('tmp_extuser'); $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); $table->add_field('username', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null); $table->add_field('mnethostid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null); $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); $table->add_index('username', XMLDB_INDEX_UNIQUE, array('mnethostid', 'username')); print_string('creatingtemptable', 'auth_ldap', 'tmp_extuser'); $dbman->create_temp_table($table); //// //// get user's list from ldap to sql in a scalable fashion //// // prepare some data we'll need $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldappagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); $ldapcookie = ''; foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldappagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldapcookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldapresult = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldapresult = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if (!$ldapresult) { continue; } if ($ldappagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldapcookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldapresult, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldapcookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } if ($entry = @ldap_first_entry($ldapconnection, $ldapresult)) { do { $value = ldap_get_values_len($ldapconnection, $entry, $this->config->user_attribute); $value = core_text::convert($value[0], $this->config->ldapencoding, 'utf-8'); $value = trim($value); $this->ldap_bulk_insert($value); } while ($entry = ldap_next_entry($ldapconnection, $entry)); } unset($ldapresult); // Free mem. } while ($ldappagedresults && $ldapcookie !== null && $ldapcookie != ''); } // If LDAP paged results were used, the current connection must be completely // closed and a new one created, to work without paged results from here on. if ($ldappagedresults) { $this->ldap_close(true); $ldapconnection = $this->ldap_connect(); } /// preserve our user database /// if the temp table is empty, it probably means that something went wrong, exit /// so as to avoid mass deletion of users; which is hard to undo $count = $DB->count_records_sql('SELECT COUNT(username) AS count, 1 FROM {tmp_extuser}'); if ($count < 1) { print_string('didntgetusersfromldap', 'auth_ldap'); $dbman->drop_table($table); $this->ldap_close(); return false; } else { print_string('gotcountrecordsfromldap', 'auth_ldap', $count); } /// User removal // Find users in DB that aren't in ldap -- to be removed! // this is still not as scalable (but how often do we mass delete?) if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { if (delete_user($user)) { echo "\t"; print_string('auth_dbdeleteuser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } else { echo "\t"; print_string('auth_dbdeleteusererror', 'auth_db', $user->username); echo "\n"; } } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } else if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.* FROM {user} u LEFT JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE u.auth = :auth AND u.deleted = 0 AND u.suspended = 0 AND e.username IS NULL"; $remove_users = $DB->get_records_sql($sql, array('auth'=>$this->authtype)); if (!empty($remove_users)) { print_string('userentriestoremove', 'auth_ldap', count($remove_users)); foreach ($remove_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbsuspenduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; \core\session\manager::destroy_user_sessions($user->id); } } else { print_string('nouserentriestoremove', 'auth_ldap'); } unset($remove_users); // Free mem! } /// Revive suspended users if (!empty($this->config->removeuser) and $this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $sql = "SELECT u.id, u.username FROM {user} u JOIN {tmp_extuser} e ON (u.username = e.username AND u.mnethostid = e.mnethostid) WHERE (u.auth = 'nologin' OR (u.auth = ? AND u.suspended = 1)) AND u.deleted = 0"; // Note: 'nologin' is there for backwards compatibility. $revive_users = $DB->get_records_sql($sql, array($this->authtype)); if (!empty($revive_users)) { print_string('userentriestorevive', 'auth_ldap', count($revive_users)); foreach ($revive_users as $user) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->auth = $this->authtype; $updateuser->suspended = 0; user_update_user($updateuser, false); echo "\t"; print_string('auth_dbreviveduser', 'auth_db', array('name'=>$user->username, 'id'=>$user->id)); echo "\n"; } } else { print_string('nouserentriestorevive', 'auth_ldap'); } unset($revive_users); } /// User Updates - time-consuming (optional) if ($updatecallback && $updatekeys = $this->get_profile_keys()) { // Run updates only if relevant. $users = $DB->get_records_sql('SELECT u.username, u.id FROM {user} u WHERE u.deleted = 0 AND u.auth = ? AND u.mnethostid = ?', array($this->authtype, $CFG->mnet_localhost_id)); if (!empty($users)) { // Update users in chunks as specified in sync_updateuserchunk. if (!empty($this->config->sync_updateuserchunk)) { foreach (array_chunk($users, $this->config->sync_updateuserchunk) as $chunk) { call_user_func($updatecallback, $chunk, $updatekeys); } } else { call_user_func($updatecallback, $users, $updatekeys); } unset($users); // Free mem. } } else { print_string('noupdatestobedone', 'auth_ldap'); } /// User Additions // Find users missing in DB that are in LDAP // and gives me a nifty object I don't want. // note: we do not care about deleted accounts anymore, this feature was replaced by suspending to nologin auth plugin $sql = 'SELECT e.id, e.username FROM {tmp_extuser} e LEFT JOIN {user} u ON (e.username = u.username AND e.mnethostid = u.mnethostid) WHERE u.id IS NULL'; $add_users = $DB->get_records_sql($sql); if (!empty($add_users)) { print_string('userentriestoadd', 'auth_ldap', count($add_users)); $errors = 0; foreach ($add_users as $user) { $transaction = $DB->start_delegated_transaction(); $user = $this->get_userinfo_asobj($user->username); // Prep a few params $user->modified = time(); $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; // get_userinfo_asobj() might have replaced $user->username with the value // from the LDAP server (which can be mixed-case). Make sure it's lowercase $user->username = trim(core_text::strtolower($user->username)); // It isn't possible to just rely on the configured suspension attribute since // things like active directory use bit masks, other things using LDAP might // do different stuff as well. // // The cast to int is a workaround for MDL-53959. $user->suspended = (int)$this->is_user_suspended($user); if (empty($user->calendartype)) { $user->calendartype = $CFG->calendartype; } // $id = user_create_user($user, false); try { $id = user_create_user($user, false); } catch (Exception $e) { print_string('invaliduserexception', 'auth_ldap', print_r($user, true) . $e->getMessage()); $errors++; $transaction->allow_commit(); continue; } echo "\t"; print_string('auth_dbinsertuser', 'auth_db', array('name'=>$user->username, 'id'=>$id)); echo "\n"; $euser = $DB->get_record('user', array('id' => $id)); if (!empty($this->config->forcechangepassword)) { set_user_preference('auth_forcepasswordchange', 1, $id); } // Save custom profile fields. $this->update_user_record($user->username, $this->get_profile_keys(true), false); // Add roles if needed. $this->sync_roles($euser); $transaction->allow_commit(); } // Display number of user creation errors, if any. if ($errors) { print_string('invalidusererrors', 'auth_ldap', $errors); } unset($add_users); // free mem } else { print_string('nouserstobeadded', 'auth_ldap'); } $dbman->drop_table($table); $this->ldap_close(); return true; } /** * Update users from the external LDAP server into Moodle's user table. * * Sync helper * * @param array $users chunk of users to update * @param array $updatekeys fields to update */ public function update_users(array $users, array $updatekeys): void { global $DB; print_string('userentriestoupdate', 'auth_ldap', count($users)); foreach ($users as $user) { $transaction = $DB->start_delegated_transaction(); echo "\t"; print_string('auth_dbupdatinguser', 'auth_db', ['name' => $user->username, 'id' => $user->id]); $userinfo = $this->get_userinfo($user->username); if (!$this->update_user_record($user->username, $updatekeys, true, $this->is_user_suspended((object) $userinfo))) { echo ' - '.get_string('skipped'); } echo "\n"; // Update system roles, if needed. $this->sync_roles($user); $transaction->allow_commit(); } } /** * Bulk insert in SQL's temp table */ function ldap_bulk_insert($username) { global $DB, $CFG; $username = core_text::strtolower($username); // usernames are __always__ lowercase. $DB->insert_record_raw('tmp_extuser', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id), false, true); echo '.'; } /** * Activates (enables) user in external LDAP so user can login * * @param mixed $username * @return boolean result */ function user_activate($username) { $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $userdn = $this->ldap_find_userdn($ldapconnection, $extusername); switch ($this->config->user_type) { case 'edir': $newinfo['loginDisabled'] = 'FALSE'; break; case 'rfc2307': case 'rfc2307bis': // Remember that we add a '*' character in front of the // external password string to 'disable' the account. We just // need to remove it. $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userPassword')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userPassword'] = ltrim($info[0]['userpassword'][0], '*'); break; case 'ad': // We need to unset the ACCOUNTDISABLE bit in the // userAccountControl attribute ( see // http://support.microsoft.com/kb/305144 ) $sr = ldap_read($ldapconnection, $userdn, '(objectClass=*)', array('userAccountControl')); $info = ldap_get_entries($ldapconnection, $sr); $info[0] = array_change_key_case($info[0], CASE_LOWER); $newinfo['userAccountControl'] = $info[0]['useraccountcontrol'][0] & (~AUTH_AD_ACCOUNTDISABLE); break; default: throw new \moodle_exception('user_activatenotsupportusertype', 'auth_ldap', '', $this->config->user_type_name); } $result = ldap_modify($ldapconnection, $userdn, $newinfo); $this->ldap_close(); return $result; } /** * Returns true if user should be coursecreator. * * @param mixed $username username (without system magic quotes) * @return mixed result null if course creators is not configured, boolean otherwise. * * @deprecated since Moodle 3.4 MDL-30634 - please do not use this function any more. */ function iscreator($username) { debugging('iscreator() is deprecated. Please use auth_plugin_ldap::is_role() instead.', DEBUG_DEVELOPER); if (empty($this->config->creators) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if(!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $group_dns = explode(';', $this->config->creators); $creator = ldap_isgroupmember($ldapconnection, $userid, $group_dns, $this->config->memberattribute); $this->ldap_close(); return $creator; } /** * Check if user has LDAP group membership. * * Returns true if user should be assigned role. * * @param mixed $username username (without system magic quotes). * @param array $role Array of role's shortname, localname, and settingname for the config value. * @return mixed result null if role/LDAP context is not configured, boolean otherwise. */ private function is_role($username, $role) { if (empty($this->config->{$role['settingname']}) or empty($this->config->memberattribute)) { return null; } $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); if ($this->config->memberattribute_isdn) { if (!($userid = $this->ldap_find_userdn($ldapconnection, $extusername))) { return false; } } else { $userid = $extusername; } $groupdns = explode(';', $this->config->{$role['settingname']}); $isrole = ldap_isgroupmember($ldapconnection, $userid, $groupdns, $this->config->memberattribute); $this->ldap_close(); return $isrole; } /** * Called when the user record is updated. * * Modifies user in external LDAP server. It takes olduser (before * changes) and newuser (after changes) compares information and * saves modified information to external LDAP server. * * @param mixed $olduser Userobject before modifications (without system magic quotes) * @param mixed $newuser Userobject new modified userobject (without system magic quotes) * @return boolean result * */ function user_update($olduser, $newuser) { global $CFG; require_once($CFG->dirroot . '/user/profile/lib.php'); if (isset($olduser->username) and isset($newuser->username) and $olduser->username != $newuser->username) { error_log($this->errorlogtag.get_string('renamingnotallowed', 'auth_ldap')); return false; } if (isset($olduser->auth) and $olduser->auth != $this->authtype) { return true; // just change auth and skip update } $attrmap = $this->ldap_attributes(); // Before doing anything else, make sure we really need to update anything // in the external LDAP server. $update_external = false; foreach ($attrmap as $key => $ldapkeys) { if (!empty($this->config->{'field_updateremote_'.$key})) { $update_external = true; break; } } if (!$update_external) { return true; } $extoldusername = core_text::convert($olduser->username, 'utf-8', $this->config->ldapencoding); $ldapconnection = $this->ldap_connect(); $search_attribs = array(); foreach ($attrmap as $key => $values) { if (!is_array($values)) { $values = array($values); } foreach ($values as $value) { if (!in_array($value, $search_attribs)) { array_push($search_attribs, $value); } } } if(!($user_dn = $this->ldap_find_userdn($ldapconnection, $extoldusername))) { return false; } // Load old custom fields. $olduserprofilefields = (array) profile_user_record($olduser->id, false); $fields = array(); foreach (profile_get_custom_fields(false) as $field) { $fields[$field->shortname] = $field; } $success = true; $user_info_result = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($user_info_result) { $user_entry = ldap_get_entries_moodle($ldapconnection, $user_info_result); if (empty($user_entry)) { $attribs = join (', ', $search_attribs); error_log($this->errorlogtag.get_string('updateusernotfound', 'auth_ldap', array('userdn'=>$user_dn, 'attribs'=>$attribs))); return false; // old user not found! } else if (count($user_entry) > 1) { error_log($this->errorlogtag.get_string('morethanoneuser', 'auth_ldap')); return false; } $user_entry = $user_entry[0]; foreach ($attrmap as $key => $ldapkeys) { if (preg_match('/^profile_field_(.*)$/', $key, $match)) { // Custom field. $fieldname = $match[1]; if (isset($fields[$fieldname])) { $class = 'profile_field_' . $fields[$fieldname]->datatype; $formfield = new $class($fields[$fieldname]->id, $olduser->id); $oldvalue = isset($olduserprofilefields[$fieldname]) ? $olduserprofilefields[$fieldname] : null; } else { $oldvalue = null; } $newvalue = $formfield->edit_save_data_preprocess($newuser->{$formfield->inputname}, new stdClass); } else { // Standard field. $oldvalue = isset($olduser->$key) ? $olduser->$key : null; $newvalue = isset($newuser->$key) ? $newuser->$key : null; } if ($newvalue !== null and $newvalue !== $oldvalue and !empty($this->config->{'field_updateremote_' . $key})) { // For ldap values that could be in more than one // ldap key, we will do our best to match // where they came from $ambiguous = true; $changed = false; if (!is_array($ldapkeys)) { $ldapkeys = array($ldapkeys); } if (count($ldapkeys) < 2) { $ambiguous = false; } $nuvalue = core_text::convert($newvalue, 'utf-8', $this->config->ldapencoding); empty($nuvalue) ? $nuvalue = array() : $nuvalue; $ouvalue = core_text::convert($oldvalue, 'utf-8', $this->config->ldapencoding); foreach ($ldapkeys as $ldapkey) { // If the field is empty in LDAP there are two options: // 1. We get the LDAP field using ldap_first_attribute. // 2. LDAP don't send the field using ldap_first_attribute. // So, for option 1 we check the if the field is retrieve it. // And get the original value of field in LDAP if the field. // Otherwise, let value in blank and delegate the check in ldap_modify. if (isset($user_entry[$ldapkey][0])) { $ldapvalue = $user_entry[$ldapkey][0]; } else { $ldapvalue = ''; } if (!$ambiguous) { // Skip update if the values already match if ($nuvalue !== $ldapvalue) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } else { // Ambiguous. Value empty before in Moodle (and LDAP) - use // 1st ldap candidate field, no need to guess if ($ouvalue === '') { // value empty before - use 1st ldap candidate // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } // We found which ldap key to update! if ($ouvalue !== '' and $ouvalue === $ldapvalue ) { // This might fail due to schema validation if (@ldap_modify($ldapconnection, $user_dn, array($ldapkey => $nuvalue))) { $changed = true; continue; } else { $success = false; error_log($this->errorlogtag.get_string ('updateremfail', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection)), 'key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); continue; } } } } if ($ambiguous and !$changed) { $success = false; error_log($this->errorlogtag.get_string ('updateremfailamb', 'auth_ldap', array('key'=>$key, 'ouvalue'=>$ouvalue, 'nuvalue'=>$nuvalue))); } } } } else { error_log($this->errorlogtag.get_string ('usernotfound', 'auth_ldap')); $success = false; } $this->ldap_close(); return $success; } /** * Changes userpassword in LDAP * * Called when the user password is updated. It assumes it is * called by an admin or that you've otherwise checked the user's * credentials * * @param object $user User table object * @param string $newpassword Plaintext password (not crypted/md5'ed) * @return boolean result * */ function user_update_password($user, $newpassword) { global $USER; $result = false; $username = $user->username; $extusername = core_text::convert($username, 'utf-8', $this->config->ldapencoding); $extpassword = core_text::convert($newpassword, 'utf-8', $this->config->ldapencoding); switch ($this->config->passtype) { case 'md5': $extpassword = '{MD5}' . base64_encode(pack('H*', md5($extpassword))); break; case 'sha1': $extpassword = '{SHA}' . base64_encode(pack('H*', sha1($extpassword))); break; case 'plaintext': default: break; // plaintext } $ldapconnection = $this->ldap_connect(); $user_dn = $this->ldap_find_userdn($ldapconnection, $extusername); if (!$user_dn) { error_log($this->errorlogtag.get_string ('nodnforusername', 'auth_ldap', $user->username)); return false; } switch ($this->config->user_type) { case 'edir': // Change password $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } // Update password expiration time, grace logins count $search_attribs = array($this->config->expireattr, 'passwordExpirationInterval', 'loginGraceLimit'); $sr = ldap_read($ldapconnection, $user_dn, '(objectClass=*)', $search_attribs); if ($sr) { $entry = ldap_get_entries_moodle($ldapconnection, $sr); $info = $entry[0]; $newattrs = array(); if (!empty($info[$this->config->expireattr][0])) { // Set expiration time only if passwordExpirationInterval is defined if (!empty($info['passwordexpirationinterval'][0])) { $expirationtime = time() + $info['passwordexpirationinterval'][0]; $ldapexpirationtime = $this->ldap_unix2expirationtime($expirationtime); $newattrs['passwordExpirationTime'] = $ldapexpirationtime; } // Set gracelogin count if (!empty($info['logingracelimit'][0])) { $newattrs['loginGraceRemaining']= $info['logingracelimit'][0]; } // Store attribute changes in LDAP $result = ldap_modify($ldapconnection, $user_dn, $newattrs); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserrorexpiregrace', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } } else { error_log($this->errorlogtag.get_string ('updatepasserrorexpire', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; case 'ad': // Passwords in Active Directory must be encoded as Unicode // strings (UCS-2 Little Endian format) and surrounded with // double quotes. See http://support.microsoft.com/?kbid=269190 if (!function_exists('mb_convert_encoding')) { error_log($this->errorlogtag.get_string ('needmbstring', 'auth_ldap')); return false; } $extpassword = mb_convert_encoding('"'.$extpassword.'"', "UCS-2LE", $this->config->ldapencoding); $result = ldap_modify($ldapconnection, $user_dn, array('unicodePwd' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } break; default: // Send LDAP the password in cleartext, it will md5 it itself $result = ldap_modify($ldapconnection, $user_dn, array('userPassword' => $extpassword)); if (!$result) { error_log($this->errorlogtag.get_string ('updatepasserror', 'auth_ldap', array('errno'=>ldap_errno($ldapconnection), 'errstring'=>ldap_err2str(ldap_errno($ldapconnection))))); } } $this->ldap_close(); return $result; } /** * Take expirationtime and return it as unix timestamp in seconds * * Takes expiration timestamp as read from LDAP and returns it as unix timestamp in seconds * Depends on $this->config->user_type variable * * @param mixed time Time stamp read from LDAP as it is. * @param string $ldapconnection Only needed for Active Directory. * @param string $user_dn User distinguished name for the user we are checking password expiration (only needed for Active Directory). * @return timestamp */ function ldap_expirationtime2unix($time, $ldapconnection, $user_dn) { $result = false; switch ($this->config->user_type) { case 'edir': $yr=substr($time, 0, 4); $mo=substr($time, 4, 2); $dt=substr($time, 6, 2); $hr=substr($time, 8, 2); $min=substr($time, 10, 2); $sec=substr($time, 12, 2); $result = mktime($hr, $min, $sec, $mo, $dt, $yr); break; case 'rfc2307': case 'rfc2307bis': $result = $time * DAYSECS; // The shadowExpire contains the number of DAYS between 01/01/1970 and the actual expiration date break; case 'ad': $result = $this->ldap_get_ad_pwdexpire($time, $ldapconnection, $user_dn); break; default: throw new \moodle_exception('auth_ldap_usertypeundefined', 'auth_ldap'); } return $result; } /** * Takes unix timestamp and returns it formated for storing in LDAP * * @param integer unix time stamp */ function ldap_unix2expirationtime($time) { $result = false; switch ($this->config->user_type) { case 'edir': $result=date('YmdHis', $time).'Z'; break; case 'rfc2307': case 'rfc2307bis': $result = $time ; // Already in correct format break; default: throw new \moodle_exception('auth_ldap_usertypeundefined2', 'auth_ldap'); } return $result; } /** * Returns user attribute mappings between moodle and LDAP * * @return array */ function ldap_attributes() { $moodleattributes = array(); // If we have custom fields then merge them with user fields. $customfields = $this->get_custom_user_profile_fields(); if (!empty($customfields) && !empty($this->userfields)) { $userfields = array_merge($this->userfields, $customfields); } else { $userfields = $this->userfields; } foreach ($userfields as $field) { if (!empty($this->config->{"field_map_$field"})) { $moodleattributes[$field] = core_text::strtolower(trim($this->config->{"field_map_$field"})); if (preg_match('/,/', $moodleattributes[$field])) { $moodleattributes[$field] = explode(',', $moodleattributes[$field]); // split ? } } } $moodleattributes['username'] = core_text::strtolower(trim($this->config->user_attribute)); $moodleattributes['suspended'] = core_text::strtolower(trim($this->config->suspended_attribute)); return $moodleattributes; } /** * Returns all usernames from LDAP * * @param $filter An LDAP search filter to select desired users * @return array of LDAP user names converted to UTF-8 */ function ldap_get_userlist($filter='*') { $fresult = array(); $ldapconnection = $this->ldap_connect(); if ($filter == '*') { $filter = '(&('.$this->config->user_attribute.'=*)'.$this->config->objectclass.')'; } $servercontrols = array(); $contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($contexts, $this->config->create_context); } $ldap_cookie = ''; $ldap_pagedresults = ldap_paged_results_supported($this->config->ldap_version, $ldapconnection); foreach ($contexts as $context) { $context = trim($context); if (empty($context)) { continue; } do { if ($ldap_pagedresults) { $servercontrols = array(array( 'oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => array( 'size' => $this->config->pagesize, 'cookie' => $ldap_cookie))); } if ($this->config->search_sub) { // Use ldap_search to find first user from subtree. $ldap_result = ldap_search($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } else { // Search only in this context. $ldap_result = ldap_list($ldapconnection, $context, $filter, array($this->config->user_attribute), 0, -1, -1, LDAP_DEREF_NEVER, $servercontrols); } if(!$ldap_result) { continue; } if ($ldap_pagedresults) { // Get next server cookie to know if we'll need to continue searching. $ldap_cookie = ''; // Get next cookie from controls. ldap_parse_result($ldapconnection, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $controls); if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) { $ldap_cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie']; } } $users = ldap_get_entries_moodle($ldapconnection, $ldap_result); // Add found users to list. for ($i = 0; $i < count($users); $i++) { $extuser = core_text::convert($users[$i][$this->config->user_attribute][0], $this->config->ldapencoding, 'utf-8'); array_push($fresult, $extuser); } unset($ldap_result); // Free mem. } while ($ldap_pagedresults && !empty($ldap_cookie)); } // If paged results were used, make sure the current connection is completely closed $this->ldap_close($ldap_pagedresults); return $fresult; } /** * Indicates if password hashes should be stored in local moodle database. * * @return bool true means flag 'not_cached' stored instead of password hash */ function prevent_local_passwords() { return !empty($this->config->preventpassindb); } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return false; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return !empty($this->config->stdchangepassword) or !empty($this->config->changepasswordurl); } /** * Returns the URL for changing the user's password, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { if (empty($this->config->stdchangepassword)) { if (!empty($this->config->changepasswordurl)) { return new moodle_url($this->config->changepasswordurl); } else { return null; } } else { return null; } } /** * Will get called before the login page is shownr. Ff NTLM SSO * is enabled, and the user is in the right network, we'll redirect * to the magic NTLM page for SSO... * */ function loginpage_hook() { global $CFG, $SESSION; // HTTPS is potentially required //httpsrequired(); - this must be used before setting the URL, it is already done on the login/index.php if (($_SERVER['REQUEST_METHOD'] === 'GET' // Only on initial GET of loginpage || ($_SERVER['REQUEST_METHOD'] === 'POST' && (get_local_referer() != strip_querystring(qualified_me())))) // Or when POSTed from another place // See MDL-14071 && !empty($this->config->ntlmsso_enabled) // SSO enabled && !empty($this->config->ntlmsso_subnet) // have a subnet to test for && empty($_GET['authldap_skipntlmsso']) // haven't failed it yet && (isguestuser() || !isloggedin()) // guestuser or not-logged-in users && address_in_subnet(getremoteaddr(), $this->config->ntlmsso_subnet)) { // First, let's remember where we were trying to get to before we got here if (empty($SESSION->wantsurl)) { $SESSION->wantsurl = null; $referer = get_local_referer(false); if ($referer && $referer != $CFG->wwwroot && $referer != $CFG->wwwroot . '/' && $referer != $CFG->wwwroot . '/login/' && $referer != $CFG->wwwroot . '/login/index.php') { $SESSION->wantsurl = $referer; } } // Now start the whole NTLM machinery. if($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESATTEMPT || $this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { if (core_useragent::is_ie()) { $sesskey = sesskey(); redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_magic.php?sesskey='.$sesskey); } else if ($this->config->ntlmsso_ie_fastpath == AUTH_NTLM_FASTPATH_YESFORM) { redirect($CFG->wwwroot.'/login/index.php?authldap_skipntlmsso=1'); } } redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_attempt.php'); } // No NTLM SSO, Use the normal login page instead. // If $SESSION->wantsurl is empty and we have a 'Referer:' header, the login // page insists on redirecting us to that page after user validation. If // we clicked on the redirect link at the ntlmsso_finish.php page (instead // of waiting for the redirection to happen) then we have a 'Referer:' header // we don't want to use at all. As we can't get rid of it, just point // $SESSION->wantsurl to $CFG->wwwroot (after all, we came from there). if (empty($SESSION->wantsurl) && (get_local_referer() == $CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php')) { $SESSION->wantsurl = $CFG->wwwroot; } } /** * To be called from a page running under NTLM's * "Integrated Windows Authentication". * * If successful, it will set a special "cookie" (not an HTTP cookie!) * in cache_flags under the $this->pluginconfig/ntlmsess "plugin" and return true. * The "cookie" will be picked up by ntlmsso_finish() to complete the * process. * * On failure it will return false for the caller to display an appropriate * error message (probably saying that Integrated Windows Auth isn't enabled!) * * NOTE that this code will execute under the OS user credentials, * so we MUST avoid dealing with files -- such as session files. * (The caller should define('NO_MOODLE_COOKIES', true) before including config.php) * */ function ntlmsso_magic($sesskey) { if (isset($_SERVER['REMOTE_USER']) && !empty($_SERVER['REMOTE_USER'])) { // HTTP __headers__ seem to be sent in ISO-8859-1 encoding // (according to my reading of RFC-1945, RFC-2616 and RFC-2617 and // my local tests), so we need to convert the REMOTE_USER value // (i.e., what we got from the HTTP WWW-Authenticate header) into UTF-8 $username = core_text::convert($_SERVER['REMOTE_USER'], 'iso-8859-1', 'utf-8'); switch ($this->config->ntlmsso_type) { case 'ntlm': // The format is now configurable, so try to extract the username $username = $this->get_ntlm_remote_user($username); if (empty($username)) { return false; } break; case 'kerberos': // Format is username@DOMAIN $username = substr($username, 0, strpos($username, '@')); break; default: error_log($this->errorlogtag.get_string ('ntlmsso_unknowntype', 'auth_ldap')); return false; // Should never happen! } $username = core_text::strtolower($username); // Compatibility hack set_cache_flag($this->pluginconfig.'/ntlmsess', $sesskey, $username, AUTH_NTLMTIMEOUT); return true; } return false; } /** * Find the session set by ntlmsso_magic(), validate it and * call authenticate_user_login() to authenticate the user through * the auth machinery. * * It is complemented by a similar check in user_login(). * * If it succeeds, it never returns. * */ function ntlmsso_finish() { global $CFG, $USER, $SESSION; $key = sesskey(); $username = get_cache_flag($this->pluginconfig.'/ntlmsess', $key); if (empty($username)) { return false; } // Here we want to trigger the whole authentication machinery // to make sure no step is bypassed... $reason = null; $user = authenticate_user_login($username, $key, false, $reason, false); if ($user) { complete_user_login($user); // Cleanup the key to prevent reuse... // and to allow re-logins with normal credentials unset_cache_flag($this->pluginconfig.'/ntlmsess', $key); // Redirection if (user_not_fully_set_up($USER, true)) { $urltogo = $CFG->wwwroot.'/user/edit.php'; // We don't delete $SESSION->wantsurl yet, so we get there later } else if (isset($SESSION->wantsurl) and (strpos($SESSION->wantsurl, $CFG->wwwroot) === 0)) { $urltogo = $SESSION->wantsurl; // Because it's an address in this site unset($SESSION->wantsurl); } else { // No wantsurl stored or external - go to homepage $urltogo = $CFG->wwwroot.'/'; unset($SESSION->wantsurl); } // We do not want to redirect if we are in a PHPUnit test. if (!PHPUNIT_TEST) { redirect($urltogo); } } // Should never reach here. return false; } /** * Sync roles for this user. * * @param object $user The user to sync (without system magic quotes). */ function sync_roles($user) { global $DB; $roles = get_ldap_assignable_role_names(2); // Admin user. foreach ($roles as $role) { $isrole = $this->is_role($user->username, $role); if ($isrole === null) { continue; // Nothing to sync - role/LDAP contexts not configured. } // Sync user. $systemcontext = context_system::instance(); if ($isrole) { // Following calls will not create duplicates. role_assign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } else { // Unassign only if previously assigned by this plugin. role_unassign($role['id'], $user->id, $systemcontext->id, $this->roleauth); } } } /** * Get password expiration time for a given user from Active Directory * * @param string $pwdlastset The time last time we changed the password. * @param resource $lcapconn The open LDAP connection. * @param string $user_dn The distinguished name of the user we are checking. * * @return string $unixtime */ function ldap_get_ad_pwdexpire($pwdlastset, $ldapconn, $user_dn){ global $CFG; if (!function_exists('bcsub')) { error_log($this->errorlogtag.get_string ('needbcmath', 'auth_ldap')); return 0; } // If UF_DONT_EXPIRE_PASSWD flag is set in user's // userAccountControl attribute, the password doesn't expire. $sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('userAccountControl')); if (!$sr) { error_log($this->errorlogtag.get_string ('useracctctrlerror', 'auth_ldap', $user_dn)); // Don't expire password, as we are not sure if it has to be // expired or not. return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $useraccountcontrol = $info['useraccountcontrol'][0]; if ($useraccountcontrol & UF_DONT_EXPIRE_PASSWD) { // Password doesn't expire. return 0; } // If pwdLastSet is zero, the user must change his/her password now // (unless UF_DONT_EXPIRE_PASSWD flag is set, but we already // tested this above) if ($pwdlastset === '0') { // Password has expired return -1; } // ---------------------------------------------------------------- // Password expiration time in Active Directory is the composition of // two values: // // - User's pwdLastSet attribute, that stores the last time // the password was changed. // // - Domain's maxPwdAge attribute, that sets how long // passwords last in this domain. // // We already have the first value (passed in as a parameter). We // need to get the second one. As we don't know the domain DN, we // have to query rootDSE's defaultNamingContext attribute to get // it. Then we have to query that DN's maxPwdAge attribute to get // the real value. // // Once we have both values, we just need to combine them. But MS // chose to use a different base and unit for time measurements. // So we need to convert the values to Unix timestamps (see // details below). // ---------------------------------------------------------------- $sr = ldap_read($ldapconn, ROOTDSE, '(objectClass=*)', array('defaultNamingContext')); if (!$sr) { error_log($this->errorlogtag.get_string ('rootdseerror', 'auth_ldap')); return 0; } $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $domaindn = $info['defaultnamingcontext'][0]; $sr = ldap_read ($ldapconn, $domaindn, '(objectClass=*)', array('maxPwdAge')); $entry = ldap_get_entries_moodle($ldapconn, $sr); $info = $entry[0]; $maxpwdage = $info['maxpwdage'][0]; if ($sr = ldap_read($ldapconn, $user_dn, '(objectClass=*)', array('msDS-ResultantPSO'))) { if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; $userpso = $info['msds-resultantpso'][0]; // If a PSO exists, FGPP is being utilized. // Grab the new maxpwdage from the msDS-MaximumPasswordAge attribute of the PSO. if (!empty($userpso)) { $sr = ldap_read($ldapconn, $userpso, '(objectClass=*)', array('msDS-MaximumPasswordAge')); if ($entry = ldap_get_entries_moodle($ldapconn, $sr)) { $info = $entry[0]; // Default value of msds-maximumpasswordage is 42 and is always set. $maxpwdage = $info['msds-maximumpasswordage'][0]; } } } } // ---------------------------------------------------------------- // MSDN says that "pwdLastSet contains the number of 100 nanosecond // intervals since January 1, 1601 (UTC), stored in a 64 bit integer". // // According to Perl's Date::Manip, the number of seconds between // this date and Unix epoch is 11644473600. So we have to // substract this value to calculate a Unix time, once we have // scaled pwdLastSet to seconds. This is the script used to // calculate the value shown above: // // #!/usr/bin/perl -w // // use Date::Manip; // // $date1 = ParseDate ("160101010000 UTC"); // $date2 = ParseDate ("197001010000 UTC"); // $delta = DateCalc($date1, $date2, \$err); // $secs = Delta_Format($delta, 0, "%st"); // print "$secs \n"; // // MSDN also says that "maxPwdAge is stored as a large integer that // represents the number of 100 nanosecond intervals from the time // the password was set before the password expires." We also need // to scale this to seconds. Bear in mind that this value is stored // as a _negative_ quantity (at least in my AD domain). // // As a last remark, if the low 32 bits of maxPwdAge are equal to 0, // the maximum password age in the domain is set to 0, which means // passwords do not expire (see // http://msdn2.microsoft.com/en-us/library/ms974598.aspx) // // As the quantities involved are too big for PHP integers, we // need to use BCMath functions to work with arbitrary precision // numbers. // ---------------------------------------------------------------- // If the low order 32 bits are 0, then passwords do not expire in // the domain. Just do '$maxpwdage mod 2^32' and check the result // (2^32 = 4294967296) if (bcmod ($maxpwdage, 4294967296) === '0') { return 0; } // Add up pwdLastSet and maxPwdAge to get password expiration // time, in MS time units. Remember maxPwdAge is stored as a // _negative_ quantity, so we need to substract it in fact. $pwdexpire = bcsub ($pwdlastset, $maxpwdage); // Scale the result to convert it to Unix time units and return // that value. return bcsub( bcdiv($pwdexpire, '10000000'), '11644473600'); } /** * Connect to the LDAP server, using the plugin configured * settings. It's actually a wrapper around ldap_connect_moodle() * * @return resource A valid LDAP connection (or dies if it can't connect) */ function ldap_connect() { // Cache ldap connections. They are expensive to set up // and can drain the TCP/IP ressources on the server if we // are syncing a lot of users (as we try to open a new connection // to get the user details). This is the least invasive way // to reuse existing connections without greater code surgery. if(!empty($this->ldapconnection)) { $this->ldapconns++; return $this->ldapconnection; } if($ldapconnection = ldap_connect_moodle($this->config->host_url, $this->config->ldap_version, $this->config->user_type, $this->config->bind_dn, $this->config->bind_pw, $this->config->opt_deref, $debuginfo, $this->config->start_tls)) { $this->ldapconns = 1; $this->ldapconnection = $ldapconnection; return $ldapconnection; } throw new \moodle_exception('auth_ldap_noconnect_all', 'auth_ldap', '', $debuginfo); } /** * Disconnects from a LDAP server * * @param force boolean Forces closing the real connection to the LDAP server, ignoring any * cached connections. This is needed when we've used paged results * and want to use normal results again. */ function ldap_close($force=false) { $this->ldapconns--; if (($this->ldapconns == 0) || ($force)) { $this->ldapconns = 0; @ldap_close($this->ldapconnection); unset($this->ldapconnection); } } /** * Search specified contexts for username and return the user dn * like: cn=username,ou=suborg,o=org. It's actually a wrapper * around ldap_find_userdn(). * * @param resource $ldapconnection a valid LDAP connection * @param string $extusername the username to search (in external LDAP encoding, no db slashes) * @return mixed the user dn (external LDAP encoding) or false */ function ldap_find_userdn($ldapconnection, $extusername) { $ldap_contexts = explode(';', $this->config->contexts); if (!empty($this->config->create_context)) { array_push($ldap_contexts, $this->config->create_context); } return ldap_find_userdn($ldapconnection, $extusername, $ldap_contexts, $this->config->objectclass, $this->config->user_attribute, $this->config->search_sub); } /** * When using NTLM SSO, the format of the remote username we get in * $_SERVER['REMOTE_USER'] may vary, depending on where from and how the web * server gets the data. So we let the admin configure the format using two * place holders (%domain% and %username%). This function tries to extract * the username (stripping the domain part and any separators if they are * present) from the value present in $_SERVER['REMOTE_USER'], using the * configured format. * * @param string $remoteuser The value from $_SERVER['REMOTE_USER'] (converted to UTF-8) * * @return string The remote username (without domain part or * separators). Empty string if we can't extract the username. */ protected function get_ntlm_remote_user($remoteuser) { if (empty($this->config->ntlmsso_remoteuserformat)) { $format = AUTH_NTLM_DEFAULT_FORMAT; } else { $format = $this->config->ntlmsso_remoteuserformat; } $format = preg_quote($format); $formatregex = preg_replace(array('#%domain%#', '#%username%#'), array('('.AUTH_NTLM_VALID_DOMAINNAME.')', '('.AUTH_NTLM_VALID_USERNAME.')'), $format); if (preg_match('#^'.$formatregex.'$#', $remoteuser, $matches)) { $user = end($matches); return $user; } /* We are unable to extract the username with the configured format. Probably * the format specified is wrong, so log a warning for the admin and return * an empty username. */ error_log($this->errorlogtag.get_string ('auth_ntlmsso_maybeinvalidformat', 'auth_ldap')); return ''; } /** * Check if the diagnostic message for the LDAP login error tells us that the * login is denied because the user password has expired or the password needs * to be changed on first login (using interactive SMB/Windows logins, not * LDAP logins). * * @param string the diagnostic message for the LDAP login error * @return bool true if the password has expired or the password must be changed on first login */ protected function ldap_ad_pwdexpired_from_diagmsg($diagmsg) { // The format of the diagnostic message is (actual examples from W2003 and W2008): // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 52e, vece" (W2003) // "80090308: LdapErr: DSID-0C090334, comment: AcceptSecurityContext error, data 773, vece" (W2003) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 52e, v1771" (W2008) // "80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error, data 773, v1771" (W2008) // We are interested in the 'data nnn' part. // if nnn == 773 then user must change password on first login // if nnn == 532 then user password has expired $diagmsg = explode(',', $diagmsg); if (preg_match('/data (773|532)/i', trim($diagmsg[2]))) { return true; } return false; } /** * Check if a user is suspended. This function is intended to be used after calling * get_userinfo_asobj. This is needed because LDAP doesn't have a notion of disabled * users, however things like MS Active Directory support it and expose information * through a field. * * @param object $user the user object returned by get_userinfo_asobj * @return boolean */ protected function is_user_suspended($user) { if (!$this->config->suspended_attribute || !isset($user->suspended)) { return false; } if ($this->config->suspended_attribute == 'useraccountcontrol' && $this->config->user_type == 'ad') { return (bool)($user->suspended & AUTH_AD_ACCOUNTDISABLE); } return (bool)$user->suspended; } /** * Test a DN * * @param resource $ldapconn * @param string $dn The DN to check for existence * @param string $message The identifier of a string as in get_string() * @param string|object|array $a An object, string or number that can be used * within translation strings as in get_string() * @return true or a message in case of error */ private function test_dn($ldapconn, $dn, $message, $a = null) { $ldapresult = @ldap_read($ldapconn, $dn, '(objectClass=*)', array()); if (!$ldapresult) { if (ldap_errno($ldapconn) == 32) { // No such object. return get_string($message, 'auth_ldap', $a); } $a = array('code' => ldap_errno($ldapconn), 'subject' => $a, 'message' => ldap_error($ldapconn)); return get_string('diag_genericerror', 'auth_ldap', $a); } return true; } /** * Test if settings are correct, print info to output. */ public function test_settings() { global $OUTPUT; if (!function_exists('ldap_connect')) { // Is php-ldap really there? echo $OUTPUT->notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } // Check to see if this is actually configured. if (empty($this->config->host_url)) { // LDAP is not even configured. echo $OUTPUT->notification(get_string('ldapnotconfigured', 'auth_ldap'), \core\output\notification::NOTIFY_ERROR); return; } if ($this->config->ldap_version != 3) { echo $OUTPUT->notification(get_string('diag_toooldversion', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); } try { $ldapconn = $this->ldap_connect(); } catch (Exception $e) { echo $OUTPUT->notification($e->getMessage(), \core\output\notification::NOTIFY_ERROR); return; } // Display paged file results. if (!ldap_paged_results_supported($this->config->ldap_version, $ldapconn)) { echo $OUTPUT->notification(get_string('pagedresultsnotsupp', 'auth_ldap'), \core\output\notification::NOTIFY_INFO); } // Check contexts. foreach (explode(';', $this->config->contexts) as $context) { $context = trim($context); if (empty($context)) { echo $OUTPUT->notification(get_string('diag_emptycontext', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); continue; } $message = $this->test_dn($ldapconn, $context, 'diag_contextnotfound', $context); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } // Create system role mapping field for each assignable system role. $roles = get_ldap_assignable_role_names(); foreach ($roles as $role) { foreach (explode(';', $this->config->{$role['settingname']}) as $groupdn) { if (empty($groupdn)) { continue; } $role['group'] = $groupdn; $message = $this->test_dn($ldapconn, $groupdn, 'diag_rolegroupnotfound', $role); if ($message !== true) { echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } } $this->ldap_close(true); // We were able to connect successfuly. echo $OUTPUT->notification(get_string('connectingldapsuccess', 'auth_ldap'), \core\output\notification::NOTIFY_SUCCESS); } /** * Get the list of profile fields. * * @param bool $fetchall Fetch all, not just those for update. * @return array */ protected function get_profile_keys($fetchall = false) { $keys = array_keys(get_object_vars($this->config)); $updatekeys = []; foreach ($keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { // If we have a field to update it from and it must be updated 'onlogin' we update it on cron. if (!empty($this->config->{'field_map_'.$match[1]})) { if ($fetchall || $this->config->{$match[0]} === 'onlogin') { array_push($updatekeys, $match[1]); // the actual key name } } } } if ($this->config->suspended_attribute && $this->config->sync_suspended) { $updatekeys[] = 'suspended'; } return $updatekeys; } } PK&9\׆ldap/ntlmsso_magic.phpnu[set_context(context_system::instance()); $authsequence = get_enabled_auth_plugins(); // Auths, in sequence. if (!in_array('ldap', $authsequence, true)) { throw new \moodle_exception('ldap_isdisabled', 'auth'); } $authplugin = get_auth_plugin('ldap'); if (empty($authplugin->config->ntlmsso_enabled)) { throw new \moodle_exception('ntlmsso_isdisabled', 'auth_ldap'); } $sesskey = required_param('sesskey', PARAM_RAW); $file = $CFG->dirroot.'/pix/spacer.gif'; if ($authplugin->ntlmsso_magic($sesskey) && file_exists($file)) { if (!empty($authplugin->config->ntlmsso_ie_fastpath)) { if (core_useragent::is_ie()) { redirect($CFG->wwwroot.'/auth/ldap/ntlmsso_finish.php'); } } // Serve GIF // Type header('Content-Type: image/gif'); header('Content-Length: '.filesize($file)); // Output file $handle = fopen($file, 'r'); fpassthru($handle); fclose($handle); exit; } else { throw new \moodle_exception('ntlmsso_iwamagicnotenabled', 'auth_ldap'); } PK&9\"mFFldap/settings.phpnu[. /** * Admin settings and defaults. * * @package auth_ldap * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { if (!function_exists('ldap_connect')) { $notify = new \core\output\notification(get_string('auth_ldap_noextension', 'auth_ldap'), \core\output\notification::NOTIFY_WARNING); $settings->add(new admin_setting_heading('auth_ldap_noextension', '', $OUTPUT->render($notify))); } else { // We use a couple of custom admin settings since we need to massage the data before it is inserted into the DB. require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_lowercase_configtext.php'); require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_contexts_configtext.php'); require_once($CFG->dirroot.'/auth/ldap/classes/admin_setting_special_ntlm_configtext.php'); // We need to use some of the Moodle LDAP constants / functions to create the list of options. require_once($CFG->dirroot.'/auth/ldap/auth.php'); // Introductory explanation. $settings->add(new admin_setting_heading('auth_ldap/pluginname', '', new lang_string('auth_ldapdescription', 'auth_ldap'))); // LDAP server settings. $settings->add(new admin_setting_heading('auth_ldap/ldapserversettings', new lang_string('auth_ldap_server_settings', 'auth_ldap'), '')); // Host. $settings->add(new admin_setting_configtext('auth_ldap/host_url', get_string('auth_ldap_host_url_key', 'auth_ldap'), get_string('auth_ldap_host_url', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Version. $versions = array(); $versions[2] = '2'; $versions[3] = '3'; $settings->add(new admin_setting_configselect('auth_ldap/ldap_version', new lang_string('auth_ldap_version_key', 'auth_ldap'), new lang_string('auth_ldap_version', 'auth_ldap'), 3, $versions)); // Start TLS. $yesno = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_ldap/start_tls', new lang_string('start_tls_key', 'auth_ldap'), new lang_string('start_tls', 'auth_ldap'), 0 , $yesno)); // Encoding. $settings->add(new admin_setting_configtext('auth_ldap/ldapencoding', get_string('auth_ldap_ldap_encoding_key', 'auth_ldap'), get_string('auth_ldap_ldap_encoding', 'auth_ldap'), 'utf-8', PARAM_RAW_TRIMMED)); // Page Size. (Hide if not available). $settings->add(new admin_setting_configtext('auth_ldap/pagesize', get_string('pagesize_key', 'auth_ldap'), get_string('pagesize', 'auth_ldap'), '250', PARAM_INT)); // Bind settings. $settings->add(new admin_setting_heading('auth_ldap/ldapbindsettings', new lang_string('auth_ldap_bind_settings', 'auth_ldap'), '')); // Store Password in DB. $settings->add(new admin_setting_configselect('auth_ldap/preventpassindb', new lang_string('auth_ldap_preventpassindb_key', 'auth_ldap'), new lang_string('auth_ldap_preventpassindb', 'auth_ldap'), 0 , $yesno)); // User ID. $settings->add(new admin_setting_configtext('auth_ldap/bind_dn', get_string('auth_ldap_bind_dn_key', 'auth_ldap'), get_string('auth_ldap_bind_dn', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Password. $settings->add(new admin_setting_configpasswordunmask('auth_ldap/bind_pw', get_string('auth_ldap_bind_pw_key', 'auth_ldap'), get_string('auth_ldap_bind_pw', 'auth_ldap'), '')); // User Lookup settings. $settings->add(new admin_setting_heading('auth_ldap/ldapuserlookup', new lang_string('auth_ldap_user_settings', 'auth_ldap'), '')); // User Type. $settings->add(new admin_setting_configselect('auth_ldap/user_type', new lang_string('auth_ldap_user_type_key', 'auth_ldap'), new lang_string('auth_ldap_user_type', 'auth_ldap'), 'default', ldap_supported_usertypes())); // Contexts. $settings->add(new auth_ldap_admin_setting_special_contexts_configtext('auth_ldap/contexts', get_string('auth_ldap_contexts_key', 'auth_ldap'), get_string('auth_ldap_contexts', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Search subcontexts. $settings->add(new admin_setting_configselect('auth_ldap/search_sub', new lang_string('auth_ldap_search_sub_key', 'auth_ldap'), new lang_string('auth_ldap_search_sub', 'auth_ldap'), 0 , $yesno)); // Dereference aliases. $optderef = array(); $optderef[LDAP_DEREF_NEVER] = get_string('no'); $optderef[LDAP_DEREF_ALWAYS] = get_string('yes'); $settings->add(new admin_setting_configselect('auth_ldap/opt_deref', new lang_string('auth_ldap_opt_deref_key', 'auth_ldap'), new lang_string('auth_ldap_opt_deref', 'auth_ldap'), LDAP_DEREF_NEVER , $optderef)); // User attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/user_attribute', get_string('auth_ldap_user_attribute_key', 'auth_ldap'), get_string('auth_ldap_user_attribute', 'auth_ldap'), '', PARAM_RAW)); // Suspended attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/suspended_attribute', get_string('auth_ldap_suspended_attribute_key', 'auth_ldap'), get_string('auth_ldap_suspended_attribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/memberattribute', get_string('auth_ldap_memberattribute_key', 'auth_ldap'), get_string('auth_ldap_memberattribute', 'auth_ldap'), '', PARAM_RAW)); // Member attribute uses dn. $settings->add(new admin_setting_configselect('auth_ldap/memberattribute_isdn', get_string('auth_ldap_memberattribute_isdn_key', 'auth_ldap'), get_string('auth_ldap_memberattribute_isdn', 'auth_ldap'), 0, $yesno)); // Object class. $settings->add(new admin_setting_configtext('auth_ldap/objectclass', get_string('auth_ldap_objectclass_key', 'auth_ldap'), get_string('auth_ldap_objectclass', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // Force Password change Header. $settings->add(new admin_setting_heading('auth_ldap/ldapforcepasswordchange', new lang_string('forcechangepassword', 'auth'), '')); // Force Password change. $settings->add(new admin_setting_configselect('auth_ldap/forcechangepassword', new lang_string('forcechangepassword', 'auth'), new lang_string('forcechangepasswordfirst_help', 'auth'), 0 , $yesno)); // Standard Password Change. $settings->add(new admin_setting_configselect('auth_ldap/stdchangepassword', new lang_string('stdchangepassword', 'auth'), new lang_string('stdchangepassword_expl', 'auth') .' '. get_string('stdchangepassword_explldap', 'auth'), 0 , $yesno)); // Password Type. $passtype = array(); $passtype['plaintext'] = get_string('plaintext', 'auth'); $passtype['md5'] = get_string('md5', 'auth'); $passtype['sha1'] = get_string('sha1', 'auth'); $settings->add(new admin_setting_configselect('auth_ldap/passtype', new lang_string('auth_ldap_passtype_key', 'auth_ldap'), new lang_string('auth_ldap_passtype', 'auth_ldap'), 'plaintext', $passtype)); // Password change URL. $settings->add(new admin_setting_configtext('auth_ldap/changepasswordurl', get_string('auth_ldap_changepasswordurl_key', 'auth_ldap'), get_string('changepasswordhelp', 'auth'), '', PARAM_URL)); // Password Expiration Header. $settings->add(new admin_setting_heading('auth_ldap/passwordexpire', new lang_string('auth_ldap_passwdexpire_settings', 'auth_ldap'), '')); // Password Expiration. // Create the description lang_string object. $strno = get_string('no'); $strldapserver = get_string('pluginname', 'auth_ldap'); $langobject = new stdClass(); $langobject->no = $strno; $langobject->ldapserver = $strldapserver; $description = new lang_string('auth_ldap_expiration_desc', 'auth_ldap', $langobject); // Now create the options. $expiration = array(); $expiration['0'] = $strno; $expiration['1'] = $strldapserver; // Add the setting. $settings->add(new admin_setting_configselect('auth_ldap/expiration', new lang_string('auth_ldap_expiration_key', 'auth_ldap'), $description, 0 , $expiration)); // Password Expiration warning. $settings->add(new admin_setting_configtext('auth_ldap/expiration_warning', get_string('auth_ldap_expiration_warning_key', 'auth_ldap'), get_string('auth_ldap_expiration_warning_desc', 'auth_ldap'), '', PARAM_RAW)); // Password Expiration attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/expireattr', get_string('auth_ldap_expireattr_key', 'auth_ldap'), get_string('auth_ldap_expireattr_desc', 'auth_ldap'), '', PARAM_RAW)); // Grace Logins. $settings->add(new admin_setting_configselect('auth_ldap/gracelogins', new lang_string('auth_ldap_gracelogins_key', 'auth_ldap'), new lang_string('auth_ldap_gracelogins_desc', 'auth_ldap'), 0 , $yesno)); // Grace logins attribute. $settings->add(new auth_ldap_admin_setting_special_lowercase_configtext('auth_ldap/graceattr', get_string('auth_ldap_gracelogin_key', 'auth_ldap'), get_string('auth_ldap_graceattr_desc', 'auth_ldap'), '', PARAM_RAW)); // User Creation. $settings->add(new admin_setting_heading('auth_ldap/usercreation', new lang_string('auth_user_create', 'auth'), '')); // Create users externally. $settings->add(new admin_setting_configselect('auth_ldap/auth_user_create', new lang_string('auth_ldap_auth_user_create_key', 'auth_ldap'), new lang_string('auth_user_creation', 'auth'), 0 , $yesno)); // Context for new users. $settings->add(new admin_setting_configtext('auth_ldap/create_context', get_string('auth_ldap_create_context_key', 'auth_ldap'), get_string('auth_ldap_create_context', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // System roles mapping header. $settings->add(new admin_setting_heading('auth_ldap/systemrolemapping', new lang_string('systemrolemapping', 'auth_ldap'), '')); // Create system role mapping field for each assignable system role. $roles = get_ldap_assignable_role_names(); foreach ($roles as $role) { // Before we can add this setting we need to check a few things. // A) It does not exceed 100 characters otherwise it will break the DB as the 'name' field // in the 'config_plugins' table is a varchar(100). // B) The setting name does not contain hyphens. If it does then it will fail the check // in parse_setting_name() and everything will explode. Role short names are validated // against PARAM_ALPHANUMEXT which is similar to the regex used in parse_setting_name() // except it also allows hyphens. // Instead of shortening the name and removing/replacing the hyphens we are showing a warning. // If we were to manipulate the setting name by removing the hyphens we may get conflicts, eg // 'thisisashortname' and 'this-is-a-short-name'. The same applies for shortening the setting name. if (core_text::strlen($role['settingname']) > 100 || !preg_match('/^[a-zA-Z0-9_]+$/', $role['settingname'])) { $url = new moodle_url('/admin/roles/define.php', array('action' => 'edit', 'roleid' => $role['id'])); $a = (object)['rolename' => $role['localname'], 'shortname' => $role['shortname'], 'charlimit' => 93, 'link' => $url->out()]; $settings->add(new admin_setting_heading('auth_ldap/role_not_mapped_' . sha1($role['settingname']), '', get_string('cannotmaprole', 'auth_ldap', $a))); } else { $settings->add(new admin_setting_configtext('auth_ldap/' . $role['settingname'], get_string('auth_ldap_rolecontext', 'auth_ldap', $role), get_string('auth_ldap_rolecontext_help', 'auth_ldap', $role), '', PARAM_RAW_TRIMMED)); } } // User Account Sync. $settings->add(new admin_setting_heading('auth_ldap/syncusers', new lang_string('auth_sync_script', 'auth'), '')); // Remove external user. $deleteopt = array(); $deleteopt[AUTH_REMOVEUSER_KEEP] = get_string('auth_remove_keep', 'auth'); $deleteopt[AUTH_REMOVEUSER_SUSPEND] = get_string('auth_remove_suspend', 'auth'); $deleteopt[AUTH_REMOVEUSER_FULLDELETE] = get_string('auth_remove_delete', 'auth'); $settings->add(new admin_setting_configselect('auth_ldap/removeuser', new lang_string('auth_remove_user_key', 'auth'), new lang_string('auth_remove_user', 'auth'), AUTH_REMOVEUSER_KEEP, $deleteopt)); // Sync Suspension. $settings->add(new admin_setting_configselect('auth_ldap/sync_suspended', new lang_string('auth_sync_suspended_key', 'auth'), new lang_string('auth_sync_suspended', 'auth'), 0 , $yesno)); // Sync update users chunk size. $settings->add(new admin_setting_configtext('auth_ldap/sync_updateuserchunk', new lang_string('sync_updateuserchunk_key', 'auth_ldap'), new lang_string('sync_updateuserchunk', 'auth_ldap'), 1000, PARAM_INT)); // NTLM SSO Header. $settings->add(new admin_setting_heading('auth_ldap/ntlm', new lang_string('auth_ntlmsso', 'auth_ldap'), '')); // Enable NTLM. $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_enabled', new lang_string('auth_ntlmsso_enabled_key', 'auth_ldap'), new lang_string('auth_ntlmsso_enabled', 'auth_ldap'), 0 , $yesno)); // Subnet. $settings->add(new admin_setting_configtext('auth_ldap/ntlmsso_subnet', get_string('auth_ntlmsso_subnet_key', 'auth_ldap'), get_string('auth_ntlmsso_subnet', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); // NTLM Fast Path. $fastpathoptions = array(); $fastpathoptions[AUTH_NTLM_FASTPATH_YESFORM] = get_string('auth_ntlmsso_ie_fastpath_yesform', 'auth_ldap'); $fastpathoptions[AUTH_NTLM_FASTPATH_YESATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_yesattempt', 'auth_ldap'); $fastpathoptions[AUTH_NTLM_FASTPATH_ATTEMPT] = get_string('auth_ntlmsso_ie_fastpath_attempt', 'auth_ldap'); $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_ie_fastpath', new lang_string('auth_ntlmsso_ie_fastpath_key', 'auth_ldap'), new lang_string('auth_ntlmsso_ie_fastpath', 'auth_ldap'), AUTH_NTLM_FASTPATH_ATTEMPT, $fastpathoptions)); // Authentication type. $types = array(); $types['ntlm'] = 'NTLM'; $types['kerberos'] = 'Kerberos'; $settings->add(new admin_setting_configselect('auth_ldap/ntlmsso_type', new lang_string('auth_ntlmsso_type_key', 'auth_ldap'), new lang_string('auth_ntlmsso_type', 'auth_ldap'), 'ntlm', $types)); // Remote Username format. $settings->add(new auth_ldap_admin_setting_special_ntlm_configtext('auth_ldap/ntlmsso_remoteuserformat', get_string('auth_ntlmsso_remoteuserformat_key', 'auth_ldap'), get_string('auth_ntlmsso_remoteuserformat', 'auth_ldap'), '', PARAM_RAW_TRIMMED)); } // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('ldap'); $help = get_string('auth_ldapextrafields', 'auth_ldap'); $help .= get_string('auth_updatelocal_expl', 'auth'); $help .= get_string('auth_fieldlock_expl', 'auth'); $help .= get_string('auth_updateremote_expl', 'auth'); $help .= '
'; $help .= get_string('auth_updateremote_ldap', 'auth'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, $help, true, true, $authplugin->get_custom_user_profile_fields()); } PK&9\wF%% tests/external/external_test.phpnu[. /** * Auth external functions tests. * * @package core_auth * @category external * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ namespace core_auth\external; use auth_email_external; use core_auth_external; use core_external\external_api; use externallib_advanced_testcase; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); /** * External auth API tests. * * @package core_auth * @copyright 2016 Juan Leyva * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.2 */ final class external_test extends externallib_advanced_testcase { /** @var string Original error log */ protected $oldlog; /** * Set up for every test */ public function setUp(): void { global $CFG; parent::setUp(); $this->resetAfterTest(true); $CFG->registerauth = 'email'; // Discard error logs. $this->oldlog = ini_get('error_log'); ini_set('error_log', "$CFG->dataroot/testlog.log"); } /** * Tear down to restore old logging.. */ protected function tearDown(): void { ini_set('error_log', $this->oldlog); parent::tearDown(); } /** * Test confirm_user */ public function test_confirm_user(): void { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $secret = $DB->get_field('user', 'secret', array('username' => $username)); // Confirm the user. $result = core_auth_external::confirm_user($username, $secret); $result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $confirmed = $DB->get_field('user', 'confirmed', array('username' => $username)); $this->assertEquals(1, $confirmed); // Try to confirm the user again. $result = core_auth_external::confirm_user($username, $secret); $result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertFalse($result['success']); $this->assertCount(1, $result['warnings']); $this->assertEquals('alreadyconfirmed', $result['warnings'][0]['warningcode']); // Try to use an invalid secret. $this->expectException('\moodle_exception'); $this->expectExceptionMessage(get_string('invalidconfirmdata', 'error')); $result = core_auth_external::confirm_user($username, 'zzZZzz'); } /** * Test age digital consent not enabled. */ public function test_age_digital_consent_verification_is_not_enabled(): void { global $CFG; $CFG->agedigitalconsentverification = 0; $result = core_auth_external::is_age_digital_consent_verification_enabled(); $result = external_api::clean_returnvalue( core_auth_external::is_age_digital_consent_verification_enabled_returns(), $result); $this->assertFalse($result['status']); } /** * Test age digital consent is enabled. */ public function test_age_digital_consent_verification_is_enabled(): void { global $CFG; $CFG->agedigitalconsentverification = 1; $result = core_auth_external::is_age_digital_consent_verification_enabled(); $result = external_api::clean_returnvalue( core_auth_external::is_age_digital_consent_verification_enabled_returns(), $result); $this->assertTrue($result['status']); } /** * Test resend_confirmation_email. */ public function test_resend_confirmation_email(): void { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $result = core_auth_external::resend_confirmation_email($username, $password); $result = external_api::clean_returnvalue(core_auth_external::resend_confirmation_email_returns(), $result); $this->assertTrue($result['status']); $this->assertEmpty($result['warnings']); $confirmed = $DB->get_field('user', 'confirmed', array('username' => $username)); $this->assertEquals(0, $confirmed); } /** * Test resend_confirmation_email invalid username. */ public function test_resend_confirmation_email_invalid_username(): void { $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); set_config('protectusernames', 0); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); $result = core_auth_external::resend_confirmation_email('abc', $password); } /** * Test resend_confirmation_email invalid password. */ public function test_resend_confirmation_email_invalid_password(): void { $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); set_config('protectusernames', 0); $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); $result = core_auth_external::resend_confirmation_email($username, 'abc'); } /** * Test resend_confirmation_email already confirmed user. */ public function test_resend_confirmation_email_already_confirmed_user(): void { global $DB; $username = 'pepe'; $password = 'abcdefAª.ªª!!3'; $firstname = 'Pepe'; $lastname = 'Pérez'; $email = 'myemail@no.zbc'; // Create new user. $result = auth_email_external::signup_user($username, $password, $firstname, $lastname, $email); $result = external_api::clean_returnvalue(auth_email_external::signup_user_returns(), $result); $this->assertTrue($result['success']); $this->assertEmpty($result['warnings']); $secret = $DB->get_field('user', 'secret', array('username' => $username)); // Confirm the user. $result = core_auth_external::confirm_user($username, $secret); $result = external_api::clean_returnvalue(core_auth_external::confirm_user_returns(), $result); $this->assertTrue($result['success']); // Keep protectusernames enabled so the call returns invalidlogin exception. $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/invalidlogin'); core_auth_external::resend_confirmation_email($username, $password); // Now disable protectusernames and expect an exception. set_config('protectusernames', 0); $this->expectException('\moodle_exception'); $this->expectExceptionMessage('error/alreadyconfirmed'); core_auth_external::resend_confirmation_email($username, $password); } } PK&9\Њrtests/privacy/provider_test.phpnu[. /** * Data provider tests. * * @package core_auth * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_auth\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; use core_privacy\tests\provider_testcase; use core_privacy\local\request\transform; use core_privacy\local\request\writer; use core_auth\privacy\provider; /** * Data provider testcase class. * * @package core_auth * @category test * @copyright 2018 Frédéric Massart * @author Frédéric Massart * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends provider_testcase { public function setUp(): void { parent::setUp(); $this->resetAfterTest(); } public function test_export_user_preferences(): void { $dg = $this->getDataGenerator(); $u1 = $dg->create_user(); $u2 = $dg->create_user(); $sysctx = \context_system::instance(); $now = time(); // Check nothing is there. writer::reset(); provider::export_user_preferences($u1->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEmpty((array) $prefs); // Set some preferences. set_user_preference('auth_forcepasswordchange', 1, $u1); set_user_preference('create_password', 1, $u1); set_user_preference('login_failed_count', 18, $u1); set_user_preference('login_failed_count_since_success', 7, $u1); set_user_preference('login_failed_last', $now - DAYSECS, $u1); set_user_preference('login_lockout', $now - HOURSECS, $u1); set_user_preference('login_lockout_ignored', 0, $u1); set_user_preference('login_lockout_secret', 'Hello world!', $u1); set_user_preference('auth_forcepasswordchange', 0, $u2); set_user_preference('create_password', 0, $u2); set_user_preference('login_lockout_ignored', 1, $u2); // Check user 1. writer::reset(); provider::export_user_preferences($u1->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEquals(transform::yesno(true), $prefs->auth_forcepasswordchange->value); $this->assertEquals(transform::yesno(true), $prefs->create_password->value); $this->assertEquals(18, $prefs->login_failed_count->value); $this->assertEquals(7, $prefs->login_failed_count_since_success->value); $this->assertEquals(transform::datetime($now - DAYSECS), $prefs->login_failed_last->value); $this->assertEquals(transform::datetime($now - HOURSECS), $prefs->login_lockout->value); $this->assertEquals(transform::yesno(false), $prefs->login_lockout_ignored->value); $this->assertEquals('Hello world!', $prefs->login_lockout_secret->value); // Check user 2. writer::reset(); provider::export_user_preferences($u2->id); $prefs = writer::with_context($sysctx)->get_user_preferences('core_auth'); $this->assertEquals(transform::yesno(false), $prefs->auth_forcepasswordchange->value); $this->assertEquals(transform::yesno(false), $prefs->create_password->value); $this->assertObjectNotHasProperty('login_failed_count', $prefs); $this->assertObjectNotHasProperty('login_failed_count_since_success', $prefs); $this->assertObjectNotHasProperty('login_failed_last', $prefs); $this->assertObjectNotHasProperty('login_lockout', $prefs); $this->assertEquals(transform::yesno(true), $prefs->login_lockout_ignored->value); $this->assertObjectNotHasProperty('login_lockout_secret', $prefs); } } PK&9\!9G tests/behat/login.phpnu[. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @author Guy Thomas * @copyright 2021 Class Technologies Inc. {@link https://www.class.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); require_once("{$CFG->dirroot}/login/lib.php"); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/')); } $username = required_param('username', PARAM_ALPHANUMEXT); $wantsurl = optional_param('wantsurl', null, PARAM_URL); if (isloggedin()) { // If the user is already logged in, log them out and redirect them back to login again. require_logout(); redirect(new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, 'wantsurl' => (new moodle_url($wantsurl))->out(false), ])); } // Note - with behat, the password is always the same as the username. $password = $username; $failurereason = null; $user = authenticate_user_login($username, $password, true, $failurereason, false); if ($failurereason) { switch($failurereason) { case AUTH_LOGIN_NOUSER: $reason = get_string('invalidlogin'); break; case AUTH_LOGIN_SUSPENDED: $reason = 'User suspended'; break; case AUTH_LOGIN_FAILED: $reason = 'Login failed'; break; case AUTH_LOGIN_LOCKOUT: $reason = 'Account locked'; break; case AUTH_LOGIN_UNAUTHORISED: $reason = get_string('unauthorisedlogin', 'core', $username); break; default: $reason = "Unknown login failure: '{$failurereason}'"; break; } // Note: Do not throw an exception here as we sometimes test that login does not work. // Exceptions are automatic failures in Behat. \core\notification::add($reason, \core\notification::ERROR); redirect(new moodle_url('/')); } if (!complete_user_login($user)) { throw new Exception("Failed to login as behat step for $username"); } if (empty($wantsurl)) { $wantsurl = core_login_get_return_url(); } redirect(new moodle_url($wantsurl)); PK&9\?tests/behat/loginform.featurenu[@auth @core_auth @javascript Feature: Test if the login form provides the correct feedback In order to check if the login form provides correct feedback As a user I need to go on login page and see feedback on incorrect username or password. Background: Given the following "users" exist: | username | | teacher1 | Scenario: Check invalid login message Given I follow "Log in" And I set the field "Username" to "teacher1" And I set the field "Password" to "incorrect" When I press "Log in" Then I should see "Invalid login, please try again" Scenario: Test login language selector Given remote langimport tests are enabled And the following "language packs" exist: | language | | nl | | es | And the following config values are set as admin: | langmenu | 1 | And I follow "Log in" And I open the action menu in "region-main" "region" # The line below contains the unicode character U+200E before and after the brackets, please be very careful editing this line. When I choose "Nederlands ‎(nl)‎" in the open action menu Then I should see "Gebruikersnaam" @_file_upload Scenario: Set logo for loginpage Given I log in as "admin" And I navigate to "Appearance > Logos" in site administration And I upload "course/tests/fixtures/image.jpg" file to "Logo" filemanager And I press "Save changes" And I log out And I follow "Log in" Then "//img[@id='logoimage']" "xpath_element" should exist Scenario: Add a custom welcome message Given the following config values are set as admin: | auth_instructions | Lorem ipsum dolor sit amet | And I follow "Log in" Then I should see "Lorem ipsum dolor sit amet" @javascript @accessibility Scenario: Show the maintenance mode message Given the following config values are set as admin: | maintenance_enabled | 1 | | maintenance_message | Back online tomorrow | And I follow "Log in" Then I should see "Back online tomorrow" And the page should meet accessibility standards with "best-practice" extra tests Scenario: User self registration Given the following config values are set as admin: | registerauth | Email-based self-registration | And I follow "Log in" Then I should see "Create new account" Scenario: Set OAuth providers Given I log in as "admin" And I navigate to "Plugins > Authentication > Manage authentication" in site administration And I click on "Enable" "link" in the "OAuth 2" "table_row" And I navigate to "Server > OAuth 2 services" in site administration And I press "Google" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Facebook" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I press "Microsoft" And I set the field "Client ID" to "1234" And I set the field "Client secret" to "1234" And I press "Save changes" And I log out And I follow "Log in" Then I should see "Google" And I should see "Facebook" And I should see "Microsoft" Scenario: Test the login page auto focus feature Given the following config values are set as admin: | loginpageautofocus | Enabled | And I follow "Log in" Then the focused element is "Username" "field" And I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" And I log out And I follow "Log in" Then the focused element is "Password" "field" @accessibility Scenario: Test the login page focus after error feature Given I follow "Log in" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpassword" And I press "Log in" And I wait until the page is ready Then the focused element is "Username" "field" And the page should meet accessibility standards with "best-practice" extra tests Scenario: Display the password visibility toggle icon Given the following config values are set as admin: | loginpasswordtoggle | 1 | When I follow "Log in" Then "Toggle sensitive" "button" should be visible And the following config values are set as admin: | loginpasswordtoggle | 0 | And I reload the page And "Toggle sensitive" "button" should not be visible Scenario: Display the password visibility toggle icon for small screens only Given the following config values are set as admin: | loginpasswordtoggle | 2 | When I follow "Log in" Then "Toggle sensitive" "button" should not be visible And I change the viewport size to "mobile" And "Toggle sensitive" "button" should be visible PK&9\ߗw? ? $tests/behat/rememberusername.featurenu[@core @core_auth Feature: Test the 'remember username' feature works. In order for users to easily log in to the site As a user I need the site to remember my username when the feature is enabled Background: Given the following "users" exist: | username | | teacher1 | # Given the user has logged in and selected 'Remember username', when they log in again, then their username should be remembered. Scenario: Check that 'remember username' works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Yes. Given the following config values are set as admin: | rememberusername | 1 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log out and check that the username was remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" # Given the user has logged in before and selected 'Remember username', when they log in again and unset 'Remember username', then # their username should be forgotten for future log in attempts. Scenario: Check that 'remember username' unsetting works without javascript for teachers. # Log in the first time with $CFG->rememberusername set to Optional. Given the following config values are set as admin: | rememberusername | 2 | And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out # Log in again, the username should have been remembered. When I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" And I log out And the following config values are set as admin: | rememberusername | 0 | # Check username has been forgotten. And I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" Then the field "username" matches value "" PK&9\MNySStests/behat/logout.phpnu[. // phpcs:disable moodle.Files.RequireLogin.Missing // phpcs:disable moodle.PHP.ForbiddenFunctions.Found /** * Login end point for Behat tests only. * * @package core_auth * @category test * @copyright Andrew Lyons * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../../config.php'); $behatrunning = defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING; if (!$behatrunning) { redirect(new moodle_url('/login/logout.php')); } require_logout(); $login = optional_param('loginpage', 0, PARAM_BOOL); if ($login) { redirect(get_login_url()); } else { redirect(new moodle_url('/')); } PK&9\W tests/behat/behat_auth.phpnu[. /** * Basic authentication steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. require_once(__DIR__ . '/../../../lib/behat/behat_base.php'); /** * Log in log out steps definitions. * * @package core_auth * @category test * @copyright 2012 David Monllaó * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_auth extends behat_base { /** * Logs in the user. There should exist a user with the same value as username and password. * * @Given /^I log in as "(?P(?:[^"]|\\")*)"$/ * @Given I am logged in as :username * @param string $username the user to log in as. * @param moodle_url|null $wantsurl optional, URL to go to after logging in. */ public function i_log_in_as(string $username, ?moodle_url $wantsurl = null) { // In the mobile app the required tasks are different (does not support $wantsurl). if ($this->is_in_app()) { $this->execute('behat_app::login', [$username]); return; } $loginurl = new moodle_url('/auth/tests/behat/login.php', [ 'username' => $username, ]); if ($wantsurl !== null) { $loginurl->param('wantsurl', $wantsurl->out_as_local_url()); } // Visit login page. $this->execute('behat_general::i_visit', [$loginurl]); } /** * Logs out of the system. * * @Given /^I log out$/ * @Given I am not logged in */ public function i_log_out() { $this->execute('behat_general::i_visit', [new moodle_url('/auth/tests/behat/logout.php')]); } } PK&9\ tests/behat/login.featurenu[@core @core_auth Feature: Authentication In order to validate my credentials in the system As a user I need to log into the system Scenario: Log in with the predefined admin user with Javascript disabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" @javascript Scenario: Log in with the predefined admin user with Javascript enabled Given I log in as "admin" Then I should see "You are logged in as Admin User" in the "page-footer" "region" Scenario: Log in as an existing admin user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "testuser" And I press "Log in" Then I should see "You are logged in as" in the "page-footer" "region" Scenario: Log in as an unexisting user filling the form Given the following "users" exist: | username | password | firstname | lastname | email | | testuser | testuser | Test | User | moodle@example.com | And I am on site homepage When I follow "Log in" And I set the field "Username" to "testuser" And I set the field "Password" to "unexisting" And I press "Log in" Then I should see "Invalid login, please try again" Scenario: Log out using the Log out link Given I log in as "admin" When I click on "Log out" "link" in the "#page-footer" "css_element" Then I should see "You are not logged in" in the "page-footer" "region" @javascript @accessibility Scenario: Login page must be accessible When I am on site homepage # The following tests are all provided to ensure that the accessibility tests themselves are tested. # In normal tests only one of the following is required. Then the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests And I follow "Log in" And the page should meet accessibility standards And the page should meet "wcag131, wcag141, wcag412" accessibility standards And the page should meet accessibility standards with "wcag131, wcag141, wcag412" extra tests @javascript @accessibility Scenario: The login page must meet accessibility standards Given the following config values are set as admin: | custommenuitems | -This is a custom item\|/customurl/ | When I am on site homepage Then the page should meet accessibility standards with "best-practice" extra tests And I follow "Log in" And the page should meet accessibility standards with "best-practice" extra tests Scenario: Alternate login URL can be bypassed Given the following config values are set as admin: | alternateloginurl | https://www.google.com/ | And I am on site homepage When I visit "/login/index.php?loginredirect=0" Then I should see "Log in to Acceptance test site" PK&9\kǩ&tests/behat/verifyageofconsent.featurenu[@core @verify_age_location Feature: Test the 'Digital age of consent verification' feature works. In order to self-register on the site As an user I need be to be over the age of digital consent Background: Given the following config values are set as admin: | registerauth | email | | agedigitalconsentverification | 1 | Scenario: User that is not considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "16" And I set the field "In which country do you live?" to "DZ" And I press "Proceed" Then I should see "New account" And I should see "Username" # Try to access the sign up page again. When I press "Cancel" And I click on "Create new account" "link" Then I should see "New account" And I should see "Username" Scenario: User that is considered a digital minor attempts to self-register on the site. # Try to access the sign up page. Given I am on homepage When I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "Age and location verification" When I set the field "What is your age?" to "12" And I set the field "In which country do you live?" to "AT" And I press "Proceed" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" # Try to access the sign up page again. When I click on "Back to the site home" "link" And I click on "Log in" "link" in the ".logininfo" "css_element" And I click on "Create new account" "link" Then I should see "You are too young to create an account on this site." And I should see "Please ask your parent/guardian to contact:" PK&9\Z (tests/behat/displayloginfailures.featurenu[@core @core_auth Feature: Test the 'showlogfailures' feature works. In order to see my recent login failures when logging in As a user I need to have at least one failed login attempt and then log in Background: Given the following "users" exist: | username | | teacher1 | And the following config values are set as admin: | displayloginfailures | 1 | # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for teachers. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "teacher1" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login" in the "page-footer" "region" # Confirm the notices disappear when navigating to another page. And I am on homepage And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login" in the "page-footer" "region" # Given the user has at least one failed login attempt, when they login, then they should see both header and footer notices. Scenario: Check that 'displayloginfailures' works without javascript for admins. # Simulate a log in failure for the teacher. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" And I set the field "Username" to "admin" And I set the field "Password" to "wrongpass" And I press "Log in" And I should see "Invalid login, please try again" # Now, log in with the correct credentials. When I set the field "Username" to "admin" And I set the field "Password" to "admin" And I press "Log in" # Confirm the notices are displayed. Then I should see "1 failed logins since your last login" in the ".navbar" "css_element" And I should see "1 failed logins since your last login (Logs)" in the "page-footer" "region" # Confirm that the link works and that the notices disappear when navigating to another page. And I click on "Logs" "link" in the "page-footer" "region" And I should see "User login failed" in the "table.reportlog" "css_element" And I should not see "1 failed logins since your last login" in the ".navbar" "css_element" And I should not see "1 failed logins since your last login (Logs)" in the "page-footer" "region" PK&9\/a  0tests/behat/validateagedigitalconsentmap.featurenu[@core @verify_age_location Feature: Test validation of 'Age of digital consent' setting. In order to set the 'Age of digital consent' setting As an admin I need to provide valid data and valid format Background: Given I log in as "admin" And I navigate to "Users > Privacy and policies > Privacy settings" in site administration Scenario: Admin provides valid value for 'Age of digital consent'. Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, 14 BE, 14 """ When I press "Save changes" Then I should see "Changes saved" And I should not see "Some settings were not changed due to an error." And I should not see "The digital age of consent is not valid:" Scenario: Admin provides invalid format for 'Age of digital consent'. # Try to set a value with missing space separator Given I set the field "s__agedigitalconsentmap" to multiline: """ *16 AT, 14 BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"*16\" has more or less than one comma separator." # Try to set a value with missing default age of consent When I set the field "s__agedigitalconsentmap" to multiline: """ AT, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: Default (*) value is missing." Scenario: Admin provides invalid age of consent or country for 'Age of digital consent'. # Try to set a value containing invalid age of consent Given I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 AT, age BE, 14 """ When I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"age\" is not a valid value for age." # Try to set a value containing invalid country When I set the field "s__agedigitalconsentmap" to multiline: """ *, 16 COUNTRY, 14 BE, 14 """ And I press "Save changes" Then I should not see "Changes saved" And I should see "Some settings were not changed due to an error." And I should see "The digital age of consent is not valid: \"COUNTRY\" is not a valid value for country." PK&9\+utests/digital_consent_test.phpnu[. namespace core_auth; /** * Digital consent helper testcase. * * @package core_auth * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class digital_consent_test extends \advanced_testcase { public function test_is_age_digital_consent_verification_enabled(): void { global $CFG; $this->resetAfterTest(); // Age of digital consent verification is enabled. $CFG->agedigitalconsentverification = 0; $isenabled = \core_auth\digital_consent::is_age_digital_consent_verification_enabled(); $this->assertFalse($isenabled); } public function test_is_minor(): void { global $CFG; $this->resetAfterTest(); $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'CZ, 13', 'DE, 14', 'DK, 13', ]); $CFG->agedigitalconsentmap = $agedigitalconsentmap; $usercountry1 = 'DK'; $usercountry2 = 'AU'; $userage1 = 12; $userage2 = 14; $userage3 = 16; // Test country exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage1, $usercountry1); $this->assertTrue($isminor); // Test country exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry1); $this->assertFalse($isminor); // Test country does not exists in agedigitalconsentmap and user age is below the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage2, $usercountry2); $this->assertTrue($isminor); // Test country does not exists in agedigitalconsentmap and user age is above the particular digital minor age. $isminor = \core_auth\digital_consent::is_minor($userage3, $usercountry2); $this->assertFalse($isminor); } public function test_parse_age_digital_consent_map_valid_format(): void { // Value of agedigitalconsentmap has a valid format. $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, 14', 'BE, 13' ]); $ageconsentmapparsed = \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); $this->assertEquals([ '*' => 16, 'AT' => 14, 'BE' => 13 ], $ageconsentmapparsed ); } public function test_parse_age_digital_consent_map_invalid_format_missing_spaces(): void { // Value of agedigitalconsentmap has an invalid format (missing space separator between values). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT14', ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcomma', 'error', 'AT14')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_default_value(): void { // Value of agedigitalconsentmap has an invalid format (missing default value). $agedigitalconsentmap = implode(PHP_EOL, [ 'BE, 16', 'AT, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvaliddefault', 'error')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_country(): void { // Value of agedigitalconsentmap has an invalid format (invalid value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'TEST, 14' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', 'TEST')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_invalid_age_string(): void { // Value of agedigitalconsentmap has an invalid format (string value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ten' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', 'ten')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_age(): void { // Value of agedigitalconsentmap has an invalid format (missing value for age). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', 'AT, ' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidage', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } public function test_parse_age_digital_consent_map_invalid_format_missing_country(): void { // Value of agedigitalconsentmap has an invalid format (missing value for country). $agedigitalconsentmap = implode(PHP_EOL, [ '*, 16', ', 12' ]); $this->expectException('moodle_exception'); $this->expectExceptionMessage(get_string('agedigitalconsentmapinvalidcountry', 'error', '')); \core_auth\digital_consent::parse_age_digital_consent_map($agedigitalconsentmap); } } PK&9\\\none/db/upgrade.phpnu[. /** * No authentication plugin upgrade code * * @package auth_none * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_none. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_none_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; } PK&9\ucnone/lang/en/auth_none.phpnu[. /** * Strings for component 'auth_none', language 'en'. * * @package auth_none * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_nonedescription'] = 'Users can sign in and create valid accounts immediately, with no authentication against an external server and no confirmation via email. Be careful using this option - think of the security and administration problems this could cause.'; $string['pluginname'] = 'No authentication'; $string['privacy:metadata'] = 'The No authentication plugin does not store any personal data.'; $string['checknoauthdetails'] = '

The No authentication plugin is not intended for production sites. Please disable it unless this is a development test site.

'; $string['checknoautherror'] = 'The No authentication plugin cannot be used on production sites.'; $string['checknoauth'] = 'No authentication'; $string['checknoauthok'] = 'The no authentication plugin is disabled.'; PK&9\tww none/lib.phpnu[. /** * Anybody can login with any password. * * @package auth_none * @category check * @copyright 2020 Brendan Heywood * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Add security check to make sure this isn't on in production. * * @return array check */ function auth_none_security_checks() { return [new auth_none\check\noauth()]; } PK&9\Vnone/version.phpnu[. /** * Version information * * @package auth_none * @copyright 2011 Petr Skoda (http://skodak.org) * @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_none'; // Full name of the plugin (used for diagnostics) PK&9\ !none/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_none. * * @package auth_none * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_none\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for auth_none implementing null_provider. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK&9\ǯ))none/classes/check/noauth.phpnu[. /** * Verifies unsupported noauth setting * * @package auth_none * @copyright 2020 Brendan Heywood * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_none\check; defined('MOODLE_INTERNAL') || die(); use core\check\result; /** * Verifies unsupported noauth setting * * @copyright 2020 Brendan Heywood * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class noauth extends \core\check\check { /** * A link to a place to action this * * @return action_link */ public function get_action_link(): ?\action_link { return new \action_link( new \moodle_url('/admin/settings.php?section=manageauths'), get_string('authsettings', 'admin')); } /** * Return result * @return result */ public function get_result(): result { if (is_enabled_auth('none')) { $status = result::ERROR; $summary = get_string('checknoautherror', 'auth_none'); } else { $status = result::OK; $summary = get_string('checknoauthok', 'auth_none'); } $details = get_string('checknoauthdetails', 'auth_none'); return new result($status, $summary, $details); } } PK&9\7none/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/none/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/none' to 'auth_none'. PK&9\{dd none/auth.phpnu[. /** * Anobody can login with any password. * * @package auth_none * @author Martin Dougiamas * @license http://www.gnu.org/copyleft/gpl.html GNU Public License */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Plugin for no authentication. */ class auth_plugin_none extends auth_plugin_base { /** * Constructor. */ public function __construct() { $this->authtype = 'none'; $this->config = get_config('auth_none'); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_none() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * 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. */ function user_login($username, $password) { global $CFG, $DB; if ($user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return validate_internal_user_password($user, $password); } return true; } /** * Updates the user's password. * * called when the user password is updated. * * @param object $user User table object * @param string $newpassword Plaintext password * @return boolean result * */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } } PK&9\O`lEEnone/settings.phpnu[. /** * Admin settings and defaults. * * @package auth_none * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_none/pluginname', '', new lang_string('auth_nonedescription', 'auth_none'))); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('none'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } PK&9\*Luumanual/db/upgrade.phpnu[. /** * Manual authentication plugin upgrade code * * @package auth_manual * @copyright 2011 Petr Skoda (http://skodak.org) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade auth_manual. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_auth_manual_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; } PK&9\*Rmanual/lang/en/auth_manual.phpnu[. /** * Strings for component 'auth_manual', language 'en'. * * @package auth_manual * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['auth_manualdescription'] = 'This method removes any way for users to create their own accounts. All accounts must be manually created by the admin user.'; $string['expiration'] = 'Enable password expiry'; $string['expiration_desc'] = 'Allow passwords to expire after a specified time.'; $string['expiration_warning'] = 'Notification threshold'; $string['expiration_warning_desc'] = 'Number of days before password expiry that a notification is issued.'; $string['passwdexpiretime'] = 'Password duration'; $string['passwdexpiretime_desc'] = 'Length of time for which a password is valid.'; $string['pluginname'] = 'Manual accounts'; $string['passwdexpire_settings'] = 'Password expiry settings'; $string['privacy:metadata:preference:passwordupdatetime'] = 'The date of the last password change.'; PK&9\Lmanual/version.phpnu[. /** * Manual authentication plugin version information * * @package auth_manual * @copyright 2011 Petr Skoda (http://skodak.org) * @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_manual'; // Full name of the plugin (used for diagnostics) PK&9\Dk k #manual/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for auth_manual. * * @package auth_manual * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_manual\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\request\writer; use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; /** * Privacy provider for the authentication manual. * * @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\user_preference_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // There is a one user preference. $collection->add_user_preference('auth_manual_passwordupdatetime', 'privacy:metadata:preference:passwordupdatetime'); return $collection; } /** * Export all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', null, $userid); if ($lastpasswordupdatetime !== null) { $time = transform::datetime($lastpasswordupdatetime); writer::export_user_preference('auth_manual', 'auth_manual_passwordupdatetime', $time, get_string('privacy:metadata:preference:passwordupdatetime', 'auth_manual') ); } } } PK&9\?(ɮmanual/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /auth/manual/*, information provided here is intended especially for developers. === 3.3 === * The config.html file was migrated to use the admin settings API. The identifier for configuration data stored in config_plugins table was converted from 'auth/manual' to 'auth_manual'. PK&9\arvA A manual/tests/manual_test.phpnu[. namespace auth_manual; use auth_plugin_manual; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/auth/manual/auth.php'); /** * Manual authentication tests class. * * @package auth_manual * @category test * @copyright 2014 Gilles-Philippe Leblanc * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class manual_test extends \advanced_testcase { /** @var auth_plugin_manual Keeps the authentication plugin. */ protected $authplugin; /** * Setup test data. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(true); $this->authplugin = new auth_plugin_manual(); set_config('expiration', '1', 'auth_manual'); set_config('expiration_warning', '2', 'auth_manual'); set_config('expirationtime', '30', 'auth_manual'); $this->authplugin->config = get_config(auth_plugin_manual::COMPONENT_NAME); } /** * Test user_update_password method. */ public function test_user_update_password(): void { $user = $this->getDataGenerator()->create_user(); $expectedtime = time(); $passwordisupdated = $this->authplugin->user_update_password($user, 'MyNewPassword*'); // Assert that the actual time should be equal or a little greater than the expected time. $this->assertGreaterThanOrEqual($expectedtime, get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id)); // Assert that the password was successfully updated. $this->assertTrue($passwordisupdated); } /** * Test test_password_expire method. */ public function test_password_expire(): void { $userrecord = array(); $expirationtime = 31 * DAYSECS; $userrecord['timecreated'] = time() - $expirationtime; $user1 = $this->getDataGenerator()->create_user($userrecord); $user2 = $this->getDataGenerator()->create_user(); // The user 1 was created 31 days ago and has not changed his password yet, so the password has expirated. $this->assertLessThanOrEqual(-1, $this->authplugin->password_expire($user1->username)); // The user 2 just came to be created and has not changed his password yet, so the password has not expirated. $this->assertEquals(30, $this->authplugin->password_expire($user2->username)); $this->authplugin->user_update_password($user1, 'MyNewPassword*'); // The user 1 just updated his password so the password has not expirated. $this->assertEquals(30, $this->authplugin->password_expire($user1->username)); } } PK&9\*7> > &manual/tests/privacy/provider_test.phpnu[. /** * Base class for unit tests for auth_manual. * * @package auth_manual * @category test * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace auth_manual\privacy; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/auth/manual/auth.php'); use core_privacy\local\request\writer; use core_privacy\local\request\transform; use auth_manual\privacy\provider; /** * Unit tests for the auth_manual implementation of the privacy API. * * @copyright 2018 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** @var \auth_plugin_manual Keeps the authentication plugin. */ protected $authplugin; /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); $this->authplugin = new \auth_plugin_manual(); } /** * Test to check export_user_preferences. * returns user preferences data. */ public function test_export_user_preferences(): void { $user = $this->getDataGenerator()->create_user(); $this->authplugin->user_update_password($user, 'MyPrivacytestPassword*'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $prefs = $writer->get_user_preferences('auth_manual'); $time = transform::datetime(get_user_preferences('auth_manual_passwordupdatetime', 0, $user->id)); $this->assertEquals($time, $prefs->auth_manual_passwordupdatetime->value); $this->assertEquals(get_string('privacy:metadata:preference:passwordupdatetime', 'auth_manual'), $prefs->auth_manual_passwordupdatetime->description); } } PK&9\uٶ&manual/tests/behat/auth_manual.featurenu[@auth @auth_manual Feature: Test manual authentication works. In order to check manual authentication As a teacher I need to go on login page and enter username and password. Background: Given the following "users" exist: | username | | teacher1 | @javascript Scenario: Check login works with javascript. Given I am on homepage And I expand navigation bar And I click on "Log in" "link" in the ".logininfo" "css_element" When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" When I press "Log in" Then I should see "You are logged in as" Scenario: Check login works without javascript. Given I am on homepage And I click on "Log in" "link" in the ".logininfo" "css_element" When I set the field "Username" to "teacher1" And I set the field "Password" to "teacher1" When I press "Log in" Then I should see "You are logged in as" PK&9\manual/auth.phpnu[. /** * Authentication Plugin: Manual Authentication * Just does a simple check against the moodle database. * * @package auth_manual * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/authlib.php'); /** * Manual authentication plugin. * * @package auth * @subpackage manual * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class auth_plugin_manual extends auth_plugin_base { /** * The name of the component. Used by the configuration. */ const COMPONENT_NAME = 'auth_manual'; const LEGACY_COMPONENT_NAME = 'auth/manual'; /** * Constructor. */ public function __construct() { $this->authtype = 'manual'; $config = get_config(self::COMPONENT_NAME); $legacyconfig = get_config(self::LEGACY_COMPONENT_NAME); $this->config = (object)array_merge((array)$legacyconfig, (array)$config); } /** * Old syntax of class constructor. Deprecated in PHP7. * * @deprecated since Moodle 3.1 */ public function auth_plugin_manual() { debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER); self::__construct(); } /** * Returns true if the username and password work and false if they are * wrong or don't exist. (Non-mnet accounts only!) * * @param string $username The username * @param string $password The password * @return bool Authentication success or failure. */ function user_login($username, $password) { global $CFG, $DB, $USER; if (!$user = $DB->get_record('user', array('username'=>$username, 'mnethostid'=>$CFG->mnet_localhost_id))) { return false; } if (!validate_internal_user_password($user, $password)) { return false; } if ($password === 'changeme') { // force the change - this is deprecated and it makes sense only for manual auth, // because most other plugins can not change password easily or // passwords are always specified by users set_user_preference('auth_forcepasswordchange', true, $user->id); } return true; } /** * Updates the user's password. * * Called when the user password is updated. * * @param object $user User table object * @param string $newpassword Plaintext password * @return boolean result */ function user_update_password($user, $newpassword) { $user = get_complete_user_data('id', $user->id); set_user_preference('auth_manual_passwordupdatetime', time(), $user->id); // This will also update the stored hash to the latest algorithm // if the existing hash is using an out-of-date algorithm (or the // legacy md5 algorithm). return update_internal_user_password($user, $newpassword); } function prevent_local_passwords() { return false; } /** * Returns true if this authentication plugin is 'internal'. * * @return bool */ function is_internal() { return true; } /** * Returns true if this authentication plugin can change the user's * password. * * @return bool */ function can_change_password() { return true; } /** * Returns the URL for changing the user's pw, or empty if the default can * be used. * * @return moodle_url */ function change_password_url() { return null; } /** * Returns true if plugin allows resetting of internal password. * * @return bool */ function can_reset_password() { return true; } /** * Returns true if plugin can be manually set. * * @return bool */ function can_be_manually_set() { return true; } /** * Return number of days to user password expires. * * If user password does not expire, it should return 0 or a positive value. * If user password is already expired, it should return negative value. * * @param mixed $username username (with system magic quotes) * @return integer */ public function password_expire($username) { $result = 0; if (!empty($this->config->expirationtime)) { $user = core_user::get_user_by_username($username, 'id,timecreated'); $lastpasswordupdatetime = get_user_preferences('auth_manual_passwordupdatetime', $user->timecreated, $user->id); $expiretime = $lastpasswordupdatetime + $this->config->expirationtime * DAYSECS; $now = time(); $result = ($expiretime - $now) / DAYSECS; if ($expiretime > $now) { $result = ceil($result); } else { $result = floor($result); } } return $result; } /** * Confirm the new user as registered. This should normally not be used, * but it may be necessary if the user auth_method is changed to manual * before the user is confirmed. * * @param string $username * @param string $confirmsecret */ function user_confirm($username, $confirmsecret = null) { global $DB; $user = get_complete_user_data('username', $username); if (!empty($user)) { if ($user->confirmed) { return AUTH_CONFIRM_ALREADY; } else { $DB->set_field("user", "confirmed", 1, array("id"=>$user->id)); return AUTH_CONFIRM_OK; } } else { return AUTH_CONFIRM_ERROR; } } } PK&9\q q manual/settings.phpnu[. /** * Admin settings and defaults * * @package auth_manual * @copyright 2017 Stephen Bourget * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { // Introductory explanation. $settings->add(new admin_setting_heading('auth_manual/pluginname', new lang_string('passwdexpire_settings', 'auth_manual'), new lang_string('auth_manualdescription', 'auth_manual'))); $expirationoptions = array( new lang_string('no'), new lang_string('yes'), ); $settings->add(new admin_setting_configselect('auth_manual/expiration', new lang_string('expiration', 'auth_manual'), new lang_string('expiration_desc', 'auth_manual'), 0, $expirationoptions)); $expirationtimeoptions = array( '30' => new lang_string('numdays', '', 30), '60' => new lang_string('numdays', '', 60), '90' => new lang_string('numdays', '', 90), '120' => new lang_string('numdays', '', 120), '150' => new lang_string('numdays', '', 150), '180' => new lang_string('numdays', '', 180), '365' => new lang_string('numdays', '', 365), ); $settings->add(new admin_setting_configselect('auth_manual/expirationtime', new lang_string('passwdexpiretime', 'auth_manual'), new lang_string('passwdexpiretime_desc', 'auth_manual'), 30, $expirationtimeoptions)); $expirationwarningoptions = array( '0' => new lang_string('never'), '1' => new lang_string('numdays', '', 1), '2' => new lang_string('numdays', '', 2), '3' => new lang_string('numdays', '', 3), '4' => new lang_string('numdays', '', 4), '5' => new lang_string('numdays', '', 5), '6' => new lang_string('numdays', '', 6), '7' => new lang_string('numdays', '', 7), '10' => new lang_string('numdays', '', 10), '14' => new lang_string('numdays', '', 14), ); $settings->add(new admin_setting_configselect('auth_manual/expiration_warning', new lang_string('expiration_warning', 'auth_manual'), new lang_string('expiration_warning_desc', 'auth_manual'), 0, $expirationwarningoptions)); // Display locking / mapping of profile fields. $authplugin = get_auth_plugin('manual'); display_auth_lock_options($settings, $authplugin->authtype, $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); } PK\hhregister.blade.phpnu[
@slot('title', 'Register') @if (session()->has('alert')) @slot('type', session('alert')['type']) @slot('msg', session('alert')['msg']) @endif
Register

Hi, welcome to MPWA MD version.

@csrf

@error('username') {{ $message }} @enderror

{{-- email icon --}}

@error('email') {{ $message }} @enderror

Already have account? Sign in here

PK\Ekulogin.blade.phpnu[ @slot('title', 'Login')
@if (session()->has('alert')) @slot('type', session('alert')['type']) @slot('msg', session('alert')['msg']) @endif
Sign In

Hi, welcome to MPWA MD version.

@csrf

@error('username') {{ $message }} @enderror

Don't have an account yet? Sign up here

PK&9\bgT T test_settings.phpnu[PK&9\ db/db/install.phpnu[PK&9\7K db/db/tasks.phpnu[PK&9\RVVdb/db/upgrade.phpnu[PK&9\!`]]db/lang/en/auth_db.phpnu[PK&9\r0db/version.phpnu[PK&9\n귲5db/classes/privacy/provider.phpnu[PK&9\3Pss4;db/classes/admin_setting_special_auth_configtext.phpnu[PK&9\!b``Bdb/classes/task/sync_users.phpnu[PK&9\/] CGIdb/upgrade.txtnu[PK&9\ƌ  -Mdb/cli/sync_users.phpnu[PK&9\H,5\\}[db/tests/db_test.phpnu[PK&9\1%Đtt Zdb/auth.phpnu[PK&9\?r5-db/settings.phpnu[PK&9\ݟ@kkHGlti/db/upgrade.phpnu[PK&9\pI&&Mlti/db/install.xmlnu[PK&9\Nll]Tlti/db/events.phpnu[PK&9\i i  Ylti/lang/en/auth_lti.phpnu[PK&9\Zn flti/login.phpnu[PK&9\~ {lti/lib.phpnu[PK&9\ZSlti/version.phpnu[PK&9\ d lti/classes/privacy/provider.phpnu[PK&9\]!k> lti/classes/local/ltiadvantage/entity/user_migration_claim.phpnu[PK&9\UN6^lti/classes/local/ltiadvantage/event/event_handler.phpnu[PK&9\[Gv v 8lti/classes/local/ltiadvantage/utility/cookie_helper.phpnu[PK&9\i/lti/classes/output/renderer.phpnu[PK&9\Fb/lti/templates/local/ltiadvantage/login.mustachenu[PK&9\ʄBlti/templates/local/ltiadvantage/account_binding_complete.mustachenu[PK&9\*F&&#glti/tests/privacy/provider_test.phpnu[PK&9\u6lti/tests/auth_test.phpnu[PK&9\,MwXwX lti/auth.phpnu[PK&9\1KKulshibboleth/db/install.phpnu[PK&9\(F!vv mshibboleth/db/upgrade.phpnu[PK&9\fyy&sshibboleth/lang/en/auth_shibboleth.phpnu[PK&9\CtIshibboleth/login.phpnu[PK&9\ b!!ɠshibboleth/lib.phpnu[PK&9\kS,shibboleth/version.phpnu[PK&9\³'shibboleth/classes/privacy/provider.phpnu[PK&9\(R 8cshibboleth/classes/admin_setting_special_wayf_select.phpnu[PK&9\_}9k k Dshibboleth/classes/admin_setting_special_convert_data_configfile.phpnu[PK&9\ ?shibboleth/classes/admin_setting_special_idp_configtextarea.phpnu[PK&9\=shibboleth/classes/helper.phpnu[PK&9\#.9(shibboleth/templates/login_form.mustachenu[PK&9\j?shibboleth/logout.phpnu[PK&9\/@ll^shibboleth/upgrade.txtnu[PK&9\kz|shibboleth/index.phpnu[PK&9\r559*shibboleth/auth.phpnu[PK&9\n[`shibboleth/settings.phpnu[PK&9\s1AArshibboleth/README.txtnu[PK&9\(rll>oauth2/db/access.phpnu[PK&9\OVVoauth2/db/upgrade.phpnu[PK&9\8oauth2/db/install.xmlnu[PK&9\LNoauth2/db/events.phpnu[PK&9\oauth2/lang/en/auth_oauth2.phpnu[PK&9\gfnoauth2/login.phpnu[PK&9\S" " oauth2/test.phpnu[PK&9\@9oauth2/lib.phpnu[PK&9\oauth2/version.phpnu[PK&9\t>u oauth2/confirm-linkedlogin.phpnu[PK&9\199oauth2/classes/api.phpnu[PK&9\MJ% #Ooauth2/classes/privacy/provider.phpnu[PK&9\El@poauth2/classes/linked_login.phpnu[PK&9\ 8 8 "~oauth2/classes/output/renderer.phpnu[PK&9\=bhh)oauth2/classes/auth.phpnu[PK&9\Q)oauth2/templates/idps.mustachenu[PK&9\墺EE%oauth2/templates/idpresponse.mustachenu[PK&9\9pp"oauth2/linkedlogins.phpnu[PK&9\7b oauth2/confirm-account.phpnu[PK&9\z'--&"oauth2/tests/privacy/provider_test.phpnu[PK&9\<Poauth2/tests/auth_test.phpnu[PK&9\?7((`oauth2/tests/api_test.phpnu[PK&9\P(oauth2/tests/behat/settings_test.featurenu[PK&9\X)-77oauth2/auth.phpnu[PK&9\@+dGGoauth2/settings.phpnu[PK&9\ nologin/lang/en/auth_nologin.phpnu[PK&9\"Jnologin/version.phpnu[PK&9\g$nologin/classes/privacy/provider.phpnu[PK&9\0 `nologin/auth.phpnu[PK&9\cW:W:6classes/external.phpnu[PK&9\1uhh)classes/form/verify_age_location_form.phpnu[PK&9\:g<<classes/privacy/provider.phpnu[PK&9\£  +classes/output/verify_age_location_page.phpnu[PK&9\Q:_classes/output/login.phpnu[PK&9\dhtt%{6classes/output/digital_minor_page.phpnu[PK&9\D>classes/digital_consent.phpnu[PK&9\__ ZMupgrade.txtnu[PK&9\ I.,,aemail/db/services.phpnu[PK&9\OS__ehemail/db/upgrade.phpnu[PK&9\sEZeDDoemail/lang/en/auth_email.phpnu[PK&9\Twemail/version.phpnu[PK&9\AFAFq|email/classes/external.phpnu[PK&9\c"email/classes/privacy/provider.phpnu[PK&9\aG&&6email/upgrade.txtnu[PK&9\,f7&7&&email/tests/external/external_test.phpnu[PK&9\g$$&*email/tests/behat/behat_auth_email.phpnu[PK&9\*ex/22 email/tests/behat/signup.featurenu[PK&9\\$& email/auth.phpnu[PK&9\4J$+email/settings.phpnu[PK&9\S<&1webservice/lang/en/auth_webservice.phpnu[PK&9\ޯŒ7webservice/version.phpnu[PK&9\D';webservice/classes/privacy/provider.phpnu[PK&9\>HBwebservice/auth.phpnu[PK&9\  JTUPGRADING.mdnu[PK&9\ aWindex.htmlnu[PK&9\S3EEWldap/db/install.phpnu[PK&9\#Xldap/db/tasks.phpnu[PK&9\_4]] ^ldap/db/upgrade.phpnu[PK&9\5BBdldap/lang/en/auth_ldap.phpnu[PK&9\)g0ldap/version.phpnu[PK&9\|hgQ77:ԫldap/classes/admin_setting_special_contexts_configtext.phpnu[PK&9\4YF!uldap/classes/privacy/provider.phpnu[PK&9\z6ldap/classes/admin_setting_special_ntlm_configtext.phpnu[PK&9\8 ldap/classes/task/sync_task.phpnu[PK&9\E ldap/classes/task/sync_roles.phpnu[PK&9\*,:ldap/classes/task/asynchronous_sync_task.phpnu[PK&9\^=Z^Cldap/classes/adminpresets/adminpresets_auth_ldap_admin_setting_special_contexts_configtext.phpnu[PK&9\;Yldap/classes/admin_setting_special_lowercase_configtext.phpnu[PK&9\ldap/upgrade.txtnu[PK&9\ldap/locallib.phpnu[PK&9\QnfJJldap/README-LDAPnu[PK&9\zT} <ldap/cli/sync_users.phpnu[PK&9\TfTfA ldap/tests/auth_ldap_test.phpnu[PK&9\#YCg ldap/ntlmsso_attempt.phpnu[PK&9\ƫÆ*m ldap/ntlmsso_finish.phpnu[PK&9\`|| q ldap/auth.phpnu[PK&9\׆ ldap/ntlmsso_magic.phpnu[PK&9\"mFFA ldap/settings.phpnu[PK&9\wF%% {; tests/external/external_test.phpnu[PK&9\Њr` tests/privacy/provider_test.phpnu[PK&9\!9G r tests/behat/login.phpnu[PK&9\? tests/behat/loginform.featurenu[PK&9\ߗw? ? $q tests/behat/rememberusername.featurenu[PK&9\MNySS tests/behat/logout.phpnu[PK&9\W  tests/behat/behat_auth.phpnu[PK&9\  tests/behat/login.featurenu[PK&9\kǩ& tests/behat/verifyageofconsent.featurenu[PK&9\Z ( tests/behat/displayloginfailures.featurenu[PK&9\/a  0 tests/behat/validateagedigitalconsentmap.featurenu[PK&9\+us tests/digital_consent_test.phpnu[PK&9\\\m none/db/upgrade.phpnu[PK&9\uc none/lang/en/auth_none.phpnu[PK&9\tww  none/lib.phpnu[PK&9\V none/version.phpnu[PK&9\ ! none/classes/privacy/provider.phpnu[PK&9\ǯ)) none/classes/check/noauth.phpnu[PK&9\7A none/upgrade.txtnu[PK&9\{dd ) none/auth.phpnu[PK&9\O`lEE' none/settings.phpnu[PK&9\*LuuP- manual/db/upgrade.phpnu[PK&9\*R 4 manual/lang/en/auth_manual.phpnu[PK&9\L; manual/version.phpnu[PK&9\Dk k #@ manual/classes/privacy/provider.phpnu[PK&9\?(ɮJ manual/upgrade.txtnu[PK&9\arvA A L manual/tests/manual_test.phpnu[PK&9\*7> > &CZ manual/tests/privacy/provider_test.phpnu[PK&9\uٶ&d manual/tests/behat/auth_manual.featurenu[PK&9\h manual/auth.phpnu[PK&9\q q  manual/settings.phpnu[PK\hh register.blade.phpnu[PK\EkuC login.blade.phpnu[PK9I