Differences Between: [Versions 310 and 311] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 18 namespace core; 19 20 /** 21 * Authentication related tests. 22 * 23 * @package core 24 * @category test 25 * @copyright 2012 Petr Skoda {@link http://skodak.org} 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 */ 28 class authlib_test extends \advanced_testcase { 29 public function test_lockout() { 30 global $CFG; 31 require_once("$CFG->libdir/authlib.php"); 32 33 $this->resetAfterTest(); 34 35 $oldlog = ini_get('error_log'); 36 ini_set('error_log', "$CFG->dataroot/testlog.log"); // Prevent standard logging. 37 38 unset_config('noemailever'); 39 40 set_config('lockoutthreshold', 0); 41 set_config('lockoutwindow', 60*20); 42 set_config('lockoutduration', 60*30); 43 44 $user = $this->getDataGenerator()->create_user(); 45 46 // Test lockout is disabled when threshold not set. 47 48 $this->assertFalse(login_is_lockedout($user)); 49 login_attempt_failed($user); 50 login_attempt_failed($user); 51 login_attempt_failed($user); 52 login_attempt_failed($user); 53 $this->assertFalse(login_is_lockedout($user)); 54 55 // Test lockout threshold works. 56 57 set_config('lockoutthreshold', 3); 58 login_attempt_failed($user); 59 login_attempt_failed($user); 60 $this->assertFalse(login_is_lockedout($user)); 61 $sink = $this->redirectEmails(); 62 login_attempt_failed($user); 63 $this->assertCount(1, $sink->get_messages()); 64 $sink->close(); 65 $this->assertTrue(login_is_lockedout($user)); 66 67 // Test unlock works. 68 69 login_unlock_account($user); 70 $this->assertFalse(login_is_lockedout($user)); 71 72 // Test lockout window works. 73 74 login_attempt_failed($user); 75 login_attempt_failed($user); 76 $this->assertFalse(login_is_lockedout($user)); 77 set_user_preference('login_failed_last', time()-60*20-10, $user); 78 login_attempt_failed($user); 79 $this->assertFalse(login_is_lockedout($user)); 80 81 // Test valid login resets window. 82 83 login_attempt_valid($user); 84 $this->assertFalse(login_is_lockedout($user)); 85 login_attempt_failed($user); 86 login_attempt_failed($user); 87 $this->assertFalse(login_is_lockedout($user)); 88 89 // Test lock duration works. 90 91 $sink = $this->redirectEmails(); 92 login_attempt_failed($user); 93 $this->assertCount(1, $sink->get_messages()); 94 $sink->close(); 95 $this->assertTrue(login_is_lockedout($user)); 96 set_user_preference('login_lockout', time()-60*30+10, $user); 97 $this->assertTrue(login_is_lockedout($user)); 98 set_user_preference('login_lockout', time()-60*30-10, $user); 99 $this->assertFalse(login_is_lockedout($user)); 100 101 // Test lockout ignored pref works. 102 103 set_user_preference('login_lockout_ignored', 1, $user); 104 login_attempt_failed($user); 105 login_attempt_failed($user); 106 login_attempt_failed($user); 107 login_attempt_failed($user); 108 $this->assertFalse(login_is_lockedout($user)); 109 110 ini_set('error_log', $oldlog); 111 } 112 113 public function test_authenticate_user_login() { 114 global $CFG; 115 116 $this->resetAfterTest(); 117 118 $oldlog = ini_get('error_log'); 119 ini_set('error_log', "$CFG->dataroot/testlog.log"); // Prevent standard logging. 120 121 unset_config('noemailever'); 122 123 set_config('lockoutthreshold', 0); 124 set_config('lockoutwindow', 60*20); 125 set_config('lockoutduration', 60*30); 126 127 $_SERVER['HTTP_USER_AGENT'] = 'no browser'; // Hack around missing user agent in CLI scripts. 128 129 $user1 = $this->getDataGenerator()->create_user(array('username'=>'username1', 'password'=>'password1', 'email'=>'email1@example.com')); 130 $user2 = $this->getDataGenerator()->create_user(array('username'=>'username2', 'password'=>'password2', 'email'=>'email2@example.com', 'suspended'=>1)); 131 $user3 = $this->getDataGenerator()->create_user(array('username'=>'username3', 'password'=>'password3', 'email'=>'email2@example.com', 'auth'=>'nologin')); 132 133 // Normal login. 134 $sink = $this->redirectEvents(); 135 $result = authenticate_user_login('username1', 'password1'); 136 $events = $sink->get_events(); 137 $sink->close(); 138 $this->assertEmpty($events); 139 $this->assertInstanceOf('stdClass', $result); 140 $this->assertEquals($user1->id, $result->id); 141 142 // Normal login with reason. 143 $reason = null; 144 $sink = $this->redirectEvents(); 145 $result = authenticate_user_login('username1', 'password1', false, $reason); 146 $events = $sink->get_events(); 147 $sink->close(); 148 $this->assertEmpty($events); 149 $this->assertInstanceOf('stdClass', $result); 150 $this->assertEquals(AUTH_LOGIN_OK, $reason); 151 152 // Test login via email 153 $reason = null; 154 $this->assertEmpty($CFG->authloginviaemail); 155 $sink = $this->redirectEvents(); 156 $result = authenticate_user_login('email1@example.com', 'password1', false, $reason); 157 $sink->close(); 158 $this->assertFalse($result); 159 $this->assertEquals(AUTH_LOGIN_NOUSER, $reason); 160 161 set_config('authloginviaemail', 1); 162 $this->assertNotEmpty($CFG->authloginviaemail); 163 $sink = $this->redirectEvents(); 164 $result = authenticate_user_login('email1@example.com', 'password1'); 165 $events = $sink->get_events(); 166 $sink->close(); 167 $this->assertEmpty($events); 168 $this->assertInstanceOf('stdClass', $result); 169 $this->assertEquals($user1->id, $result->id); 170 171 $reason = null; 172 $sink = $this->redirectEvents(); 173 $result = authenticate_user_login('email2@example.com', 'password2', false, $reason); 174 $events = $sink->get_events(); 175 $sink->close(); 176 $this->assertFalse($result); 177 $this->assertEquals(AUTH_LOGIN_NOUSER, $reason); 178 set_config('authloginviaemail', 0); 179 180 $reason = null; 181 // Capture failed login event. 182 $sink = $this->redirectEvents(); 183 $result = authenticate_user_login('username1', 'nopass', false, $reason); 184 $events = $sink->get_events(); 185 $sink->close(); 186 $event = array_pop($events); 187 188 $this->assertFalse($result); 189 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 190 // Test Event. 191 $this->assertInstanceOf('\core\event\user_login_failed', $event); 192 $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1'); 193 $this->assertEventLegacyLogData($expectedlogdata, $event); 194 $eventdata = $event->get_data(); 195 $this->assertSame($eventdata['other']['username'], 'username1'); 196 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED); 197 $this->assertEventContextNotUsed($event); 198 199 // Capture failed login token. 200 unset($CFG->alternateloginurl); 201 unset($CFG->disablelogintoken); 202 $sink = $this->redirectEvents(); 203 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 204 $events = $sink->get_events(); 205 $sink->close(); 206 $event = array_pop($events); 207 208 $this->assertFalse($result); 209 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 210 // Test Event. 211 $this->assertInstanceOf('\core\event\user_login_failed', $event); 212 $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1'); 213 $this->assertEventLegacyLogData($expectedlogdata, $event); 214 $eventdata = $event->get_data(); 215 $this->assertSame($eventdata['other']['username'], 'username1'); 216 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED); 217 $this->assertEventContextNotUsed($event); 218 219 // Login should work with invalid token if CFG login token settings override it. 220 $CFG->alternateloginurl = 'http://localhost/'; 221 $sink = $this->redirectEvents(); 222 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 223 $events = $sink->get_events(); 224 $sink->close(); 225 $this->assertEmpty($events); 226 $this->assertInstanceOf('stdClass', $result); 227 $this->assertEquals(AUTH_LOGIN_OK, $reason); 228 229 unset($CFG->alternateloginurl); 230 $CFG->disablelogintoken = true; 231 232 $sink = $this->redirectEvents(); 233 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 234 $events = $sink->get_events(); 235 $sink->close(); 236 $this->assertEmpty($events); 237 $this->assertInstanceOf('stdClass', $result); 238 $this->assertEquals(AUTH_LOGIN_OK, $reason); 239 240 unset($CFG->disablelogintoken); 241 // Normal login with valid token. 242 $reason = null; 243 $token = \core\session\manager::get_login_token(); 244 $sink = $this->redirectEvents(); 245 $result = authenticate_user_login('username1', 'password1', false, $reason, $token); 246 $events = $sink->get_events(); 247 $sink->close(); 248 $this->assertEmpty($events); 249 $this->assertInstanceOf('stdClass', $result); 250 $this->assertEquals(AUTH_LOGIN_OK, $reason); 251 252 $reason = null; 253 // Capture failed login event. 254 $sink = $this->redirectEvents(); 255 $result = authenticate_user_login('username2', 'password2', false, $reason); 256 $events = $sink->get_events(); 257 $sink->close(); 258 $event = array_pop($events); 259 260 $this->assertFalse($result); 261 $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); 262 // Test Event. 263 $this->assertInstanceOf('\core\event\user_login_failed', $event); 264 $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username2'); 265 $this->assertEventLegacyLogData($expectedlogdata, $event); 266 $eventdata = $event->get_data(); 267 $this->assertSame($eventdata['other']['username'], 'username2'); 268 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); 269 $this->assertEventContextNotUsed($event); 270 271 $reason = null; 272 // Capture failed login event. 273 $sink = $this->redirectEvents(); 274 $result = authenticate_user_login('username3', 'password3', false, $reason); 275 $events = $sink->get_events(); 276 $sink->close(); 277 $event = array_pop($events); 278 279 $this->assertFalse($result); 280 $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); 281 // Test Event. 282 $this->assertInstanceOf('\core\event\user_login_failed', $event); 283 $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username3'); 284 $this->assertEventLegacyLogData($expectedlogdata, $event); 285 $eventdata = $event->get_data(); 286 $this->assertSame($eventdata['other']['username'], 'username3'); 287 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); 288 $this->assertEventContextNotUsed($event); 289 290 $reason = null; 291 // Capture failed login event. 292 $sink = $this->redirectEvents(); 293 $result = authenticate_user_login('username4', 'password3', false, $reason); 294 $events = $sink->get_events(); 295 $sink->close(); 296 $event = array_pop($events); 297 298 $this->assertFalse($result); 299 $this->assertEquals(AUTH_LOGIN_NOUSER, $reason); 300 // Test Event. 301 $this->assertInstanceOf('\core\event\user_login_failed', $event); 302 $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username4'); 303 $this->assertEventLegacyLogData($expectedlogdata, $event); 304 $eventdata = $event->get_data(); 305 $this->assertSame($eventdata['other']['username'], 'username4'); 306 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_NOUSER); 307 $this->assertEventContextNotUsed($event); 308 309 set_config('lockoutthreshold', 3); 310 311 $reason = null; 312 $result = authenticate_user_login('username1', 'nopass', false, $reason); 313 $this->assertFalse($result); 314 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 315 $result = authenticate_user_login('username1', 'nopass', false, $reason); 316 $this->assertFalse($result); 317 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 318 $sink = $this->redirectEmails(); 319 $result = authenticate_user_login('username1', 'nopass', false, $reason); 320 $this->assertCount(1, $sink->get_messages()); 321 $sink->close(); 322 $this->assertFalse($result); 323 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 324 325 $result = authenticate_user_login('username1', 'password1', false, $reason); 326 $this->assertFalse($result); 327 $this->assertEquals(AUTH_LOGIN_LOCKOUT, $reason); 328 329 $result = authenticate_user_login('username1', 'password1', true, $reason); 330 $this->assertInstanceOf('stdClass', $result); 331 $this->assertEquals(AUTH_LOGIN_OK, $reason); 332 333 ini_set('error_log', $oldlog); 334 335 // Test password policy check on login. 336 $CFG->passwordpolicy = 0; 337 $CFG->passwordpolicycheckonlogin = 1; 338 339 // First test with password policy disabled. 340 $user4 = $this->getDataGenerator()->create_user(array('username' => 'username4', 'password' => 'a')); 341 $sink = $this->redirectEvents(); 342 $reason = null; 343 $result = authenticate_user_login('username4', 'a', false, $reason); 344 $events = $sink->get_events(); 345 $sink->close(); 346 $notifications = notification::fetch(); 347 $this->assertInstanceOf('stdClass', $result); 348 $this->assertEquals(AUTH_LOGIN_OK, $reason); 349 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 350 // Check no events. 351 $this->assertEquals(count($events), 0); 352 // Check no notifications. 353 $this->assertEquals(count($notifications), 0); 354 355 // Now test with the password policy enabled, flip reset flag. 356 $sink = $this->redirectEvents(); 357 $reason = null; 358 $CFG->passwordpolicy = 1; 359 $result = authenticate_user_login('username4', 'a', false, $reason); 360 $events = $sink->get_events(); 361 $sink->close(); 362 $this->assertInstanceOf('stdClass', $result); 363 $this->assertEquals(AUTH_LOGIN_OK, $reason); 364 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', true, $result), true); 365 // Check that an event was emitted for the policy failure. 366 $this->assertEquals(count($events), 1); 367 $this->assertEquals(reset($events)->eventname, '\core\event\user_password_policy_failed'); 368 // Check notification fired. 369 $notifications = notification::fetch(); 370 $this->assertEquals(count($notifications), 1); 371 372 // Now the same tests with a user that passes the password policy. 373 $user5 = $this->getDataGenerator()->create_user(array('username' => 'username5', 'password' => 'ThisPassword1sSecure!')); 374 $reason = null; 375 $CFG->passwordpolicy = 0; 376 $sink = $this->redirectEvents(); 377 $result = authenticate_user_login('username5', 'ThisPassword1sSecure!', false, $reason); 378 $events = $sink->get_events(); 379 $sink->close(); 380 $notifications = notification::fetch(); 381 $this->assertInstanceOf('stdClass', $result); 382 $this->assertEquals(AUTH_LOGIN_OK, $reason); 383 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 384 // Check no events. 385 $this->assertEquals(count($events), 0); 386 // Check no notifications. 387 $this->assertEquals(count($notifications), 0); 388 389 $reason = null; 390 $CFG->passwordpolicy = 1; 391 $sink = $this->redirectEvents(); 392 $result = authenticate_user_login('username5', 'ThisPassword1sSecure!', false, $reason); 393 $events = $sink->get_events(); 394 $sink->close(); 395 $notifications = notification::fetch(); 396 $this->assertInstanceOf('stdClass', $result); 397 $this->assertEquals(AUTH_LOGIN_OK, $reason); 398 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 399 // Check no events. 400 $this->assertEquals(count($events), 0); 401 // Check no notifications. 402 $this->assertEquals(count($notifications), 0); 403 } 404 405 public function test_user_loggedin_event_exceptions() { 406 try { 407 $event = \core\event\user_loggedin::create(array('objectid' => 1)); 408 $this->fail('\core\event\user_loggedin requires other[\'username\']'); 409 } catch(\Exception $e) { 410 $this->assertInstanceOf('coding_exception', $e); 411 } 412 } 413 414 /** 415 * Test the {@link signup_validate_data()} duplicate email validation. 416 */ 417 public function test_signup_validate_data_same_email() { 418 global $CFG; 419 require_once($CFG->libdir . '/authlib.php'); 420 require_once($CFG->libdir . '/phpmailer/moodle_phpmailer.php'); 421 require_once($CFG->dirroot . '/user/profile/lib.php'); 422 423 $this->resetAfterTest(); 424 425 $CFG->registerauth = 'email'; 426 $CFG->passwordpolicy = false; 427 428 // In this test, we want to check accent-sensitive email search. However, accented email addresses do not pass 429 // the default `validate_email()` and Moodle does not yet provide a CFG switch to allow such emails. So we 430 // inject our own validation method here and revert it back once we are done. This custom validator method is 431 // identical to the default 'php' validator with the only difference: it has the FILTER_FLAG_EMAIL_UNICODE set 432 // so that it allows to use non-ASCII characters in email addresses. 433 $defaultvalidator = \moodle_phpmailer::$validator; 434 \moodle_phpmailer::$validator = function($address) { 435 return (bool) filter_var($address, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE); 436 }; 437 438 // Check that two users cannot share the same email address if the site is configured so. 439 // Emails in Moodle are supposed to be case-insensitive (and accent-sensitive but accents are not yet supported). 440 $CFG->allowaccountssameemail = false; 441 442 $u1 = $this->getDataGenerator()->create_user([ 443 'username' => 'abcdef', 444 'email' => 'abcdef@example.com', 445 ]); 446 447 $formdata = [ 448 'username' => 'newuser', 449 'firstname' => 'First', 450 'lastname' => 'Last', 451 'password' => 'weak', 452 'email' => 'ABCDEF@example.com', 453 ]; 454 455 $errors = signup_validate_data($formdata, []); 456 $this->assertStringContainsString('This email address is already registered.', $errors['email']); 457 458 // Emails are accent-sensitive though so if we change a -> á in the u1's email, it should pass. 459 // Please note that Moodle does not normally support such emails yet. We test the DB search sensitivity here. 460 $formdata['email'] = 'ábcdef@example.com'; 461 $errors = signup_validate_data($formdata, []); 462 $this->assertArrayNotHasKey('email', $errors); 463 464 // Check that users can share the same email if the site is configured so. 465 $CFG->allowaccountssameemail = true; 466 $formdata['email'] = 'abcdef@example.com'; 467 $errors = signup_validate_data($formdata, []); 468 $this->assertArrayNotHasKey('email', $errors); 469 470 // Restore the original email address validator. 471 \moodle_phpmailer::$validator = $defaultvalidator; 472 } 473 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body