├── .github └── workflows │ └── ci.yml ├── .travis.yml ├── README.md ├── auth.php ├── classes ├── core_userkey_manager.php ├── privacy │ └── provider.php └── userkey_manager_interface.php ├── db ├── access.php ├── services.php └── upgrade.php ├── externallib.php ├── lang └── en │ └── auth_userkey.php ├── login.php ├── logout.php ├── pix └── catalyst-logo.png ├── settings.php ├── tests ├── auth_plugin_test.php ├── core_userkey_manager_test.php ├── externallib_test.php └── fake_userkey_manager.php └── version.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/ci.yml 2 | name: ci 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | ci: 8 | uses: catalyst/catalyst-moodle-workflows/.github/workflows/ci.yml@main -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | notifications: 4 | email: 5 | recipients: 6 | - dmitriim@catalyst-au.net 7 | 8 | sudo: false 9 | 10 | cache: 11 | directories: 12 | - $HOME/.composer/cache 13 | 14 | addons: 15 | postgresql: "9.6" 16 | 17 | php: 18 | - 7.1 19 | - 7.2 20 | - 7.3 21 | 22 | env: 23 | global: 24 | - DB=pgsql 25 | matrix: 26 | - MOODLE_BRANCH=MOODLE_33_STABLE 27 | - MOODLE_BRANCH=MOODLE_34_STABLE 28 | - MOODLE_BRANCH=MOODLE_35_STABLE 29 | - MOODLE_BRANCH=MOODLE_36_STABLE 30 | - MOODLE_BRANCH=MOODLE_37_STABLE 31 | - MOODLE_BRANCH=MOODLE_38_STABLE 32 | - MOODLE_BRANCH=master 33 | 34 | matrix: 35 | exclude: 36 | - php: 7.1 37 | env: MOODLE_BRANCH=master 38 | - php: 7.2 39 | env: MOODLE_BRANCH=MOODLE_33_STABLE 40 | - php: 7.3 41 | env: MOODLE_BRANCH=MOODLE_33_STABLE 42 | - php: 7.3 43 | env: MOODLE_BRANCH=MOODLE_34_STABLE 44 | - php: 7.3 45 | env: MOODLE_BRANCH=MOODLE_35_STABLE 46 | 47 | before_install: 48 | - cd ../.. 49 | - composer selfupdate 50 | - composer create-project -n --no-dev moodlerooms/moodle-plugin-ci ci ^1 51 | - export PATH="$(cd ci/bin; pwd):$(cd ci/vendor/bin; pwd):$PATH" 52 | 53 | install: 54 | - moodle-plugin-ci install -vvv 55 | 56 | script: 57 | - moodle-plugin-ci phplint 58 | - moodle-plugin-ci phpmd 59 | - moodle-plugin-ci codechecker 60 | - moodle-plugin-ci csslint 61 | - moodle-plugin-ci shifter 62 | - moodle-plugin-ci jshint 63 | - moodle-plugin-ci phpunit 64 | - moodle-plugin-ci behat 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/catalyst/moodle-auth_userkey/ci/MOODLE_33PLUS) 2 | 3 | 4 | Log in to Moodle using one time user key. 5 | ========================================= 6 | 7 | Auth plugin for organising simple one way SSO(single sign on) between moodle and your external web 8 | application. The main idea is to make a web call to moodle and provide one of the possible matching 9 | fields to find required user and generate one time login URL. A user can be redirected to this 10 | URL to be log in to Moodle without typing username and password. 11 | 12 | 13 | Using 14 | ----- 15 | 1. Install the plugin as usual. 16 | 2. Enable the userkey authentication plugin (Site administration -> Plugins -> Authentication and then enable User key). 17 | 3. Configure the plugin. Set required Mapping field, User key life time, IP restriction and Logout redirect URL. 18 | 4. Enable and configure just installed plugin. Set required Mapping field, User key life time, IP restriction and Logout redirect URL. 19 | 5. Enable web service advance feature (Admin > Advanced features), more info http://docs.moodle.org/en/Web_services 20 | 6. Enable one of the supported protocols (Admin > Plugins > Web services > Manage protocols) 21 | 7. Create a token for a specific user and for the service 'User key authentication web service' (Admin > Plugins > Web services > Manage tokens) 22 | 8. Make sure that the "web service" user has 'auth/userkey:generatekey' capability. 23 | 9. Authorise the "web service" user: Admin > Plugins > Web services > External services, select 'Authorised users' for the web service, and add the user. 24 | 10. Configure your external application to make a web call to get login URL. 25 | 11. Redirect your users to this URL to be logged in to Moodle. 26 | 27 | Configuration 28 | ------------- 29 | 30 | **Mapping field** 31 | 32 | Required data structure for web call is related to mapping field you configured. 33 | 34 | For example XML-RPC (PHP structure) description for different mapping field settings: 35 | 36 | ***User name*** 37 | 38 | [user] => 39 | Array 40 | ( 41 | [username] => string 42 | ) 43 | 44 | ***Email Address*** 45 | 46 | [user] => 47 | Array 48 | ( 49 | [email] => string 50 | ) 51 | 52 | ***ID number*** 53 | 54 | [user] => 55 | Array 56 | ( 57 | [idnumber] => string 58 | ) 59 | 60 | ***Web service will return following structure or standard Moodle webservice error message.*** 61 | 62 | Array 63 | ( 64 | [loginurl] => string 65 | ) 66 | 67 | Please navigate to API documentation to get full description for "auth_userkey_request_login_url" function. 68 | e.g. http://yourmoodle.com/admin/webservice/documentation.php 69 | 70 | You can amend login URL by "wantsurl" parameter to redirect user after they logged in to Moodle. 71 | 72 | E.g. http://yourmoodle.com/auth/userkey/login.php?key=uniquekey&wantsurl=http://yourmoodle.com/course/view.php?id=3 73 | 74 | Wantsurl maybe internal and external. 75 | 76 | 77 | **User key life time** 78 | 79 | This setting describes for how long a user key will be valid. If you try to use expired key then you will 80 | get an error. 81 | 82 | **IP restriction** 83 | 84 | If this setting is set to yes, then your web application has to provie user's ip address to generate a user key. Then 85 | the user should have provided ip when using this key. If ip address is different a user will get an error. 86 | 87 | **Redirect after logout from Moodle** 88 | 89 | You can set URL to redirect users after they logged out from Moodle. For example you can redirect them 90 | to logout script of your web application to log users out from it as well. This setting is optional. 91 | 92 | **URL of SSO host** 93 | 94 | You can set URL to redirect users before they see Moodle login page. For example you can redirect them 95 | to your web application to login page. You can use "enrolkey_skipsso" URL parameter to bypass this option. 96 | E.g. http://yourmoodle.com/login/index.php?enrolkey_skipsso=1 97 | 98 | **Logout URL** 99 | 100 | If you need to logout users after they logged out from the external application, you can redirect them 101 | to logout script with required parameter "return". 102 | 103 | E.g. http://yourmoodle.com/auth/userkey/logout.php?return=www.google.com 104 | 105 | 106 | Users will be logged out from Moodle and then redirected to the provided URL. 107 | In case when a user session is already expired, the user will be still redirected. 108 | 109 | 110 | **Example client** 111 | 112 | **Note:** the code below is not for production use. It's just a quick and dirty way to test the functionality. 113 | 114 | The code below defines a function that can be used to obtain a login url. 115 | You will need to add/remove parameters depending on whether you have update/create user enabled and which mapping field you are using. 116 | 117 | The required library curl can be obtained from https://github.com/moodlehq/sample-ws-clients 118 | ```php 119 | /** 120 | * @param string $useremail Email address of user to create token for. 121 | * @param string $firstname First name of user (used to update/create user). 122 | * @param string $lastname Last name of user (used to update/create user). 123 | * @param string $username Username of user (used to update/create user). 124 | * @param string $ipaddress IP address of end user that login request will come from (probably $_SERVER['REMOTE_ADDR']). 125 | * @param int $courseid Course id to send logged in users to, defaults to site home. 126 | * @param int $modname Name of course module to send users to, defaults to none. 127 | * @param int $activityid cmid to send logged in users to, defaults to site home. 128 | * @return bool|string 129 | */ 130 | function getloginurl($useremail, $firstname, $lastname, $username, $courseid = null, $modname = null, $activityid = null) { 131 | require_once('curl.php'); 132 | 133 | $token = 'YOUR_TOKEN'; 134 | $domainname = 'http://MOODLE_WWW_ROOT'; 135 | $functionname = 'auth_userkey_request_login_url'; 136 | 137 | $param = [ 138 | 'user' => [ 139 | 'firstname' => $firstname, // You will not need this parameter, if you are not creating/updating users 140 | 'lastname' => $lastname, // You will not need this parameter, if you are not creating/updating users 141 | 'username' => $username, 142 | 'email' => $useremail, 143 | ] 144 | ]; 145 | 146 | $serverurl = $domainname . '/webservice/rest/server.php' . '?wstoken=' . $token . '&wsfunction=' . $functionname . '&moodlewsrestformat=json'; 147 | $curl = new curl; // The required library curl can be obtained from https://github.com/moodlehq/sample-ws-clients 148 | 149 | try { 150 | $resp = $curl->post($serverurl, $param); 151 | $resp = json_decode($resp); 152 | if ($resp && !empty($resp->loginurl)) { 153 | $loginurl = $resp->loginurl; 154 | } 155 | } catch (Exception $ex) { 156 | return false; 157 | } 158 | 159 | if (!isset($loginurl)) { 160 | return false; 161 | } 162 | 163 | $path = ''; 164 | if (isset($courseid)) { 165 | $path = '&wantsurl=' . urlencode("$domainname/course/view.php?id=$courseid"); 166 | } 167 | if (isset($modname) && isset($activityid)) { 168 | $path = '&wantsurl=' . urlencode("$domainname/mod/$modname/view.php?id=$activityid"); 169 | } 170 | 171 | return $loginurl . $path; 172 | } 173 | 174 | echo getloginurl('barrywhite@googlemail.com', 'barry', 'white', 'barrywhite', 2, 'certificate', 8); 175 | ``` 176 | 177 | 178 | # Crafted by Catalyst IT 179 | 180 | This plugin was developed by Catalyst IT Australia: 181 | 182 | https://www.catalyst-au.net/ 183 | 184 | ![Catalyst IT](/pix/catalyst-logo.png?raw=true) 185 | 186 | # Contributing and Support 187 | 188 | Issues, and pull requests using github are welcome and encouraged! 189 | 190 | https://github.com/catalyst/moodle-auth_userkey/issues 191 | 192 | If you would like commercial support or would like to sponsor additional improvements 193 | to this plugin please contact us: 194 | 195 | https://www.catalyst-au.net/contact-us 196 | -------------------------------------------------------------------------------- /auth.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * User key auth method. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | use auth_userkey\core_userkey_manager; 28 | use auth_userkey\userkey_manager_interface; 29 | 30 | require_once($CFG->libdir . "/externallib.php"); 31 | require_once($CFG->libdir.'/authlib.php'); 32 | require_once($CFG->dirroot . '/user/lib.php'); 33 | 34 | /** 35 | * User key authentication plugin. 36 | */ 37 | class auth_plugin_userkey extends auth_plugin_base { 38 | 39 | /** 40 | * Default mapping field. 41 | */ 42 | const DEFAULT_MAPPING_FIELD = 'email'; 43 | 44 | /** 45 | * User key manager. 46 | * 47 | * @var userkey_manager_interface 48 | */ 49 | protected $userkeymanager; 50 | 51 | /** 52 | * Defaults for config form. 53 | * 54 | * @var array 55 | */ 56 | protected $defaults = array( 57 | 'mappingfield' => self::DEFAULT_MAPPING_FIELD, 58 | 'keylifetime' => 60, 59 | 'iprestriction' => 0, 60 | 'ipwhitelist' => '', 61 | 'redirecturl' => '', 62 | 'ssourl' => '', 63 | 'createuser' => false, 64 | 'updateuser' => false, 65 | ); 66 | 67 | /** 68 | * Constructor. 69 | */ 70 | public function __construct() { 71 | $this->authtype = 'userkey'; 72 | $this->config = get_config('auth_userkey'); 73 | $this->userkeymanager = new core_userkey_manager($this->config); 74 | } 75 | 76 | /** 77 | * All the checking happens before the login page in this hook. 78 | * 79 | * It redirects a user if required or return true. 80 | */ 81 | public function pre_loginpage_hook() { 82 | global $SESSION; 83 | 84 | // If we previously tried to skip SSO on, but then navigated 85 | // away, and come in from another deep link while SSO only is 86 | // on, then reset the previous session memory of forcing SSO. 87 | if (isset($SESSION->enrolkey_skipsso)) { 88 | unset($SESSION->enrolkey_skipsso); 89 | } 90 | 91 | return $this->loginpage_hook(); 92 | } 93 | 94 | /** 95 | * All the checking happens before the login page in this hook. 96 | * 97 | * It redirects a user if required or return true. 98 | */ 99 | public function loginpage_hook() { 100 | if ($this->should_login_redirect()) { 101 | $this->redirect($this->config->ssourl); 102 | } 103 | 104 | return true; 105 | } 106 | 107 | /** 108 | * Redirects the user to provided URL. 109 | * 110 | * @param string $url URL to redirect to. 111 | * 112 | * @throws \moodle_exception If gets running via CLI or AJAX call. 113 | */ 114 | protected function redirect($url) { 115 | if (CLI_SCRIPT || AJAX_SCRIPT) { 116 | throw new moodle_exception('redirecterrordetected', 'auth_userkey', '', $url); 117 | } 118 | 119 | redirect($url); 120 | } 121 | 122 | /** 123 | * Don't allow login using login form. 124 | * 125 | * @param string $username The username (with system magic quotes) 126 | * @param string $password The password (with system magic quotes) 127 | * 128 | * @return bool Authentication success or failure. 129 | */ 130 | public function user_login($username, $password) { 131 | return false; 132 | } 133 | 134 | /** 135 | * Logs a user in using userkey and redirects after. 136 | * 137 | * @throws \moodle_exception If something went wrong. 138 | */ 139 | public function user_login_userkey() { 140 | global $SESSION, $CFG, $USER; 141 | 142 | $keyvalue = required_param('key', PARAM_ALPHANUM); 143 | $wantsurl = optional_param('wantsurl', '', PARAM_URL); 144 | 145 | if (!empty($wantsurl)) { 146 | $redirecturl = $wantsurl; 147 | } else { 148 | $redirecturl = $CFG->wwwroot; 149 | } 150 | 151 | try { 152 | $key = $this->userkeymanager->validate_key($keyvalue); 153 | } catch (moodle_exception $exception) { 154 | // If user is logged in and key is not valid, we'd like to logout a user. 155 | if (isloggedin()) { 156 | require_logout(); 157 | } 158 | throw $exception; 159 | } 160 | 161 | if (isloggedin()) { 162 | if ($USER->id != $key->userid) { 163 | // Logout the current user if it's different to one that associated to the valid key. 164 | require_logout(); 165 | } else { 166 | // Don't process further if the user is already logged in. 167 | $this->userkeymanager->delete_keys($key->userid); 168 | $this->redirect($redirecturl); 169 | } 170 | } 171 | 172 | $this->userkeymanager->delete_keys($key->userid); 173 | 174 | $user = get_complete_user_data('id', $key->userid); 175 | complete_user_login($user); 176 | 177 | // Identify this session as using user key auth method. 178 | $SESSION->userkey = true; 179 | 180 | $this->redirect($redirecturl); 181 | } 182 | 183 | /** 184 | * Don't store local passwords. 185 | * 186 | * @return bool True. 187 | */ 188 | public function prevent_local_passwords() { 189 | return true; 190 | } 191 | 192 | /** 193 | * Returns true if this authentication plugin is external. 194 | * 195 | * @return bool False. 196 | */ 197 | public function is_internal() { 198 | return false; 199 | } 200 | 201 | /** 202 | * The plugin can't change the user's password. 203 | * 204 | * @return bool False. 205 | */ 206 | public function can_change_password() { 207 | return false; 208 | } 209 | 210 | /** 211 | * Set userkey manager. 212 | * 213 | * This function is the only way to inject dependency, because of the way auth plugins work. 214 | * 215 | * @param \auth_userkey\userkey_manager_interface $keymanager 216 | */ 217 | public function set_userkey_manager(userkey_manager_interface $keymanager) { 218 | $this->userkeymanager = $keymanager; 219 | } 220 | 221 | /** 222 | * Return mapping field to find a lms user. 223 | * 224 | * @return string 225 | */ 226 | public function get_mapping_field() { 227 | if (isset($this->config->mappingfield) && !empty($this->config->mappingfield)) { 228 | return $this->config->mappingfield; 229 | } 230 | 231 | return self::DEFAULT_MAPPING_FIELD; 232 | } 233 | 234 | /** 235 | * Check if we need to create a new user. 236 | * 237 | * @return bool 238 | */ 239 | protected function should_create_user() { 240 | if (isset($this->config->createuser) && $this->config->createuser == true) { 241 | return true; 242 | } 243 | 244 | return false; 245 | } 246 | 247 | /** 248 | * Check if we need to update users. 249 | * 250 | * @return bool 251 | */ 252 | protected function should_update_user() { 253 | if (isset($this->config->updateuser) && $this->config->updateuser == true) { 254 | return true; 255 | } 256 | 257 | return false; 258 | } 259 | 260 | /** 261 | * Check if restriction by IP is enabled. 262 | * 263 | * @return bool 264 | */ 265 | protected function is_ip_restriction_enabled() { 266 | if (isset($this->config->iprestriction) && $this->config->iprestriction == true) { 267 | return true; 268 | } 269 | 270 | return false; 271 | } 272 | 273 | /** 274 | * Create a new user. 275 | * 276 | * @param array $data Validated user data from web service. 277 | * 278 | * @return object User object. 279 | */ 280 | protected function create_user(array $data) { 281 | global $DB, $CFG; 282 | 283 | $user = $data; 284 | unset($user['ip']); 285 | $user['auth'] = 'userkey'; 286 | $user['confirmed'] = 1; 287 | $user['mnethostid'] = $CFG->mnet_localhost_id; 288 | 289 | $requiredfieds = ['username', 'email', 'firstname', 'lastname']; 290 | $missingfields = []; 291 | foreach ($requiredfieds as $requiredfied) { 292 | if (empty($user[$requiredfied])) { 293 | $missingfields[] = $requiredfied; 294 | } 295 | } 296 | if (!empty($missingfields)) { 297 | throw new invalid_parameter_exception('Unable to create user, missing value(s): ' . implode(',', $missingfields)); 298 | } 299 | 300 | if ($DB->record_exists('user', array('username' => $user['username'], 'mnethostid' => $CFG->mnet_localhost_id))) { 301 | throw new invalid_parameter_exception('Username already exists: '.$user['username']); 302 | } 303 | if (!validate_email($user['email'])) { 304 | throw new invalid_parameter_exception('Email address is invalid: '.$user['email']); 305 | } else if (empty($CFG->allowaccountssameemail) && 306 | $DB->record_exists('user', array('email' => $user['email'], 'mnethostid' => $user['mnethostid']))) { 307 | throw new invalid_parameter_exception('Email address already exists: '.$user['email']); 308 | } 309 | 310 | $userid = user_create_user($user); 311 | return $DB->get_record('user', ['id' => $userid]); 312 | } 313 | 314 | /** 315 | * Update an existing user. 316 | * 317 | * @param stdClass $user Existing user record. 318 | * @param array $data Validated user data from web service. 319 | * 320 | * @return object User object. 321 | */ 322 | protected function update_user(\stdClass $user, array $data) { 323 | global $DB, $CFG; 324 | 325 | $userdata = $data; 326 | unset($userdata['ip']); 327 | $userdata['auth'] = 'userkey'; 328 | 329 | $changed = false; 330 | foreach ($userdata as $key => $value) { 331 | if ($user->$key != $value) { 332 | $changed = true; 333 | break; 334 | } 335 | } 336 | 337 | if (!$changed) { 338 | return $user; 339 | } 340 | 341 | if ( 342 | $user->username != $userdata['username'] 343 | && 344 | $DB->record_exists('user', array('username' => $userdata['username'], 'mnethostid' => $CFG->mnet_localhost_id)) 345 | ) { 346 | throw new invalid_parameter_exception('Username already exists: '.$userdata['username']); 347 | } 348 | if (!validate_email($userdata['email'])) { 349 | throw new invalid_parameter_exception('Email address is invalid: '.$userdata['email']); 350 | } else if ( 351 | empty($CFG->allowaccountssameemail) 352 | && 353 | $user->email != $userdata['email'] 354 | && 355 | $DB->record_exists('user', array('email' => $userdata['email'], 'mnethostid' => $CFG->mnet_localhost_id)) 356 | ) { 357 | throw new invalid_parameter_exception('Email address already exists: '.$userdata['email']); 358 | } 359 | $userdata['id'] = $user->id; 360 | 361 | $userdata = (object) $userdata; 362 | user_update_user($userdata, false); 363 | return $DB->get_record('user', ['id' => $user->id]); 364 | } 365 | 366 | /** 367 | * Validate user data from web service. 368 | * 369 | * @param mixed $data User data from web service. 370 | * 371 | * @return array 372 | * 373 | * @throws \invalid_parameter_exception If provided data is invalid. 374 | */ 375 | protected function validate_user_data($data) { 376 | $data = (array)$data; 377 | 378 | $mappingfield = $this->get_mapping_field(); 379 | 380 | if (!isset($data[$mappingfield]) || empty($data[$mappingfield])) { 381 | throw new invalid_parameter_exception('Required field "' . $mappingfield . '" is not set or empty.'); 382 | } 383 | 384 | if ($this->is_ip_restriction_enabled() && !isset($data['ip'])) { 385 | throw new invalid_parameter_exception('Required parameter "ip" is not set.'); 386 | } 387 | 388 | return $data; 389 | } 390 | 391 | /** 392 | * Return user object. 393 | * 394 | * @param array $data Validated user data. 395 | * 396 | * @return object A user object. 397 | * 398 | * @throws \invalid_parameter_exception If user is not exist and we don't need to create a new. 399 | */ 400 | protected function get_user(array $data) { 401 | global $DB, $CFG; 402 | 403 | $mappingfield = $this->get_mapping_field(); 404 | 405 | $params = array( 406 | $mappingfield => $data[$mappingfield], 407 | 'mnethostid' => $CFG->mnet_localhost_id, 408 | ); 409 | 410 | $user = $DB->get_record('user', $params); 411 | 412 | if (empty($user)) { 413 | if ($this->should_create_user()) { 414 | $user = $this->create_user($data); 415 | } else { 416 | throw new invalid_parameter_exception('User is not exist'); 417 | } 418 | } else if ($this->should_update_user()) { 419 | $user = $this->update_user($user, $data); 420 | } 421 | 422 | return $user; 423 | } 424 | 425 | /** 426 | * Return allowed IPs from user data. 427 | * 428 | * @param array $data Validated user data. 429 | * 430 | * @return null|string Allowed IPs or null. 431 | */ 432 | protected function get_allowed_ips(array $data) { 433 | if (isset($data['ip']) && !empty($data['ip'])) { 434 | return $data['ip']; 435 | } 436 | 437 | return null; 438 | } 439 | 440 | /** 441 | * Generate login user key. 442 | * 443 | * @param array $data Validated user data. 444 | * 445 | * @return string 446 | * @throws \invalid_parameter_exception 447 | */ 448 | protected function generate_user_key(array $data) { 449 | $user = $this->get_user($data); 450 | $ips = $this->get_allowed_ips($data); 451 | 452 | return $this->userkeymanager->create_key($user->id, $ips); 453 | } 454 | 455 | /** 456 | * Return login URL. 457 | * 458 | * @param array|stdClass $data User data from web service. 459 | * 460 | * @return string Login URL. 461 | * 462 | * @throws \invalid_parameter_exception 463 | */ 464 | public function get_login_url($data) { 465 | global $CFG; 466 | 467 | $userdata = $this->validate_user_data($data); 468 | $userkey = $this->generate_user_key($userdata); 469 | 470 | return $CFG->wwwroot . '/auth/userkey/login.php?key=' . $userkey; 471 | } 472 | 473 | /** 474 | * Return a list of mapping fields. 475 | * 476 | * @return array 477 | */ 478 | public function get_allowed_mapping_fields() { 479 | return array( 480 | 'username' => get_string('username'), 481 | 'email' => get_string('email'), 482 | 'idnumber' => get_string('idnumber'), 483 | ); 484 | } 485 | 486 | /** 487 | * Return a mapping parameter for request_login_url_parameters(). 488 | * 489 | * @return array 490 | */ 491 | protected function get_mapping_parameter() { 492 | $mappingfield = $this->get_mapping_field(); 493 | 494 | switch ($mappingfield) { 495 | case 'username': 496 | $parameter = array( 497 | 'username' => new external_value( 498 | PARAM_USERNAME, 499 | 'Username' 500 | ), 501 | ); 502 | break; 503 | 504 | case 'email': 505 | $parameter = array( 506 | 'email' => new external_value( 507 | PARAM_EMAIL, 508 | 'A valid email address' 509 | ), 510 | ); 511 | break; 512 | 513 | case 'idnumber': 514 | $parameter = array( 515 | 'idnumber' => new external_value( 516 | PARAM_RAW, 517 | 'An arbitrary ID code number perhaps from the institution' 518 | ), 519 | ); 520 | break; 521 | 522 | default: 523 | $parameter = array(); 524 | break; 525 | } 526 | 527 | return $parameter; 528 | } 529 | 530 | /** 531 | * Return user fields parameters for request_login_url_parameters(). 532 | * 533 | * @return array 534 | */ 535 | protected function get_user_fields_parameters() { 536 | $parameters = array(); 537 | 538 | if ($this->is_ip_restriction_enabled()) { 539 | $parameters['ip'] = new external_value( 540 | PARAM_HOST, 541 | 'User IP address' 542 | ); 543 | } 544 | 545 | $mappingfield = $this->get_mapping_field(); 546 | if ($this->should_create_user() || $this->should_update_user()) { 547 | $parameters['firstname'] = new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL); 548 | $parameters['lastname'] = new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL); 549 | 550 | if ($mappingfield != 'email') { 551 | $parameters['email'] = new external_value(PARAM_RAW_TRIMMED, 'A valid and unique email address', VALUE_OPTIONAL); 552 | } 553 | if ($mappingfield != 'username') { 554 | $parameters['username'] = new external_value(PARAM_USERNAME, 'A valid and unique username', VALUE_OPTIONAL); 555 | } 556 | } 557 | 558 | return $parameters; 559 | } 560 | 561 | /** 562 | * Return parameters for request_login_url_parameters(). 563 | * 564 | * @return array 565 | */ 566 | public function get_request_login_url_user_parameters() { 567 | $parameters = array_merge($this->get_mapping_parameter(), $this->get_user_fields_parameters()); 568 | 569 | return $parameters; 570 | } 571 | 572 | /** 573 | * Check if we should redirect a user as part of login. 574 | * 575 | * @return bool 576 | */ 577 | protected function should_login_redirect() { 578 | global $SESSION; 579 | 580 | $skipsso = optional_param('enrolkey_skipsso', 0, PARAM_BOOL); 581 | 582 | // Check whether we've skipped SSO already. 583 | // This is here because loginpage_hook is called again during form 584 | // submission (all of login.php is processed) and ?skipsso=on is not 585 | // preserved forcing us to the SSO. 586 | if ((isset($SESSION->enrolkey_skipsso) && $SESSION->enrolkey_skipsso == 1)) { 587 | return false; 588 | } 589 | 590 | $SESSION->enrolkey_skipsso = $skipsso; 591 | 592 | // If SSO only is set and user is not passing the skip param 593 | // or has it already set in their session then redirect to the SSO URL. 594 | if (isset($this->config->ssourl) && $this->config->ssourl != '' && !$skipsso) { 595 | return true; 596 | } 597 | 598 | } 599 | 600 | /** 601 | * Check if we should redirect a user after logout. 602 | * 603 | * @return bool 604 | */ 605 | protected function should_logout_redirect() { 606 | global $SESSION; 607 | 608 | if (!isset($SESSION->userkey)) { 609 | return false; 610 | } 611 | 612 | if (!isset($this->config->redirecturl)) { 613 | return false; 614 | } 615 | 616 | if (empty($this->config->redirecturl)) { 617 | return false; 618 | } 619 | 620 | return true; 621 | } 622 | 623 | 624 | /** 625 | * Logout page hook. 626 | * 627 | * Override redirect URL after logout. 628 | * 629 | * @see auth_plugin_base::logoutpage_hook() 630 | */ 631 | public function logoutpage_hook() { 632 | global $redirect; 633 | 634 | if ($this->should_logout_redirect()) { 635 | $redirect = $this->config->redirecturl; 636 | } 637 | } 638 | 639 | /** 640 | * Log out user and redirect. 641 | */ 642 | public function user_logout_userkey() { 643 | global $CFG, $USER; 644 | 645 | $redirect = required_param('return', PARAM_LOCALURL); 646 | 647 | // We redirect when user's session in Moodle already has expired 648 | // or the user is still logged in using "userkey" auth type. 649 | if (!isloggedin() || $USER->auth == 'userkey') { 650 | require_logout(); 651 | $this->redirect($redirect); 652 | } else { 653 | // If logged in with different auth type, then display an error. 654 | throw new moodle_exception('incorrectlogout', 'auth_userkey', $CFG->wwwroot); 655 | } 656 | } 657 | } 658 | -------------------------------------------------------------------------------- /classes/core_userkey_manager.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace auth_userkey; 18 | 19 | /** 20 | * Key manager class. 21 | * 22 | * @package auth_userkey 23 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class core_userkey_manager implements userkey_manager_interface { 27 | 28 | /** 29 | * This script script required by core create_user_key(). 30 | */ 31 | const CORE_USER_KEY_MANAGER_SCRIPT = 'auth/userkey'; 32 | 33 | /** 34 | * Default life time of the user key in seconds. 35 | */ 36 | const DEFAULT_KEY_LIFE_TIME_IN_SECONDS = 60; 37 | 38 | /** 39 | * Config object. 40 | * 41 | * @var \stdClass 42 | */ 43 | protected $config; 44 | 45 | /** 46 | * Constructor. 47 | * 48 | * @param \stdClass $config 49 | */ 50 | public function __construct(\stdClass $config) { 51 | $this->config = $config; 52 | } 53 | 54 | /** 55 | * Create a user key. 56 | * 57 | * @param int $userid User ID. 58 | * @param null|array $allowedips A list of allowed ips for this key. 59 | * 60 | * @return string Generated key. 61 | */ 62 | public function create_key($userid, $allowedips = null) { 63 | $this->delete_keys($userid); 64 | 65 | if (isset($this->config->keylifetime) && (int)$this->config->keylifetime > 0) { 66 | $validuntil = time() + $this->config->keylifetime; 67 | } else { 68 | $validuntil = time() + self::DEFAULT_KEY_LIFE_TIME_IN_SECONDS; 69 | } 70 | 71 | $iprestriction = null; 72 | 73 | if (isset($this->config->iprestriction) && !empty($this->config->iprestriction)) { 74 | if ($allowedips) { 75 | $iprestriction = $allowedips; 76 | } else { 77 | $iprestriction = getremoteaddr(null); 78 | } 79 | } 80 | 81 | return create_user_key( 82 | self::CORE_USER_KEY_MANAGER_SCRIPT, 83 | $userid, 84 | $userid, 85 | $iprestriction, 86 | $validuntil 87 | ); 88 | } 89 | 90 | /** 91 | * Delete all keys for a specific user. 92 | * 93 | * @param int $userid User ID. 94 | */ 95 | public function delete_keys($userid) { 96 | delete_user_key(self::CORE_USER_KEY_MANAGER_SCRIPT, $userid); 97 | } 98 | 99 | /** 100 | * Validates key and returns key data object if valid. 101 | * 102 | * @param string $keyvalue User key value. 103 | * 104 | * @return object Key object including userid property. 105 | * 106 | * @throws \moodle_exception If provided key is not valid. 107 | */ 108 | public function validate_key($keyvalue) { 109 | global $DB; 110 | 111 | $options = array( 112 | 'script' => self::CORE_USER_KEY_MANAGER_SCRIPT, 113 | 'value' => $keyvalue 114 | ); 115 | 116 | if (!$key = $DB->get_record('user_private_key', $options)) { 117 | throw new \moodle_exception('invalidkey'); 118 | } 119 | 120 | if (!empty($key->validuntil) && $key->validuntil < time()) { 121 | throw new \moodle_exception('expiredkey'); 122 | } 123 | 124 | $this->validate_ip_address($key); 125 | 126 | if (!$user = $DB->get_record('user', array('id' => $key->userid))) { 127 | throw new \moodle_exception('invaliduserid'); 128 | } 129 | return $key; 130 | } 131 | 132 | /** 133 | * Validates key IP address and returns true if valid. 134 | * 135 | * @param object $key Key object including userid property. 136 | * 137 | * @throws \moodle_exception If provided key is not valid. 138 | */ 139 | protected function validate_ip_address($key) { 140 | if (!$key->iprestriction) { 141 | return true; 142 | } 143 | 144 | $remoteaddr = getremoteaddr(null); 145 | 146 | if (empty($remoteaddr)) { 147 | throw new \moodle_exception('noip', 'auth_userkey'); 148 | } 149 | 150 | if (address_in_subnet($remoteaddr, $key->iprestriction)) { 151 | return true; 152 | } 153 | 154 | if (isset($this->config->ipwhitelist)) { 155 | $ips = explode(';', $this->config->ipwhitelist); 156 | foreach ($ips as $ip) { 157 | if (address_in_subnet($remoteaddr, $ip)) { 158 | return true; 159 | } 160 | } 161 | } 162 | 163 | throw new \moodle_exception('ipmismatch', 'error', '', null, "Remote address: $remoteaddr\nKey IP: $key->iprestriction"); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /classes/privacy/provider.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Privacy provider. 19 | * 20 | * @package auth_userkey 21 | * @author Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @copyright 2020 Catalyst IT 23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 | */ 25 | 26 | namespace auth_userkey\privacy; 27 | 28 | use core_privacy\local\metadata\null_provider; 29 | use core_privacy\local\legacy_polyfill; 30 | 31 | /** 32 | * Privacy provider. 33 | * 34 | * @copyright 2020 Catalyst IT 35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 | */ 37 | class provider implements null_provider { 38 | 39 | use legacy_polyfill; 40 | 41 | /** 42 | * Get the language string identifier with the component's language 43 | * file to explain why this plugin stores no data. 44 | * 45 | * @return string 46 | */ 47 | public static function _get_reason() { 48 | return 'privacy:metadata'; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /classes/userkey_manager_interface.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Key manager interface. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | namespace auth_userkey; 26 | 27 | /** 28 | * Interface userkey_manager_interface describes key manager behaviour. 29 | * 30 | * @package auth_userkey 31 | */ 32 | interface userkey_manager_interface { 33 | /** 34 | * Create a user key. 35 | * 36 | * @param int $userid User ID. 37 | * @param null|array $allowedips A list of allowed ips for this key. 38 | * 39 | * @return string Generated key. 40 | */ 41 | public function create_key($userid, $allowedips = null); 42 | 43 | /** 44 | * Delete all keys for a specific user. 45 | * 46 | * @param int $userid User ID. 47 | */ 48 | public function delete_keys($userid); 49 | 50 | /** 51 | * Validates key and returns key data object if valid. 52 | * 53 | * @param string $keyvalue Key value. 54 | * 55 | * @return object Key object including userid property. 56 | * 57 | * @throws \moodle_exception If provided key is not valid. 58 | */ 59 | public function validate_key($keyvalue); 60 | 61 | } 62 | -------------------------------------------------------------------------------- /db/access.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * User key auth method caps. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | $capabilities = array( 28 | 'auth/userkey:generatekey' => array( 29 | 'riskbitmask' => RISK_PERSONAL | RISK_SPAM | RISK_XSS , 30 | 31 | 'captype' => 'write', 32 | 'contextlevel' => CONTEXT_SYSTEM, 33 | 'archetypes' => array( 34 | ), 35 | ), 36 | ); 37 | -------------------------------------------------------------------------------- /db/services.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Web services for auth_userkey. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | $functions = array( 28 | 'auth_userkey_request_login_url' => array( 29 | 'classname' => 'auth_userkey_external', 30 | 'methodname' => 'request_login_url', 31 | 'classpath' => 'auth/userkey/externallib.php', 32 | 'description' => 'Return one time key based login URL', 33 | 'type' => 'write', 34 | 'capabilities' => 'auth/userkey:generatekey', 35 | ) 36 | ); 37 | 38 | $services = array( 39 | 'User key authentication web service' => array( 40 | 'functions' => array ('auth_userkey_request_login_url'), 41 | 'restrictedusers' => 1, 42 | 'enabled' => 1, 43 | ) 44 | ); 45 | -------------------------------------------------------------------------------- /db/upgrade.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Upgrade script. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2018 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | /** 26 | * Upgrade hook. 27 | * 28 | * @param string $oldversion Old version of the plugin. 29 | * @return bool 30 | */ 31 | function xmldb_auth_userkey_upgrade($oldversion) { 32 | global $DB; 33 | 34 | if ($oldversion < 2018050200) { 35 | // Confirm all previously created users. 36 | $DB->execute("UPDATE {user} SET confirmed=? WHERE auth=?", array(1, 'userkey')); 37 | upgrade_plugin_savepoint(true, 2018050200, 'auth', 'userkey'); 38 | } 39 | 40 | return true; 41 | } 42 | -------------------------------------------------------------------------------- /externallib.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Webservices for auth_userkey. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die(); 26 | 27 | require_once($CFG->libdir . "/externallib.php"); 28 | require_once($CFG->dirroot . "/webservice/lib.php"); 29 | require_once($CFG->dirroot . "/auth/userkey/auth.php"); 30 | 31 | /** 32 | * Webservices for auth_userkey. 33 | * 34 | * @package auth_userkey 35 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 36 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 37 | */ 38 | class auth_userkey_external extends external_api { 39 | 40 | /** 41 | * Return request_login_url webservice parameters. 42 | * 43 | * @return \external_function_parameters 44 | */ 45 | public static function request_login_url_parameters() { 46 | return new external_function_parameters( 47 | array( 48 | 'user' => new external_single_structure( 49 | get_auth_plugin('userkey')->get_request_login_url_user_parameters() 50 | ) 51 | ) 52 | ); 53 | } 54 | 55 | /** 56 | * Return login url array. 57 | * 58 | * @param array $user 59 | * 60 | * @return array 61 | * @throws \dml_exception 62 | * @throws \required_capability_exception 63 | * @throws \webservice_access_exception 64 | */ 65 | public static function request_login_url($user) { 66 | 67 | if (!is_enabled_auth('userkey')) { 68 | throw new webservice_access_exception(get_string('pluginisdisabled', 'auth_userkey')); 69 | } 70 | 71 | $context = context_system::instance(); 72 | require_capability('auth/userkey:generatekey', $context); 73 | 74 | $auth = get_auth_plugin('userkey'); 75 | $loginurl = $auth->get_login_url($user); 76 | 77 | return array( 78 | 'loginurl' => $loginurl, 79 | ); 80 | } 81 | 82 | /** 83 | * Describe request_login_url webservice return structure. 84 | * 85 | * @return \external_single_structure 86 | */ 87 | public static function request_login_url_returns() { 88 | return new external_single_structure( 89 | array( 90 | 'loginurl' => new external_value(PARAM_RAW, 'Login URL for a user to log in'), 91 | ) 92 | ); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /lang/en/auth_userkey.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Strings for auth_userkey. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | $string['pluginname'] = 'User key authentication'; 28 | $string['auth_userkeydescription'] = 'Log in to Moodle using one time user key.'; 29 | $string['mappingfield'] = 'Mapping field'; 30 | $string['mappingfield_desc'] = 'This user field will be used to find relevant user in the LMS.'; 31 | $string['iprestriction'] = 'IP restriction'; 32 | $string['iprestriction_desc'] = 'If enabled, a web call has to contain "ip" parameter when requesting login URL. 33 | A user has to have provided IP to be able to use a key to login to LMS.'; 34 | $string['ipwhitelist'] = 'Whitelist IP ranges'; 35 | $string['ipwhitelist_desc'] = "Ignore IP restrictions if the IP address the token was issued for or the login attempt comes from falls within any of these ranges. 36 | \nThis can happen when some users reach Moodle or the system issuing login tokens via a private network or DMZ. 37 | \nIf the route to either the system issuing tokens or this Moodle is via a private address range then set this value to 10.0.0.0/8;172.16.0.0/12;192.168.0.0/16"; 38 | $string['keylifetime'] = 'User key life time'; 39 | $string['keylifetime_desc'] = 'Life time in seconds of the each user login key.'; 40 | $string['incorrectkeylifetime'] = 'User key life time should be a number'; 41 | $string['createuser'] = 'Create user?'; 42 | $string['createuser_desc'] = 'If enabled, a new user will be created if fail to find one in LMS.'; 43 | $string['updateuser'] = 'Update user?'; 44 | $string['updateuser_desc'] = 'If enabled, users will be updated with the properties supplied when the webservice is called.'; 45 | $string['redirecturl'] = 'Logout redirect URL'; 46 | $string['redirecturl_desc'] = 'Optionally you can redirect users to this URL after they logged out from LMS.'; 47 | $string['incorrectredirecturl'] = 'You should provide valid URL'; 48 | $string['incorrectssourl'] = 'You should provide valid URL'; 49 | $string['userkey:generatekey'] = 'Generate login user key'; 50 | $string['pluginisdisabled'] = 'The userkey authentication plugin is disabled.'; 51 | $string['ssourl'] = 'URL of SSO host'; 52 | $string['ssourl_desc'] = 'URL of the SSO host to redirect users to. If defined users will be redirected here on login instead of the Moodle Login page'; 53 | $string['redirecterrordetected'] = 'Unsupported redirect to {$a} detected, execution terminated.'; 54 | $string['noip'] = 'Unable to fetch IP address of client.'; 55 | $string['privacy:metadata'] = 'User key authentication plugin does not store any personal data.'; 56 | $string['incorrectlogout'] = 'Incorrect logout request'; 57 | -------------------------------------------------------------------------------- /login.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Login page for auth_userkey. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(dirname(__FILE__) . '/../../config.php'); 26 | 27 | if (!is_enabled_auth('userkey')) { 28 | throw new moodle_exception(get_string('pluginisdisabled', 'auth_userkey')); 29 | } 30 | 31 | get_auth_plugin('userkey')->user_login_userkey(); 32 | -------------------------------------------------------------------------------- /logout.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Logout page for auth_userkey. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | require_once(dirname(__FILE__) . '/../../config.php'); 26 | 27 | if (!is_enabled_auth('userkey')) { 28 | throw new moodle_exception(get_string('pluginisdisabled', 'auth_userkey')); 29 | } 30 | 31 | get_auth_plugin('userkey')->user_logout_userkey(); 32 | -------------------------------------------------------------------------------- /pix/catalyst-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/catalyst/moodle-auth_userkey/9c9266a8264ca2dc7d0e91d8878f4383054f516e/pix/catalyst-logo.png -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Admin settings and defaults 19 | * 20 | * @package auth_userkey 21 | * @copyright 2017 Stephen Bourget 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | if ($ADMIN->fulltree) { 28 | $yesno = array(get_string('no'), get_string('yes')); 29 | $fields = get_auth_plugin('userkey')->get_allowed_mapping_fields(); 30 | 31 | $settings->add(new admin_setting_configselect('auth_userkey/mappingfield', 32 | new lang_string('mappingfield', 'auth_userkey'), 33 | new lang_string('mappingfield_desc', 'auth_userkey'), 0, $fields)); 34 | 35 | $settings->add(new admin_setting_configtext('auth_userkey/keylifetime', get_string('keylifetime', 'auth_userkey'), 36 | get_string('keylifetime_desc', 'auth_userkey', 'auth'), 37 | '60', PARAM_INT)); 38 | 39 | $settings->add(new admin_setting_configselect('auth_userkey/iprestriction', 40 | new lang_string('iprestriction', 'auth_userkey'), 41 | new lang_string('iprestriction_desc', 'auth_userkey'), 0, $yesno)); 42 | 43 | $settings->add(new admin_setting_configtext('auth_userkey/ipwhitelist', get_string('ipwhitelist', 'auth_userkey'), 44 | get_string('ipwhitelist_desc', 'auth_userkey', 'auth'), 45 | '', PARAM_TEXT)); 46 | 47 | $settings->add(new admin_setting_configtext('auth_userkey/redirecturl', get_string('redirecturl', 'auth_userkey'), 48 | get_string('redirecturl_desc', 'auth_userkey', 'auth'), 49 | '', PARAM_URL)); 50 | 51 | $settings->add(new admin_setting_configtext('auth_userkey/ssourl', get_string('ssourl', 'auth_userkey'), 52 | get_string('ssourl_desc', 'auth_userkey', 'auth'), 53 | '', PARAM_URL)); 54 | 55 | $settings->add(new admin_setting_configselect('auth_userkey/createuser', 56 | new lang_string('createuser', 'auth_userkey'), 57 | new lang_string('createuser_desc', 'auth_userkey'), 0, $yesno)); 58 | 59 | $settings->add(new admin_setting_configselect('auth_userkey/updateuser', 60 | new lang_string('updateuser', 'auth_userkey'), 61 | new lang_string('updateuser_desc', 'auth_userkey'), 0, $yesno)); 62 | 63 | // Display locking / mapping of profile fields. 64 | $authplugin = get_auth_plugin('userkey'); 65 | display_auth_lock_options($settings, $authplugin->authtype, 66 | $authplugin->userfields, get_string('auth_fieldlocks_help', 'auth'), false, false); 67 | } 68 | -------------------------------------------------------------------------------- /tests/auth_plugin_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace auth_userkey; 18 | 19 | use advanced_testcase; 20 | use auth_plugin_userkey; 21 | use stdClass; 22 | use invalid_parameter_exception; 23 | use moodle_exception; 24 | use external_value; 25 | 26 | /** 27 | * Tests for auth_plugin_userkey class. 28 | * 29 | * @covers \auth_plugin_userkey 30 | * 31 | * @package auth_userkey 32 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 33 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 | */ 35 | class auth_plugin_test extends advanced_testcase { 36 | /** 37 | * An instance of auth_plugin_userkey class. 38 | * @var auth_plugin_userkey 39 | */ 40 | protected $auth; 41 | 42 | /** 43 | * User object. 44 | * @var 45 | */ 46 | protected $user; 47 | 48 | /** 49 | * Path used for the redirection. 50 | * @var string 51 | */ 52 | const REDIRECTION_PATH = "/redirection"; 53 | 54 | /** 55 | * Initial set up. 56 | */ 57 | public function setUp(): void { 58 | global $CFG; 59 | 60 | require_once($CFG->libdir . "/externallib.php"); 61 | require_once($CFG->dirroot . '/auth/userkey/tests/fake_userkey_manager.php'); 62 | require_once($CFG->dirroot . '/auth/userkey/auth.php'); 63 | require_once($CFG->dirroot . '/user/lib.php'); 64 | 65 | parent::setUp(); 66 | 67 | $this->resetAfterTest(); 68 | $CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR; 69 | $this->auth = new auth_plugin_userkey(); 70 | $this->user = self::getDataGenerator()->create_user(); 71 | } 72 | 73 | /** 74 | * A helper function to create TestKey. 75 | * 76 | * @param array $record Key record. 77 | */ 78 | protected function create_user_private_key(array $record = []) { 79 | global $DB; 80 | 81 | $record = (object)$record; 82 | 83 | if (!isset($record->value)) { 84 | $record->value = 'TestKey'; 85 | } 86 | 87 | if (!isset($record->userid)) { 88 | $record->userid = $this->user->id; 89 | } 90 | 91 | if (!isset($record->userid)) { 92 | $record->instance = $this->user->id; 93 | } 94 | 95 | if (!isset($record->iprestriction)) { 96 | $record->iprestriction = null; 97 | } 98 | if (!isset($record->validuntil)) { 99 | $record->validuntil = time() + 300; 100 | } 101 | if (!isset($record->timecreated)) { 102 | $record->timecreated = time(); 103 | } 104 | 105 | $record->script = 'auth/userkey'; 106 | 107 | $DB->insert_record('user_private_key', $record); 108 | } 109 | 110 | /** 111 | * Test that users can't login using login form. 112 | */ 113 | public function test_users_can_not_login_using_login_form() { 114 | $user = new stdClass(); 115 | $user->auth = 'userkey'; 116 | $user->username = 'username'; 117 | $user->password = 'correctpassword'; 118 | 119 | self::getDataGenerator()->create_user($user); 120 | 121 | $this->assertFalse($this->auth->user_login('username', 'correctpassword')); 122 | $this->assertFalse($this->auth->user_login('username', 'incorrectpassword')); 123 | } 124 | 125 | /** 126 | * Test that the plugin doesn't allow to store users passwords. 127 | */ 128 | public function test_auth_plugin_does_not_allow_to_store_passwords() { 129 | $this->assertTrue($this->auth->prevent_local_passwords()); 130 | } 131 | 132 | /** 133 | * Test that the plugin is external. 134 | */ 135 | public function test_auth_plugin_is_external() { 136 | $this->assertFalse($this->auth->is_internal()); 137 | } 138 | 139 | /** 140 | * Test that the plugin doesn't allow users to change the passwords. 141 | */ 142 | public function test_auth_plugin_does_not_allow_to_change_passwords() { 143 | $this->assertFalse($this->auth->can_change_password()); 144 | } 145 | 146 | /** 147 | * Test that default mapping field gets returned correctly. 148 | */ 149 | public function test_get_default_mapping_field() { 150 | $expected = 'email'; 151 | $actual = $this->auth->get_mapping_field(); 152 | 153 | $this->assertEquals($expected, $actual); 154 | } 155 | 156 | /** 157 | * Test that logout page hook sets global redirect variable correctly. 158 | */ 159 | public function test_logoutpage_hook_sets_global_redirect_correctly() { 160 | global $redirect, $SESSION; 161 | 162 | $this->auth->logoutpage_hook(); 163 | $this->assertEquals('', $redirect); 164 | 165 | $SESSION->userkey = true; 166 | $this->auth = new auth_plugin_userkey(); 167 | $this->auth->logoutpage_hook(); 168 | $this->assertEquals('', $redirect); 169 | 170 | unset($SESSION->userkey); 171 | set_config('redirecturl', 'http://example.com', 'auth_userkey'); 172 | $this->auth = new auth_plugin_userkey(); 173 | $this->auth->logoutpage_hook(); 174 | $this->assertEquals('', $redirect); 175 | 176 | $SESSION->userkey = true; 177 | set_config('redirecturl', 'http://example.com', 'auth_userkey'); 178 | $this->auth = new auth_plugin_userkey(); 179 | $this->auth->logoutpage_hook(); 180 | $this->assertEquals('http://example.com', $redirect); 181 | } 182 | 183 | /** 184 | * Test that configured mapping field gets returned correctly. 185 | */ 186 | public function test_get_mapping_field() { 187 | set_config('mappingfield', 'username', 'auth_userkey'); 188 | $this->auth = new auth_plugin_userkey(); 189 | 190 | $expected = 'username'; 191 | $actual = $this->auth->get_mapping_field(); 192 | 193 | $this->assertEquals($expected, $actual); 194 | } 195 | 196 | /** 197 | * Test that auth plugin throws correct exception if default mapping field is not provided. 198 | */ 199 | public function test_throwing_exception_if_default_mapping_field_is_not_provided() { 200 | $user = array(); 201 | $this->expectException(invalid_parameter_exception::class); 202 | $this->expectExceptionMessage('Invalid parameter value detected (Required field "email" is not set or empty.)'); 203 | 204 | $actual = $this->auth->get_login_url($user); 205 | } 206 | 207 | /** 208 | * Test that auth plugin throws correct exception if username mapping field is not provided, but set in configs. 209 | */ 210 | public function test_throwing_exception_if_mapping_field_username_is_not_provided() { 211 | $user = array(); 212 | set_config('mappingfield', 'username', 'auth_userkey'); 213 | $this->auth = new auth_plugin_userkey(); 214 | 215 | $this->expectException(invalid_parameter_exception::class); 216 | $this->expectExceptionMessage('Invalid parameter value detected (Required field "username" is not set or empty.)'); 217 | 218 | $actual = $this->auth->get_login_url($user); 219 | } 220 | 221 | /** 222 | * Test that auth plugin throws correct exception if idnumber mapping field is not provided, but set in configs. 223 | */ 224 | public function test_throwing_exception_if_mapping_field_idnumber_is_not_provided() { 225 | $user = array(); 226 | set_config('mappingfield', 'idnumber', 'auth_userkey'); 227 | $this->auth = new auth_plugin_userkey(); 228 | 229 | $this->expectException(invalid_parameter_exception::class); 230 | $this->expectExceptionMessage('Invalid parameter value detected (Required field "idnumber" is not set or empty.)'); 231 | 232 | $actual = $this->auth->get_login_url($user); 233 | } 234 | 235 | /** 236 | * Test that auth plugin throws correct exception if we trying to request not existing user. 237 | */ 238 | public function test_throwing_exception_if_user_is_not_exist() { 239 | $user = array(); 240 | $user['email'] = 'notexists@test.com'; 241 | 242 | $this->expectException(invalid_parameter_exception::class); 243 | $this->expectExceptionMessage('Invalid parameter value detected (User is not exist)'); 244 | $actual = $this->auth->get_login_url($user); 245 | } 246 | 247 | /** 248 | * Test that auth plugin throws correct exception if we trying to request user, 249 | * but ip field is not set and iprestriction is enabled. 250 | */ 251 | public function test_throwing_exception_if_iprestriction_is_enabled_but_ip_is_missing_in_data() { 252 | $user = array(); 253 | $user['email'] = 'exists@test.com'; 254 | set_config('iprestriction', true, 'auth_userkey'); 255 | $this->auth = new auth_plugin_userkey(); 256 | 257 | $this->expectException(invalid_parameter_exception::class); 258 | $this->expectExceptionMessage('Invalid parameter value detected (Required parameter "ip" is not set.)'); 259 | 260 | $actual = $this->auth->get_login_url($user); 261 | } 262 | 263 | /** 264 | * Test that we can request a user provided user data as an array. 265 | */ 266 | public function test_return_correct_login_url_if_user_is_array() { 267 | global $CFG; 268 | 269 | $user = array(); 270 | $user['username'] = 'username'; 271 | $user['email'] = 'exists@test.com'; 272 | 273 | self::getDataGenerator()->create_user($user); 274 | 275 | $userkeymanager = new fake_userkey_manager(); 276 | $this->auth->set_userkey_manager($userkeymanager); 277 | 278 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; 279 | $actual = $this->auth->get_login_url($user); 280 | 281 | $this->assertEquals($expected, $actual); 282 | } 283 | 284 | /** 285 | * Test that we can request a user provided user data as an object. 286 | */ 287 | public function test_return_correct_login_url_if_user_is_object() { 288 | global $CFG; 289 | 290 | $user = new stdClass(); 291 | $user->username = 'username'; 292 | $user->email = 'exists@test.com'; 293 | 294 | self::getDataGenerator()->create_user($user); 295 | 296 | $userkeymanager = new fake_userkey_manager(); 297 | $this->auth->set_userkey_manager($userkeymanager); 298 | 299 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; 300 | $actual = $this->auth->get_login_url($user); 301 | 302 | $this->assertEquals($expected, $actual); 303 | } 304 | 305 | /** 306 | * Test that we can request a user provided user data as an object. 307 | */ 308 | public function test_return_correct_login_url_if_iprestriction_is_enabled_and_data_is_correct() { 309 | global $CFG; 310 | 311 | $user = new stdClass(); 312 | $user->username = 'username'; 313 | $user->email = 'exists@test.com'; 314 | $user->ip = '192.168.1.1'; 315 | 316 | self::getDataGenerator()->create_user($user); 317 | 318 | $userkeymanager = new fake_userkey_manager(); 319 | $this->auth->set_userkey_manager($userkeymanager); 320 | 321 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; 322 | $actual = $this->auth->get_login_url($user); 323 | 324 | $this->assertEquals($expected, $actual); 325 | } 326 | 327 | /** 328 | * Test that we can request a key for a new user. 329 | */ 330 | public function test_return_correct_login_url_and_create_new_user() { 331 | global $CFG, $DB; 332 | 333 | set_config('createuser', true, 'auth_userkey'); 334 | $this->auth = new auth_plugin_userkey(); 335 | 336 | $userkeymanager = new fake_userkey_manager(); 337 | $this->auth->set_userkey_manager($userkeymanager); 338 | 339 | $user = new stdClass(); 340 | $user->username = 'username'; 341 | $user->email = 'username@test.com'; 342 | $user->firstname = 'user'; 343 | $user->lastname = 'name'; 344 | $user->ip = '192.168.1.1'; 345 | 346 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; 347 | $actual = $this->auth->get_login_url($user); 348 | 349 | $this->assertEquals($expected, $actual); 350 | 351 | $userrecord = $DB->get_record('user', ['username' => 'username']); 352 | $this->assertEquals($user->email, $userrecord->email); 353 | $this->assertEquals($user->firstname, $userrecord->firstname); 354 | $this->assertEquals($user->lastname, $userrecord->lastname); 355 | $this->assertEquals(1, $userrecord->confirmed); 356 | $this->assertEquals('userkey', $userrecord->auth); 357 | } 358 | 359 | /** 360 | * Test that we can request a key for a new user. 361 | */ 362 | public function test_missing_data_to_create_user() { 363 | global $CFG, $DB; 364 | 365 | set_config('createuser', true, 'auth_userkey'); 366 | $this->auth = new auth_plugin_userkey(); 367 | 368 | $userkeymanager = new fake_userkey_manager(); 369 | $this->auth->set_userkey_manager($userkeymanager); 370 | 371 | $user = new stdClass(); 372 | $user->email = 'username@test.com'; 373 | $user->ip = '192.168.1.1'; 374 | 375 | $this->expectException(invalid_parameter_exception::class); 376 | $this->expectExceptionMessage('Unable to create user, missing value(s): username,firstname,lastname'); 377 | 378 | $this->auth->get_login_url($user); 379 | } 380 | 381 | /** 382 | * Test that when we attempt to create a new user duplicate usernames are caught. 383 | */ 384 | public function test_create_refuse_duplicate_username() { 385 | set_config('createuser', true, 'auth_userkey'); 386 | $this->auth = new auth_plugin_userkey(); 387 | 388 | $userkeymanager = new fake_userkey_manager(); 389 | $this->auth->set_userkey_manager($userkeymanager); 390 | 391 | $originaluser = new stdClass(); 392 | $originaluser->username = 'username'; 393 | $originaluser->email = 'username@test.com'; 394 | $originaluser->firstname = 'user'; 395 | $originaluser->lastname = 'name'; 396 | $originaluser->city = 'brighton'; 397 | $originaluser->ip = '192.168.1.1'; 398 | 399 | self::getDataGenerator()->create_user($originaluser); 400 | 401 | $duplicateuser = clone ($originaluser); 402 | $duplicateuser->email = 'duplicateuser@test.com'; 403 | 404 | $this->expectException(invalid_parameter_exception::class); 405 | $this->expectExceptionMessage('Username already exists: username'); 406 | 407 | $this->auth->get_login_url($duplicateuser); 408 | } 409 | 410 | /** 411 | * Test that when we attempt to create a new user duplicate emails are caught. 412 | */ 413 | public function test_create_refuse_duplicate_email() { 414 | set_config('createuser', true, 'auth_userkey'); 415 | set_config('mappingfield', 'username', 'auth_userkey'); 416 | $this->auth = new auth_plugin_userkey(); 417 | 418 | $userkeymanager = new fake_userkey_manager(); 419 | $this->auth->set_userkey_manager($userkeymanager); 420 | 421 | $originaluser = new stdClass(); 422 | $originaluser->username = 'username'; 423 | $originaluser->email = 'username@test.com'; 424 | $originaluser->firstname = 'user'; 425 | $originaluser->lastname = 'name'; 426 | $originaluser->city = 'brighton'; 427 | $originaluser->ip = '192.168.1.1'; 428 | 429 | self::getDataGenerator()->create_user($originaluser); 430 | 431 | $duplicateuser = clone ($originaluser); 432 | $duplicateuser->username = 'duplicateuser'; 433 | 434 | $this->expectException(invalid_parameter_exception::class); 435 | $this->expectExceptionMessage('Email address already exists: username@test.com'); 436 | 437 | $this->auth->get_login_url($duplicateuser); 438 | } 439 | 440 | /** 441 | * Test that we can request a key for an existing user and update their details. 442 | */ 443 | public function test_return_correct_login_url_and_update_user() { 444 | global $CFG, $DB; 445 | 446 | set_config('updateuser', true, 'auth_userkey'); 447 | $this->auth = new auth_plugin_userkey(); 448 | 449 | $userkeymanager = new fake_userkey_manager(); 450 | $this->auth->set_userkey_manager($userkeymanager); 451 | 452 | $originaluser = new stdClass(); 453 | $originaluser->username = 'username'; 454 | $originaluser->email = 'username@test.com'; 455 | $originaluser->firstname = 'user'; 456 | $originaluser->lastname = 'name'; 457 | $originaluser->city = 'brighton'; 458 | $originaluser->ip = '192.168.1.1'; 459 | 460 | self::getDataGenerator()->create_user($originaluser); 461 | 462 | $user = new stdClass(); 463 | $user->username = 'usernamechanged'; 464 | $user->email = 'username@test.com'; 465 | $user->firstname = 'userchanged'; 466 | $user->lastname = 'namechanged'; 467 | $user->ip = '192.168.1.1'; 468 | 469 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=FaKeKeyFoRtEsTiNg'; 470 | $actual = $this->auth->get_login_url($user); 471 | 472 | $this->assertEquals($expected, $actual); 473 | 474 | $userrecord = $DB->get_record('user', ['email' => $user->email]); 475 | $this->assertEquals($user->username, $userrecord->username); 476 | $this->assertEquals($user->firstname, $userrecord->firstname); 477 | $this->assertEquals($user->lastname, $userrecord->lastname); 478 | $this->assertEquals($originaluser->city, $userrecord->city); 479 | $this->assertEquals('userkey', $userrecord->auth); 480 | } 481 | 482 | /** 483 | * Test that when we attempt to update a user duplicate emails are caught. 484 | */ 485 | public function test_update_refuse_duplicate_email() { 486 | set_config('updateuser', true, 'auth_userkey'); 487 | set_config('mappingfield', 'username', 'auth_userkey'); 488 | $this->auth = new auth_plugin_userkey(); 489 | 490 | $userkeymanager = new fake_userkey_manager(); 491 | $this->auth->set_userkey_manager($userkeymanager); 492 | 493 | self::getDataGenerator()->create_user(['email' => 'trytoduplicate@test.com']); 494 | self::getDataGenerator()->create_user(['username' => 'username']); 495 | 496 | $originaluser = new stdClass(); 497 | $originaluser->username = 'username'; 498 | $originaluser->email = 'trytoduplicate@test.com'; 499 | $originaluser->firstname = 'user'; 500 | $originaluser->lastname = 'name'; 501 | $originaluser->city = 'brighton'; 502 | $originaluser->ip = '192.168.1.1'; 503 | 504 | $this->expectException(invalid_parameter_exception::class); 505 | $this->expectExceptionMessage('Email address already exists: trytoduplicate@test.com'); 506 | 507 | $this->auth->get_login_url($originaluser); 508 | } 509 | 510 | /** 511 | * Test that when we attempt to update a user duplicate usernames are caught. 512 | */ 513 | public function test_update_refuse_duplicate_username() { 514 | set_config('updateuser', true, 'auth_userkey'); 515 | $this->auth = new auth_plugin_userkey(); 516 | 517 | $userkeymanager = new fake_userkey_manager(); 518 | $this->auth->set_userkey_manager($userkeymanager); 519 | 520 | self::getDataGenerator()->create_user(['username' => 'trytoduplicate']); 521 | self::getDataGenerator()->create_user(['email' => 'username@test.com']); 522 | 523 | $originaluser = new stdClass(); 524 | $originaluser->username = 'trytoduplicate'; 525 | $originaluser->email = 'username@test.com'; 526 | $originaluser->firstname = 'user'; 527 | $originaluser->lastname = 'name'; 528 | $originaluser->city = 'brighton'; 529 | $originaluser->ip = '192.168.1.1'; 530 | 531 | $this->expectException(invalid_parameter_exception::class); 532 | $this->expectExceptionMessage('Username already exists: trytoduplicate'); 533 | 534 | $this->auth->get_login_url($originaluser); 535 | } 536 | 537 | /** 538 | * Test that we can get login url if we do not use fake keymanager. 539 | */ 540 | public function test_return_correct_login_url_if_user_is_object_using_default_keymanager() { 541 | global $DB, $CFG; 542 | 543 | $user = array(); 544 | $user['username'] = 'username'; 545 | $user['email'] = 'exists@test.com'; 546 | 547 | $user = self::getDataGenerator()->create_user($user); 548 | 549 | create_user_key('auth/userkey', $user->id); 550 | create_user_key('auth/userkey', $user->id); 551 | create_user_key('auth/userkey', $user->id); 552 | $keys = $DB->get_records('user_private_key', array('userid' => $user->id)); 553 | 554 | $this->assertEquals(3, count($keys)); 555 | 556 | $actual = $this->auth->get_login_url($user); 557 | 558 | $keys = $DB->get_records('user_private_key', array('userid' => $user->id)); 559 | $this->assertEquals(1, count($keys)); 560 | 561 | $actualkey = $DB->get_record('user_private_key', array('userid' => $user->id)); 562 | 563 | $expected = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 564 | 565 | $this->assertEquals($expected, $actual); 566 | } 567 | 568 | /** 569 | * Test that we can return correct allowed mapping fields. 570 | */ 571 | public function test_get_allowed_mapping_fields_list() { 572 | $expected = array( 573 | 'username' => 'Username', 574 | 'email' => 'Email address', 575 | 'idnumber' => 'ID number', 576 | ); 577 | 578 | $actual = $this->auth->get_allowed_mapping_fields(); 579 | 580 | $this->assertEquals($expected, $actual); 581 | } 582 | 583 | /** 584 | * Test that we can get correct request parameters based on the plugin configuration. 585 | */ 586 | public function test_get_request_login_url_user_parameters_based_on_plugin_config() { 587 | // Check email as it should be set by default. 588 | $expected = array( 589 | 'email' => new external_value( 590 | PARAM_EMAIL, 591 | 'A valid email address' 592 | ), 593 | ); 594 | 595 | $actual = $this->auth->get_request_login_url_user_parameters(); 596 | $this->assertEquals($expected, $actual); 597 | 598 | // Check username. 599 | set_config('mappingfield', 'username', 'auth_userkey'); 600 | $this->auth = new auth_plugin_userkey(); 601 | 602 | $expected = array( 603 | 'username' => new external_value( 604 | PARAM_USERNAME, 605 | 'Username' 606 | ), 607 | ); 608 | 609 | $actual = $this->auth->get_request_login_url_user_parameters(); 610 | $this->assertEquals($expected, $actual); 611 | 612 | // Check idnumber. 613 | set_config('mappingfield', 'idnumber', 'auth_userkey'); 614 | $this->auth = new auth_plugin_userkey(); 615 | 616 | $expected = array( 617 | 'idnumber' => new external_value( 618 | PARAM_RAW, 619 | 'An arbitrary ID code number perhaps from the institution' 620 | ), 621 | ); 622 | 623 | $actual = $this->auth->get_request_login_url_user_parameters(); 624 | $this->assertEquals($expected, $actual); 625 | 626 | // Check some junk field name. 627 | set_config('mappingfield', 'junkfield', 'auth_userkey'); 628 | $this->auth = new auth_plugin_userkey(); 629 | 630 | $expected = array(); 631 | 632 | $actual = $this->auth->get_request_login_url_user_parameters(); 633 | $this->assertEquals($expected, $actual); 634 | 635 | // Check IP if iprestriction disabled. 636 | set_config('iprestriction', false, 'auth_userkey'); 637 | $this->auth = new auth_plugin_userkey(); 638 | $expected = array(); 639 | $actual = $this->auth->get_request_login_url_user_parameters(); 640 | $this->assertEquals($expected, $actual); 641 | 642 | // Check IP if iprestriction enabled. 643 | set_config('iprestriction', true, 'auth_userkey'); 644 | $this->auth = new auth_plugin_userkey(); 645 | $expected = array( 646 | 'ip' => new external_value( 647 | PARAM_HOST, 648 | 'User IP address' 649 | ), 650 | ); 651 | $actual = $this->auth->get_request_login_url_user_parameters(); 652 | $this->assertEquals($expected, $actual); 653 | 654 | // Check IP if createuser enabled. 655 | set_config('createuser', true, 'auth_userkey'); 656 | $this->auth = new auth_plugin_userkey(); 657 | $expected = array( 658 | 'ip' => new external_value(PARAM_HOST, 'User IP address'), 659 | 'firstname' => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL), 660 | 'lastname' => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL), 661 | 'email' => new external_value(PARAM_RAW_TRIMMED, 'A valid and unique email address', VALUE_OPTIONAL), 662 | 'username' => new external_value(PARAM_USERNAME, 'A valid and unique username', VALUE_OPTIONAL), 663 | ); 664 | $actual = $this->auth->get_request_login_url_user_parameters(); 665 | $this->assertEquals($expected, $actual); 666 | set_config('createuser', false, 'auth_userkey'); 667 | 668 | // Check IP if updateuser enabled. 669 | set_config('updateuser', true, 'auth_userkey'); 670 | $this->auth = new auth_plugin_userkey(); 671 | $expected = array( 672 | 'ip' => new external_value(PARAM_HOST, 'User IP address'), 673 | 'firstname' => new external_value(PARAM_NOTAGS, 'The first name(s) of the user', VALUE_OPTIONAL), 674 | 'lastname' => new external_value(PARAM_NOTAGS, 'The family name of the user', VALUE_OPTIONAL), 675 | 'email' => new external_value(PARAM_RAW_TRIMMED, 'A valid and unique email address', VALUE_OPTIONAL), 676 | 'username' => new external_value(PARAM_USERNAME, 'A valid and unique username', VALUE_OPTIONAL), 677 | ); 678 | $actual = $this->auth->get_request_login_url_user_parameters(); 679 | $this->assertEquals($expected, $actual); 680 | set_config('updateuser', false, 'auth_userkey'); 681 | } 682 | 683 | /** 684 | * Data provider for testing URL validation functions. 685 | * 686 | * @return array First element URL, the second URL is error message. Empty error massage means no errors. 687 | */ 688 | public function url_data_provider() { 689 | return array( 690 | array('', ''), 691 | array('http://google.com/', ''), 692 | array('https://google.com', ''), 693 | array('http://some.very.long.and.silly.domain/with/a/path/', ''), 694 | array('http://0.255.1.1/numericip.php', ''), 695 | array('http://0.255.1.1/numericip.php?test=1&id=2', ''), 696 | array('/just/a/path', 'You should provide valid URL'), 697 | array('random string', 'You should provide valid URL'), 698 | array(123456, 'You should provide valid URL'), 699 | array('php://google.com', 'You should provide valid URL'), 700 | ); 701 | } 702 | 703 | /** 704 | * Test required parameter exception gets thrown id try to login, but key is not set. 705 | */ 706 | public function test_required_parameter_exception_thrown_if_key_not_set() { 707 | $this->expectException(moodle_exception::class); 708 | $this->expectExceptionMessage('A required parameter (key) was missing'); 709 | 710 | $this->auth->user_login_userkey(); 711 | } 712 | 713 | /** 714 | * Test that incorrect key exception gets thrown if a key is incorrect. 715 | */ 716 | public function test_invalid_key_exception_thrown_if_invalid_key() { 717 | $this->expectException(moodle_exception::class); 718 | $this->expectExceptionMessage('Incorrect key'); 719 | 720 | $_POST['key'] = 'InvalidKey'; 721 | $this->auth->user_login_userkey(); 722 | } 723 | 724 | /** 725 | * Test that expired key exception gets thrown if a key is expired. 726 | */ 727 | public function test_expired_key_exception_thrown_if_expired_key() { 728 | $this->create_user_private_key(['validuntil' => time() - 3000]); 729 | 730 | $this->expectException(moodle_exception::class); 731 | $this->expectExceptionMessage('Expired key'); 732 | 733 | $_POST['key'] = 'TestKey'; 734 | $this->auth->user_login_userkey(); 735 | } 736 | 737 | /** 738 | * Test that IP address mismatch exception gets thrown if incorrect IP. 739 | */ 740 | public function test_ipmismatch_exception_thrown_if_ip_is_incorrect() { 741 | $this->create_user_private_key(['iprestriction' => '192.168.1.1']); 742 | 743 | $_POST['key'] = 'TestKey'; 744 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.2'; 745 | 746 | $this->expectException(moodle_exception::class); 747 | $this->expectExceptionMessage('Client IP address mismatch'); 748 | 749 | $this->auth->user_login_userkey(); 750 | } 751 | 752 | /** 753 | * Test that IP address mismatch exception gets thrown if incorrect IP and outside whitelist. 754 | */ 755 | public function test_ipmismatch_exception_thrown_if_ip_is_outside_whitelist() { 756 | set_config('ipwhitelist', '10.0.0.0/8;172.16.0.0/12;192.168.0.0/16', 'auth_userkey'); 757 | $this->create_user_private_key(['iprestriction' => '192.161.1.1']); 758 | 759 | $_POST['key'] = 'TestKey'; 760 | $_SERVER['HTTP_CLIENT_IP'] = '192.161.1.2'; 761 | 762 | $this->expectException(moodle_exception::class); 763 | $this->expectExceptionMessage('Client IP address mismatch'); 764 | 765 | $this->auth->user_login_userkey(); 766 | } 767 | 768 | /** 769 | * Test that IP address mismatch exception gets thrown if user id is incorrect. 770 | */ 771 | public function test_invalid_user_exception_thrown_if_user_is_invalid() { 772 | $this->create_user_private_key([ 773 | 'userid' => 777, 774 | 'instance' => 777, 775 | 'iprestriction' => '192.168.1.1', 776 | ]); 777 | 778 | $_POST['key'] = 'TestKey'; 779 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 780 | 781 | $this->expectException(moodle_exception::class); 782 | $this->expectExceptionMessage('Invalid user'); 783 | 784 | $this->auth->user_login_userkey(); 785 | } 786 | 787 | /** 788 | * Test that key gets removed after a user logged in. 789 | */ 790 | public function test_that_key_gets_removed_after_user_logged_in() { 791 | global $DB; 792 | 793 | $this->create_user_private_key([ 794 | 'value' => 'RemoveKey', 795 | 'iprestriction' => '192.168.1.1', 796 | ]); 797 | 798 | $_POST['key'] = 'RemoveKey'; 799 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 800 | 801 | try { 802 | // Using @ is the only way to test this. Thanks moodle! 803 | @$this->auth->user_login_userkey(); 804 | } catch (moodle_exception $e) { 805 | $keyexists = $DB->record_exists('user_private_key', array('value' => 'RemoveKey')); 806 | $this->assertFalse($keyexists); 807 | } 808 | } 809 | 810 | /** 811 | * Test that a user logs in and gets redirected correctly. 812 | */ 813 | public function test_that_user_logged_in_and_redirected() { 814 | global $CFG; 815 | 816 | $this->create_user_private_key(); 817 | $CFG->wwwroot = 'http://www.example.com/moodle'; 818 | $_POST['key'] = 'TestKey'; 819 | 820 | $this->expectException(moodle_exception::class); 821 | $this->expectExceptionMessage('Unsupported redirect to http://www.example.com/moodle detected, execution terminated'); 822 | 823 | @$this->auth->user_login_userkey(); 824 | } 825 | 826 | /** 827 | * Test that a user logs in correctly. 828 | */ 829 | public function test_that_user_logged_in_correctly() { 830 | global $USER, $SESSION; 831 | 832 | $this->create_user_private_key(); 833 | 834 | $_POST['key'] = 'TestKey'; 835 | 836 | try { 837 | // Using @ is the only way to test this. Thanks moodle! 838 | @$this->auth->user_login_userkey(); 839 | } catch (moodle_exception $e) { 840 | $this->assertEquals($this->user->id, $USER->id); 841 | $this->assertSame(sesskey(), $USER->sesskey); 842 | $this->assertObjectHasAttribute('userkey', $SESSION); 843 | } 844 | } 845 | 846 | /** 847 | * Test that a user gets redirected to internal wantsurl URL successful log in. 848 | */ 849 | public function test_that_user_gets_redirected_to_internal_wantsurl() { 850 | $this->create_user_private_key(); 851 | $_POST['key'] = 'TestKey'; 852 | $_POST['wantsurl'] = '/course/index.php?id=12&key=134'; 853 | 854 | $this->expectException(moodle_exception::class); 855 | $this->expectExceptionMessage('Unsupported redirect to /course/index.php?id=12&key=134 detected, execution terminated'); 856 | 857 | // Using @ is the only way to test this. Thanks moodle! 858 | @$this->auth->user_login_userkey(); 859 | } 860 | 861 | /** 862 | * Test that a user gets redirected to external wantsurl URL successful log in. 863 | */ 864 | public function test_that_user_gets_redirected_to_external_wantsurl() { 865 | $this->create_user_private_key(); 866 | 867 | $_POST['key'] = 'TestKey'; 868 | $_POST['wantsurl'] = 'http://test.com/course/index.php?id=12&key=134'; 869 | 870 | $this->expectException(moodle_exception::class); 871 | $this->expectExceptionMessage('Unsupported redirect to http://test.com/course/index.php?id=12&key=134 detected, execution terminated'); 872 | 873 | // Using @ is the only way to test this. Thanks moodle! 874 | @$this->auth->user_login_userkey(); 875 | } 876 | 877 | /** 878 | * Test that login hook redirects a user if skipsso not set and ssourl is set. 879 | */ 880 | public function test_loginpage_hook_redirects_if_skipsso_not_set_and_ssourl_set() { 881 | global $SESSION; 882 | 883 | $SESSION->enrolkey_skipsso = 0; 884 | set_config('ssourl', 'http://google.com', 'auth_userkey'); 885 | $this->auth = new auth_plugin_userkey(); 886 | 887 | $this->expectException(moodle_exception::class); 888 | $this->expectExceptionMessage('Unsupported redirect to http://google.com detected, execution terminated.'); 889 | 890 | $this->auth->loginpage_hook(); 891 | } 892 | 893 | /** 894 | * Test that login hook does not redirect a user if skipsso not set and ssourl is not set. 895 | */ 896 | public function test_loginpage_hook_does_not_redirect_if_skipsso_not_set_and_ssourl_not_set() { 897 | global $SESSION; 898 | 899 | $SESSION->enrolkey_skipsso = 0; 900 | set_config('ssourl', '', 'auth_userkey'); 901 | $this->auth = new auth_plugin_userkey(); 902 | 903 | $this->assertTrue($this->auth->loginpage_hook()); 904 | } 905 | 906 | /** 907 | * Test that login hook does not redirect a user if skipsso is set and ssourl is not set. 908 | */ 909 | public function test_loginpage_hook_does_not_redirect_if_skipsso_set_and_ssourl_not_set() { 910 | global $SESSION; 911 | 912 | $SESSION->enrolkey_skipsso = 1; 913 | set_config('ssourl', '', 'auth_userkey'); 914 | $this->auth = new auth_plugin_userkey(); 915 | 916 | $this->assertTrue($this->auth->loginpage_hook()); 917 | } 918 | 919 | /** 920 | * Test that pre login hook redirects a user if skipsso not set and ssourl is set. 921 | */ 922 | public function test_pre_loginpage_hook_redirects_if_skipsso_not_set_and_ssourl_set() { 923 | global $SESSION; 924 | 925 | $SESSION->enrolkey_skipsso = 0; 926 | set_config('ssourl', 'http://google.com', 'auth_userkey'); 927 | $this->auth = new auth_plugin_userkey(); 928 | 929 | $this->expectException(moodle_exception::class); 930 | $this->expectExceptionMessage('Unsupported redirect to http://google.com detected, execution terminated.'); 931 | 932 | $this->auth->pre_loginpage_hook(); 933 | } 934 | 935 | /** 936 | * Test that pre login hook does not redirect a user if skipsso is not set and ssourl is not set. 937 | */ 938 | public function test_pre_loginpage_hook_does_not_redirect_if_skipsso_not_set_and_ssourl_not_set() { 939 | global $SESSION; 940 | 941 | $SESSION->enrolkey_skipsso = 0; 942 | set_config('ssourl', '', 'auth_userkey'); 943 | $this->auth = new auth_plugin_userkey(); 944 | 945 | $this->assertTrue($this->auth->pre_loginpage_hook()); 946 | } 947 | 948 | /** 949 | * Test that login page hook does not redirect a user if skipsso is set and ssourl is not set. 950 | */ 951 | public function test_pre_loginpage_hook_does_not_redirect_if_skipsso_set_and_ssourl_not_set() { 952 | global $SESSION; 953 | 954 | $SESSION->enrolkey_skipsso = 1; 955 | set_config('ssourl', '', 'auth_userkey'); 956 | $this->auth = new auth_plugin_userkey(); 957 | 958 | $this->assertTrue($this->auth->pre_loginpage_hook()); 959 | } 960 | 961 | /** 962 | * Test that if one user logged, he will be logged out before a new one is authorised. 963 | */ 964 | public function test_that_different_authorised_user_is_logged_out_and_new_one_logged_in() { 965 | global $USER, $SESSION; 966 | 967 | $user = $this->getDataGenerator()->create_user(); 968 | $this->setUser($user); 969 | $this->assertEquals($USER->id, $user->id); 970 | 971 | $this->create_user_private_key(); 972 | 973 | $_POST['key'] = 'TestKey'; 974 | 975 | try { 976 | // Using @ is the only way to test this. Thanks moodle! 977 | @$this->auth->user_login_userkey(); 978 | } catch (moodle_exception $e) { 979 | $this->assertEquals($this->user->id, $USER->id); 980 | $this->assertSame(sesskey(), $USER->sesskey); 981 | $this->assertObjectHasAttribute('userkey', $SESSION); 982 | } 983 | } 984 | 985 | /** 986 | * Test that authorised user gets logged out when trying to logged in with invalid key. 987 | */ 988 | public function test_if_invalid_key_authorised_user_gets_logged_out() { 989 | global $USER, $SESSION; 990 | 991 | $user = $this->getDataGenerator()->create_user(); 992 | $this->setUser($user); 993 | $this->assertEquals($USER->id, $user->id); 994 | 995 | $this->create_user_private_key(); 996 | 997 | $_POST['key'] = 'Incorrect Key'; 998 | 999 | try { 1000 | // Using @ is the only way to test this. Thanks moodle! 1001 | @$this->auth->user_login_userkey(); 1002 | } catch (moodle_exception $e) { 1003 | $this->assertEquals('Incorrect key', $e->getMessage()); 1004 | $this->assertEmpty($USER->id); 1005 | $this->assertEquals(new stdClass(), $SESSION); 1006 | } 1007 | } 1008 | 1009 | /** 1010 | * Test if a user is logged in and tries to log in again it stays logged in. 1011 | */ 1012 | public function test_that_already_logged_in_user_stays_logged_in() { 1013 | global $DB, $USER, $SESSION; 1014 | 1015 | $this->setUser($this->user); 1016 | $this->assertEquals($USER->id, $this->user->id); 1017 | 1018 | $this->create_user_private_key(); 1019 | 1020 | $_POST['key'] = 'TestKey'; 1021 | 1022 | try { 1023 | // Using @ is the only way to test this. Thanks moodle! 1024 | @$this->auth->user_login_userkey(); 1025 | } catch (moodle_exception $e) { 1026 | $this->assertEquals($this->user->id, $USER->id); 1027 | $this->assertSame(sesskey(), $USER->sesskey); 1028 | $this->assertObjectNotHasAttribute('userkey', $SESSION); 1029 | $keyexists = $DB->record_exists('user_private_key', array('value' => 'TestKey')); 1030 | $this->assertFalse($keyexists); 1031 | } 1032 | } 1033 | 1034 | /** 1035 | * Test when try to logout, but required return is not set. 1036 | */ 1037 | public function test_user_logout_userkey_when_required_return_not_set() { 1038 | $this->expectException(moodle_exception::class); 1039 | $this->expectExceptionMessage('A required parameter (return) was missing'); 1040 | 1041 | $this->auth->user_logout_userkey(); 1042 | } 1043 | 1044 | /** 1045 | * Test when try to logout, but user is not logged in. 1046 | */ 1047 | public function test_user_logout_userkey_when_user_is_not_logged_in() { 1048 | $_POST['return'] = self::REDIRECTION_PATH; 1049 | 1050 | $this->expectException(moodle_exception::class); 1051 | $this->expectExceptionMessage( 1052 | sprintf("Unsupported redirect to %s detected, execution terminated.", self::REDIRECTION_PATH) 1053 | ); 1054 | 1055 | $this->auth->user_logout_userkey(); 1056 | } 1057 | 1058 | /** 1059 | * Test when try to logout, but user logged in with different auth type. 1060 | */ 1061 | public function test_user_logout_userkey_when_user_logged_in_with_different_auth() { 1062 | global $USER; 1063 | 1064 | $_POST['return'] = self::REDIRECTION_PATH; 1065 | 1066 | $this->setUser($this->user); 1067 | try { 1068 | $this->auth->user_logout_userkey(); 1069 | } catch (moodle_exception $e) { 1070 | $this->assertTrue(isloggedin()); 1071 | $this->assertEquals($USER->id, $this->user->id); 1072 | $this->assertEquals( 1073 | 'Incorrect logout request', 1074 | $e->getMessage() 1075 | ); 1076 | } 1077 | } 1078 | 1079 | /** 1080 | * Test when try to logout, but user logged in with different auth type. 1081 | */ 1082 | public function test_user_logout_userkey_when_user_logged_in_but_return_not_set() { 1083 | $this->setUser($this->user); 1084 | 1085 | $this->expectException(moodle_exception::class); 1086 | $this->expectExceptionMessage('A required parameter (return) was missing'); 1087 | 1088 | $this->auth->user_logout_userkey(); 1089 | } 1090 | 1091 | /** 1092 | * Test successful logout. 1093 | */ 1094 | public function test_user_logout_userkey_logging_out() { 1095 | global $USER; 1096 | 1097 | $this->setUser($this->user); 1098 | $USER->auth = 'userkey'; 1099 | $_POST['return'] = self::REDIRECTION_PATH; 1100 | 1101 | try { 1102 | $this->auth->user_logout_userkey(); 1103 | } catch (moodle_exception $e) { 1104 | $this->assertFalse(isloggedin()); 1105 | $this->assertEquals( 1106 | sprintf('Unsupported redirect to %s detected, execution terminated.', self::REDIRECTION_PATH), 1107 | $e->getMessage() 1108 | ); 1109 | } 1110 | } 1111 | } 1112 | -------------------------------------------------------------------------------- /tests/core_userkey_manager_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace auth_userkey; 18 | 19 | /** 20 | * Tests for core_userkey_manager class. 21 | * 22 | * Key validation is fully covered in auth_plugin_test.php file. 23 | * TODO: write tests for validate_key() function. 24 | * 25 | * @covers \auth_userkey\core_userkey_manager 26 | * 27 | * @package auth_userkey 28 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 29 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 30 | */ 31 | class core_userkey_manager_test extends \advanced_testcase { 32 | /** 33 | * Test user object. 34 | * @var 35 | */ 36 | protected $user; 37 | 38 | /** 39 | * Test config object. 40 | * @var 41 | */ 42 | protected $config; 43 | 44 | /** 45 | * Initial set up. 46 | */ 47 | public function setUp(): void { 48 | global $CFG; 49 | 50 | parent::setUp(); 51 | 52 | $this->resetAfterTest(); 53 | $CFG->getremoteaddrconf = GETREMOTEADDR_SKIP_HTTP_X_FORWARDED_FOR; 54 | $this->user = self::getDataGenerator()->create_user(); 55 | $this->config = new \stdClass(); 56 | } 57 | 58 | /** 59 | * Test that core_userkey_manager implements userkey_manager_interface interface. 60 | */ 61 | public function test_implements_userkey_manager_interface() { 62 | $manager = new core_userkey_manager($this->config); 63 | 64 | $expected = 'auth_userkey\userkey_manager_interface'; 65 | $this->assertInstanceOf($expected, $manager); 66 | } 67 | 68 | /** 69 | * Test that key gets created correctly if config option iprestriction is not set. 70 | */ 71 | public function test_create_correct_key_if_iprestriction_is_not_set() { 72 | global $DB; 73 | 74 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 75 | $manager = new core_userkey_manager($this->config); 76 | $value = $manager->create_key($this->user->id); 77 | 78 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 79 | 80 | $this->assertEquals($value, $actualkey->value); 81 | $this->assertEquals($this->user->id, $actualkey->userid); 82 | $this->assertEquals('auth/userkey', $actualkey->script); 83 | $this->assertEquals($this->user->id, $actualkey->instance); 84 | $this->assertEquals(null, $actualkey->iprestriction); 85 | $this->assertEquals(time() + 60, $actualkey->validuntil); 86 | } 87 | 88 | /** 89 | * Test that key gets created correctly if config option iprestriction is set to true. 90 | */ 91 | public function test_create_correct_key_if_iprestriction_is_true() { 92 | global $DB; 93 | 94 | $this->config->iprestriction = true; 95 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 96 | $manager = new core_userkey_manager($this->config); 97 | $value = $manager->create_key($this->user->id); 98 | 99 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 100 | 101 | $this->assertEquals($value, $actualkey->value); 102 | $this->assertEquals($this->user->id, $actualkey->userid); 103 | $this->assertEquals('auth/userkey', $actualkey->script); 104 | $this->assertEquals($this->user->id, $actualkey->instance); 105 | $this->assertEquals('192.168.1.1', $actualkey->iprestriction); 106 | $this->assertEquals(time() + 60, $actualkey->validuntil); 107 | } 108 | 109 | /** 110 | * Test that key gets created correctly if config option iprestriction is set to true and we set allowedips. 111 | */ 112 | public function test_create_correct_key_if_iprestriction_is_true_and_we_set_allowedips() { 113 | global $DB; 114 | 115 | $this->config->iprestriction = true; 116 | $manager = new core_userkey_manager($this->config); 117 | $value = $manager->create_key($this->user->id, '192.168.1.3'); 118 | 119 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 120 | 121 | $this->assertEquals($value, $actualkey->value); 122 | $this->assertEquals($this->user->id, $actualkey->userid); 123 | $this->assertEquals('auth/userkey', $actualkey->script); 124 | $this->assertEquals($this->user->id, $actualkey->instance); 125 | $this->assertEquals('192.168.1.3', $actualkey->iprestriction); 126 | $this->assertEquals(time() + 60, $actualkey->validuntil); 127 | } 128 | 129 | /** 130 | * Test that key gets created correctly if config option iprestriction is set to false. 131 | */ 132 | public function test_create_correct_key_if_iprestriction_is_false() { 133 | global $DB; 134 | 135 | $this->config->iprestriction = false; 136 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 137 | $manager = new core_userkey_manager($this->config); 138 | $value = $manager->create_key($this->user->id); 139 | 140 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 141 | 142 | $this->assertEquals($value, $actualkey->value); 143 | $this->assertEquals($this->user->id, $actualkey->userid); 144 | $this->assertEquals('auth/userkey', $actualkey->script); 145 | $this->assertEquals($this->user->id, $actualkey->instance); 146 | $this->assertEquals(null, $actualkey->iprestriction); 147 | $this->assertEquals(time() + 60, $actualkey->validuntil); 148 | } 149 | 150 | /** 151 | * Test that IP address mismatch exception gets thrown if incorrect IP and outside whitelist. 152 | */ 153 | public function test_exception_if_ip_is_outside_whitelist() { 154 | global $DB; 155 | 156 | $this->config->iprestriction = true; 157 | $this->config->ipwhitelist = '10.0.0.0/8;172.16.0.0/12;192.168.0.0/16'; 158 | 159 | $manager = new core_userkey_manager($this->config); 160 | $value = $manager->create_key($this->user->id, '193.168.1.1'); 161 | 162 | $_SERVER['HTTP_CLIENT_IP'] = '193.168.1.2'; 163 | 164 | $this->expectException(\moodle_exception::class); 165 | $this->expectExceptionMessage('Client IP address mismatch'); 166 | 167 | $manager->validate_key($value); 168 | } 169 | 170 | /** 171 | * Test that IP address mismatch exception gets thrown if incorrect IP and outside whitelist. 172 | */ 173 | public function test_create_correct_key_if_ip_correct_not_whitelisted_and_whitelist_set() { 174 | global $DB; 175 | 176 | $this->config->iprestriction = true; 177 | 178 | $this->config->ipwhitelist = '10.0.0.0/8;172.16.0.0/12;192.168.0.0/16'; 179 | 180 | $manager = new core_userkey_manager($this->config); 181 | $value = $manager->create_key($this->user->id, '193.168.1.1'); 182 | 183 | $_SERVER['HTTP_CLIENT_IP'] = '193.168.1.1'; 184 | 185 | $key = $manager->validate_key($value); 186 | $this->assertEquals($this->user->id, $key->userid); 187 | } 188 | 189 | /** 190 | * Test that key is accepted if incorrect IP and within whitelist. 191 | */ 192 | public function test_create_correct_key_if_ip_is_whitelisted() { 193 | global $DB; 194 | 195 | $this->config->iprestriction = true; 196 | 197 | $this->config->ipwhitelist = '10.0.0.0/8;172.16.0.0/12;192.168.0.0/16'; 198 | 199 | $manager = new core_userkey_manager($this->config); 200 | $value = $manager->create_key($this->user->id, '192.168.1.1'); 201 | 202 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.2'; 203 | 204 | $key = $manager->validate_key($value); 205 | $this->assertEquals($this->user->id, $key->userid); 206 | } 207 | 208 | /** 209 | * Test that key gets created correctly if config option iprestriction is set to false and we set allowedips. 210 | */ 211 | public function test_create_correct_key_if_iprestriction_is_falseand_we_set_allowedips() { 212 | global $DB; 213 | 214 | $this->config->iprestriction = false; 215 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 216 | $manager = new core_userkey_manager($this->config); 217 | $value = $manager->create_key($this->user->id, '192.168.1.1'); 218 | 219 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 220 | 221 | $this->assertEquals($value, $actualkey->value); 222 | $this->assertEquals($this->user->id, $actualkey->userid); 223 | $this->assertEquals('auth/userkey', $actualkey->script); 224 | $this->assertEquals($this->user->id, $actualkey->instance); 225 | $this->assertEquals(null, $actualkey->iprestriction); 226 | $this->assertEquals(time() + 60, $actualkey->validuntil); 227 | } 228 | 229 | /** 230 | * Test that key gets created correctly if config option iprestriction is set to a string. 231 | */ 232 | public function test_create_correct_key_if_iprestriction_is_string() { 233 | global $DB; 234 | 235 | $this->config->iprestriction = 'string'; 236 | $_SERVER['HTTP_CLIENT_IP'] = '192.168.1.1'; 237 | $manager = new core_userkey_manager($this->config); 238 | $value = $manager->create_key($this->user->id); 239 | 240 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 241 | 242 | $this->assertEquals($value, $actualkey->value); 243 | $this->assertEquals($this->user->id, $actualkey->userid); 244 | $this->assertEquals('auth/userkey', $actualkey->script); 245 | $this->assertEquals($this->user->id, $actualkey->instance); 246 | $this->assertEquals('192.168.1.1', $actualkey->iprestriction); 247 | $this->assertEquals(time() + 60, $actualkey->validuntil); 248 | } 249 | 250 | /** 251 | * Test that key gets created correctly if config option keylifetime is not set. 252 | */ 253 | public function test_create_correct_key_if_keylifetime_is_not_set() { 254 | global $DB; 255 | 256 | $manager = new core_userkey_manager($this->config); 257 | $value = $manager->create_key($this->user->id); 258 | 259 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 260 | 261 | $this->assertEquals($value, $actualkey->value); 262 | $this->assertEquals($this->user->id, $actualkey->userid); 263 | $this->assertEquals('auth/userkey', $actualkey->script); 264 | $this->assertEquals($this->user->id, $actualkey->instance); 265 | $this->assertEquals(null, $actualkey->iprestriction); 266 | $this->assertEquals(time() + 60, $actualkey->validuntil); 267 | } 268 | 269 | /** 270 | * Test that key gets created correctly if config option keylifetime is set to integer. 271 | */ 272 | public function test_create_correct_key_if_keylifetime_is_set_to_integer() { 273 | global $DB; 274 | 275 | $this->config->keylifetime = 3000; 276 | 277 | $manager = new core_userkey_manager($this->config); 278 | $value = $manager->create_key($this->user->id); 279 | 280 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 281 | 282 | $this->assertEquals($value, $actualkey->value); 283 | $this->assertEquals($this->user->id, $actualkey->userid); 284 | $this->assertEquals('auth/userkey', $actualkey->script); 285 | $this->assertEquals($this->user->id, $actualkey->instance); 286 | $this->assertEquals(null, $actualkey->iprestriction); 287 | $this->assertEquals(time() + 3000, $actualkey->validuntil); 288 | 289 | } 290 | 291 | /** 292 | * Test that key gets created correctly if config option keylifetime is set to a string. 293 | */ 294 | public function test_create_correct_key_if_keylifetime_is_set_to_string() { 295 | global $DB; 296 | 297 | $this->config->keylifetime = '3000'; 298 | 299 | $manager = new core_userkey_manager($this->config); 300 | $value = $manager->create_key($this->user->id); 301 | 302 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 303 | 304 | $this->assertEquals($value, $actualkey->value); 305 | $this->assertEquals($this->user->id, $actualkey->userid); 306 | $this->assertEquals('auth/userkey', $actualkey->script); 307 | $this->assertEquals($this->user->id, $actualkey->instance); 308 | $this->assertEquals(null, $actualkey->iprestriction); 309 | $this->assertEquals(time() + 3000, $actualkey->validuntil); 310 | 311 | } 312 | 313 | /** 314 | * Test that we can delete created key. 315 | */ 316 | public function test_can_delete_created_key() { 317 | global $DB; 318 | 319 | $manager = new core_userkey_manager($this->config); 320 | $value = $manager->create_key($this->user->id); 321 | 322 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 323 | $this->assertEquals(1, count($keys)); 324 | 325 | $manager->delete_keys($this->user->id); 326 | 327 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 328 | $this->assertEquals(0, count($keys)); 329 | } 330 | 331 | /** 332 | * Test that we can delete all existing keys. 333 | */ 334 | public function test_can_delete_all_existing_keys() { 335 | global $DB; 336 | 337 | $manager = new core_userkey_manager($this->config); 338 | 339 | create_user_key('auth/userkey', $this->user->id); 340 | create_user_key('auth/userkey', $this->user->id); 341 | create_user_key('auth/userkey', $this->user->id); 342 | 343 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 344 | $this->assertEquals(3, count($keys)); 345 | 346 | $manager->delete_keys($this->user->id); 347 | 348 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 349 | $this->assertEquals(0, count($keys)); 350 | } 351 | 352 | /** 353 | * Test that we create only one key. 354 | */ 355 | public function test_create_only_one_key() { 356 | global $DB; 357 | 358 | $manager = new core_userkey_manager($this->config); 359 | 360 | create_user_key('auth/userkey', $this->user->id); 361 | create_user_key('auth/userkey', $this->user->id); 362 | create_user_key('auth/userkey', $this->user->id); 363 | 364 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 365 | $this->assertEquals(3, count($keys)); 366 | 367 | $manager->create_key($this->user->id); 368 | $keys = $DB->get_records('user_private_key', array('userid' => $this->user->id)); 369 | $this->assertEquals(1, count($keys)); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /tests/externallib_test.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace auth_userkey; 18 | 19 | use advanced_testcase; 20 | use webservice_access_exception; 21 | use auth_userkey_external; 22 | use external_api; 23 | use invalid_parameter_exception; 24 | use required_capability_exception; 25 | use context_system; 26 | 27 | /** 28 | * Tests for externallib.php. 29 | * 30 | * @covers \auth_userkey_external 31 | * 32 | * @package auth_userkey 33 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 | */ 36 | class externallib_test extends advanced_testcase { 37 | /** 38 | * User object. 39 | * 40 | * @var 41 | */ 42 | protected $user = array(); 43 | 44 | /** 45 | * Initial set up. 46 | */ 47 | public function setUp(): void { 48 | global $CFG; 49 | 50 | require_once($CFG->libdir . "/externallib.php"); 51 | require_once($CFG->dirroot . '/auth/userkey/externallib.php'); 52 | 53 | $this->resetAfterTest(); 54 | 55 | $user = array(); 56 | $user['username'] = 'username'; 57 | $user['email'] = 'exists@test.com'; 58 | $user['idnumber'] = 'idnumber'; 59 | $this->user = self::getDataGenerator()->create_user($user); 60 | } 61 | 62 | /** 63 | * Test call with incorrect required parameter. 64 | */ 65 | public function test_throwing_plugin_disabled_exception() { 66 | $this->setAdminUser(); 67 | 68 | $params = array( 69 | 'bla' => 'exists@test.com', 70 | ); 71 | 72 | $this->expectException(webservice_access_exception::class); 73 | $this->expectExceptionMessage('Access control exception (The userkey authentication plugin is disabled.)'); 74 | 75 | // Simulate the web service server. 76 | $result = auth_userkey_external::request_login_url($params); 77 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 78 | } 79 | 80 | /** 81 | * Test successful web service calls. 82 | */ 83 | public function test_successful_webservice_calls() { 84 | global $DB, $CFG; 85 | 86 | $CFG->auth = "userkey"; 87 | $this->setAdminUser(); 88 | 89 | // Email. 90 | $params = array( 91 | 'email' => 'exists@test.com', 92 | ); 93 | 94 | // Simulate the web service server. 95 | $result = auth_userkey_external::request_login_url($params); 96 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 97 | 98 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 99 | $expectedurl = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 100 | 101 | $this->assertTrue(is_array($result)); 102 | $this->assertTrue(key_exists('loginurl', $result)); 103 | $this->assertEquals($expectedurl, $result['loginurl']); 104 | 105 | // Username. 106 | set_config('mappingfield', 'username', 'auth_userkey'); 107 | $params = array( 108 | 'username' => 'username', 109 | ); 110 | 111 | // Simulate the web service server. 112 | $result = auth_userkey_external::request_login_url($params); 113 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 114 | 115 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 116 | $expectedurl = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 117 | 118 | $this->assertTrue(is_array($result)); 119 | $this->assertTrue(key_exists('loginurl', $result)); 120 | $this->assertEquals($expectedurl, $result['loginurl']); 121 | 122 | // Idnumber. 123 | set_config('mappingfield', 'idnumber', 'auth_userkey'); 124 | $params = array( 125 | 'idnumber' => 'idnumber', 126 | ); 127 | 128 | // Simulate the web service server. 129 | $result = auth_userkey_external::request_login_url($params); 130 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 131 | 132 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 133 | $expectedurl = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 134 | 135 | $this->assertTrue(is_array($result)); 136 | $this->assertTrue(key_exists('loginurl', $result)); 137 | $this->assertEquals($expectedurl, $result['loginurl']); 138 | 139 | // IP restriction. 140 | set_config('iprestriction', true, 'auth_userkey'); 141 | set_config('mappingfield', 'idnumber', 'auth_userkey'); 142 | $params = array( 143 | 'idnumber' => 'idnumber', 144 | 'ip' => '192.168.1.1', 145 | ); 146 | 147 | // Simulate the web service server. 148 | $result = auth_userkey_external::request_login_url($params); 149 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 150 | 151 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 152 | $expectedurl = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 153 | 154 | $this->assertTrue(is_array($result)); 155 | $this->assertTrue(key_exists('loginurl', $result)); 156 | $this->assertEquals($expectedurl, $result['loginurl']); 157 | } 158 | 159 | /** 160 | * Test call with missing email required parameter. 161 | */ 162 | public function test_exception_thrown_if_required_parameter_email_is_not_set() { 163 | global $CFG; 164 | 165 | $this->setAdminUser(); 166 | $CFG->auth = "userkey"; 167 | 168 | $params = array( 169 | 'bla' => 'exists@test.com', 170 | ); 171 | 172 | $this->expectException(invalid_parameter_exception::class); 173 | $this->expectExceptionMessage('Invalid parameter value detected (Required field "email" is not set or empty.)'); 174 | 175 | auth_userkey_external::request_login_url($params); 176 | } 177 | 178 | /** 179 | * Test call with missing ip required parameter. 180 | */ 181 | public function test_exception_thrown_if_required_parameter_op_is_not_set() { 182 | global $CFG; 183 | 184 | $this->setAdminUser(); 185 | $CFG->auth = "userkey"; 186 | 187 | set_config('iprestriction', true, 'auth_userkey'); 188 | 189 | $params = array( 190 | 'email' => 'exists@test.com', 191 | ); 192 | 193 | $this->expectException(invalid_parameter_exception::class); 194 | $this->expectExceptionMessage('Invalid parameter value detected (Required parameter "ip" is not set.)'); 195 | 196 | auth_userkey_external::request_login_url($params); 197 | } 198 | 199 | /** 200 | * Test request for a user who is not exist. 201 | */ 202 | public function test_request_not_existing_user() { 203 | global $CFG; 204 | 205 | $this->setAdminUser(); 206 | $CFG->auth = "userkey"; 207 | 208 | $params = array( 209 | 'email' => 'notexists@test.com', 210 | ); 211 | 212 | $this->expectException(invalid_parameter_exception::class); 213 | $this->expectExceptionMessage('Invalid parameter value detected (User is not exist)'); 214 | 215 | // Simulate the web service server. 216 | $result = auth_userkey_external::request_login_url($params); 217 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 218 | } 219 | 220 | /** 221 | * Test that permission exception gets thrown if user doesn't have required permissions. 222 | */ 223 | public function test_throwing_of_permission_exception() { 224 | global $CFG; 225 | 226 | $this->setUser($this->user); 227 | $CFG->auth = "userkey"; 228 | 229 | $params = array( 230 | 'email' => 'notexists@test.com', 231 | ); 232 | 233 | $this->expectException(required_capability_exception::class); 234 | $this->expectExceptionMessage('Sorry, but you do not currently have permissions to do that (Generate login user key)'); 235 | 236 | // Simulate the web service server. 237 | $result = auth_userkey_external::request_login_url($params); 238 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 239 | } 240 | 241 | /** 242 | * Test request gets executed correctly if use has required permissions. 243 | */ 244 | public function test_request_gets_executed_if_user_has_permission() { 245 | global $CFG, $DB; 246 | 247 | $this->setUser($this->user); 248 | $CFG->auth = "userkey"; 249 | 250 | $context = context_system::instance(); 251 | $studentrole = $DB->get_record('role', array('shortname' => 'student'), '*', MUST_EXIST); 252 | assign_capability('auth/userkey:generatekey', CAP_ALLOW, $studentrole->id, $context->id); 253 | role_assign($studentrole->id, $this->user->id, $context->id); 254 | 255 | $params = array( 256 | 'email' => 'exists@test.com', 257 | ); 258 | 259 | // Simulate the web service server. 260 | $result = auth_userkey_external::request_login_url($params); 261 | $result = external_api::clean_returnvalue(auth_userkey_external::request_login_url_returns(), $result); 262 | 263 | $actualkey = $DB->get_record('user_private_key', array('userid' => $this->user->id)); 264 | $expectedurl = $CFG->wwwroot . '/auth/userkey/login.php?key=' . $actualkey->value; 265 | 266 | $this->assertTrue(is_array($result)); 267 | $this->assertTrue(key_exists('loginurl', $result)); 268 | $this->assertEquals($expectedurl, $result['loginurl']); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/fake_userkey_manager.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | namespace auth_userkey; 18 | 19 | /** 20 | * Fake userkey manager for testing. 21 | * 22 | * @package auth_userkey 23 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 25 | */ 26 | class fake_userkey_manager implements userkey_manager_interface { 27 | 28 | /** 29 | * Create key. 30 | * 31 | * @param int $userid user ID. 32 | * @param null $allowedips Allowed IPs. 33 | * 34 | * @return string 35 | */ 36 | public function create_key($userid, $allowedips = null) { 37 | return 'FaKeKeyFoRtEsTiNg'; 38 | } 39 | 40 | /** 41 | * Delete keys for a user. 42 | * 43 | * @param int $userid User ID to delete keys for. 44 | */ 45 | public function delete_keys($userid) { 46 | } 47 | 48 | /** 49 | * Validate provided key. 50 | * 51 | * @param string $keyvalue Key to validate. 52 | * 53 | * @return object|void 54 | */ 55 | public function validate_key($keyvalue) { 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /version.php: -------------------------------------------------------------------------------- 1 | . 16 | 17 | /** 18 | * Version details. 19 | * 20 | * @package auth_userkey 21 | * @copyright 2016 Dmitrii Metelkin (dmitriim@catalyst-au.net) 22 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 | */ 24 | 25 | defined('MOODLE_INTERNAL') || die; 26 | 27 | $plugin->version = 2022081901; // The current plugin version (Date: YYYYMMDDXX). 28 | $plugin->release = 2022081901; // Match release exactly to version. 29 | $plugin->requires = 2017051500; // Requires Moodle 3.3 version. 30 | $plugin->component = 'auth_userkey'; // Full name of the plugin (used for diagnostics). 31 | $plugin->maturity = MATURITY_STABLE; 32 | $plugin->supported = [33, 401]; // A range of branch numbers of supported moodle versions. 33 | --------------------------------------------------------------------------------