Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
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 $eventdata = $event->get_data(); 193 $this->assertSame($eventdata['other']['username'], 'username1'); 194 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED); 195 $this->assertEventContextNotUsed($event); 196 197 // Capture failed login token. 198 unset($CFG->alternateloginurl); 199 unset($CFG->disablelogintoken); 200 $sink = $this->redirectEvents(); 201 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 202 $events = $sink->get_events(); 203 $sink->close(); 204 $event = array_pop($events); 205 206 $this->assertFalse($result); 207 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 208 // Test Event. 209 $this->assertInstanceOf('\core\event\user_login_failed', $event); 210 $eventdata = $event->get_data(); 211 $this->assertSame($eventdata['other']['username'], 'username1'); 212 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED); 213 $this->assertEventContextNotUsed($event); 214 215 // Login should work with invalid token if CFG login token settings override it. 216 $CFG->alternateloginurl = 'http://localhost/'; 217 $sink = $this->redirectEvents(); 218 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 219 $events = $sink->get_events(); 220 $sink->close(); 221 $this->assertEmpty($events); 222 $this->assertInstanceOf('stdClass', $result); 223 $this->assertEquals(AUTH_LOGIN_OK, $reason); 224 225 unset($CFG->alternateloginurl); 226 $CFG->disablelogintoken = true; 227 228 $sink = $this->redirectEvents(); 229 $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken'); 230 $events = $sink->get_events(); 231 $sink->close(); 232 $this->assertEmpty($events); 233 $this->assertInstanceOf('stdClass', $result); 234 $this->assertEquals(AUTH_LOGIN_OK, $reason); 235 236 unset($CFG->disablelogintoken); 237 // Normal login with valid token. 238 $reason = null; 239 $token = \core\session\manager::get_login_token(); 240 $sink = $this->redirectEvents(); 241 $result = authenticate_user_login('username1', 'password1', false, $reason, $token); 242 $events = $sink->get_events(); 243 $sink->close(); 244 $this->assertEmpty($events); 245 $this->assertInstanceOf('stdClass', $result); 246 $this->assertEquals(AUTH_LOGIN_OK, $reason); 247 248 $reason = null; 249 // Capture failed login event. 250 $sink = $this->redirectEvents(); 251 $result = authenticate_user_login('username2', 'password2', false, $reason); 252 $events = $sink->get_events(); 253 $sink->close(); 254 $event = array_pop($events); 255 256 $this->assertFalse($result); 257 $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); 258 // Test Event. 259 $this->assertInstanceOf('\core\event\user_login_failed', $event); 260 $eventdata = $event->get_data(); 261 $this->assertSame($eventdata['other']['username'], 'username2'); 262 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); 263 $this->assertEventContextNotUsed($event); 264 265 $reason = null; 266 // Capture failed login event. 267 $sink = $this->redirectEvents(); 268 $result = authenticate_user_login('username3', 'password3', false, $reason); 269 $events = $sink->get_events(); 270 $sink->close(); 271 $event = array_pop($events); 272 273 $this->assertFalse($result); 274 $this->assertEquals(AUTH_LOGIN_SUSPENDED, $reason); 275 // Test Event. 276 $this->assertInstanceOf('\core\event\user_login_failed', $event); 277 $eventdata = $event->get_data(); 278 $this->assertSame($eventdata['other']['username'], 'username3'); 279 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_SUSPENDED); 280 $this->assertEventContextNotUsed($event); 281 282 $reason = null; 283 // Capture failed login event. 284 $sink = $this->redirectEvents(); 285 $result = authenticate_user_login('username4', 'password3', false, $reason); 286 $events = $sink->get_events(); 287 $sink->close(); 288 $event = array_pop($events); 289 290 $this->assertFalse($result); 291 $this->assertEquals(AUTH_LOGIN_NOUSER, $reason); 292 // Test Event. 293 $this->assertInstanceOf('\core\event\user_login_failed', $event); 294 $eventdata = $event->get_data(); 295 $this->assertSame($eventdata['other']['username'], 'username4'); 296 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_NOUSER); 297 $this->assertEventContextNotUsed($event); 298 299 set_config('lockoutthreshold', 3); 300 301 $reason = null; 302 $result = authenticate_user_login('username1', 'nopass', false, $reason); 303 $this->assertFalse($result); 304 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 305 $result = authenticate_user_login('username1', 'nopass', false, $reason); 306 $this->assertFalse($result); 307 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 308 $sink = $this->redirectEmails(); 309 $result = authenticate_user_login('username1', 'nopass', false, $reason); 310 $this->assertCount(1, $sink->get_messages()); 311 $sink->close(); 312 $this->assertFalse($result); 313 $this->assertEquals(AUTH_LOGIN_FAILED, $reason); 314 315 $result = authenticate_user_login('username1', 'password1', false, $reason); 316 $this->assertFalse($result); 317 $this->assertEquals(AUTH_LOGIN_LOCKOUT, $reason); 318 319 $result = authenticate_user_login('username1', 'password1', true, $reason); 320 $this->assertInstanceOf('stdClass', $result); 321 $this->assertEquals(AUTH_LOGIN_OK, $reason); 322 323 ini_set('error_log', $oldlog); 324 325 // Test password policy check on login. 326 $CFG->passwordpolicy = 0; 327 $CFG->passwordpolicycheckonlogin = 1; 328 329 // First test with password policy disabled. 330 $user4 = $this->getDataGenerator()->create_user(array('username' => 'username4', 'password' => 'a')); 331 $sink = $this->redirectEvents(); 332 $reason = null; 333 $result = authenticate_user_login('username4', 'a', false, $reason); 334 $events = $sink->get_events(); 335 $sink->close(); 336 $notifications = notification::fetch(); 337 $this->assertInstanceOf('stdClass', $result); 338 $this->assertEquals(AUTH_LOGIN_OK, $reason); 339 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 340 // Check no events. 341 $this->assertEquals(count($events), 0); 342 // Check no notifications. 343 $this->assertEquals(count($notifications), 0); 344 345 // Now test with the password policy enabled, flip reset flag. 346 $sink = $this->redirectEvents(); 347 $reason = null; 348 $CFG->passwordpolicy = 1; 349 $result = authenticate_user_login('username4', 'a', false, $reason); 350 $events = $sink->get_events(); 351 $sink->close(); 352 $this->assertInstanceOf('stdClass', $result); 353 $this->assertEquals(AUTH_LOGIN_OK, $reason); 354 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', true, $result), true); 355 // Check that an event was emitted for the policy failure. 356 $this->assertEquals(count($events), 1); 357 $this->assertEquals(reset($events)->eventname, '\core\event\user_password_policy_failed'); 358 // Check notification fired. 359 $notifications = notification::fetch(); 360 $this->assertEquals(count($notifications), 1); 361 362 // Now the same tests with a user that passes the password policy. 363 $user5 = $this->getDataGenerator()->create_user(array('username' => 'username5', 'password' => 'ThisPassword1sSecure!')); 364 $reason = null; 365 $CFG->passwordpolicy = 0; 366 $sink = $this->redirectEvents(); 367 $result = authenticate_user_login('username5', 'ThisPassword1sSecure!', false, $reason); 368 $events = $sink->get_events(); 369 $sink->close(); 370 $notifications = notification::fetch(); 371 $this->assertInstanceOf('stdClass', $result); 372 $this->assertEquals(AUTH_LOGIN_OK, $reason); 373 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 374 // Check no events. 375 $this->assertEquals(count($events), 0); 376 // Check no notifications. 377 $this->assertEquals(count($notifications), 0); 378 379 $reason = null; 380 $CFG->passwordpolicy = 1; 381 $sink = $this->redirectEvents(); 382 $result = authenticate_user_login('username5', 'ThisPassword1sSecure!', false, $reason); 383 $events = $sink->get_events(); 384 $sink->close(); 385 $notifications = notification::fetch(); 386 $this->assertInstanceOf('stdClass', $result); 387 $this->assertEquals(AUTH_LOGIN_OK, $reason); 388 $this->assertEquals(get_user_preferences('auth_forcepasswordchange', false, $result), false); 389 // Check no events. 390 $this->assertEquals(count($events), 0); 391 // Check no notifications. 392 $this->assertEquals(count($notifications), 0); 393 394 // Capture failed login reCaptcha. 395 $CFG->recaptchapublickey = 'randompublickey'; 396 $CFG->recaptchaprivatekey = 'randomprivatekey'; 397 $CFG->enableloginrecaptcha = true; 398 399 // Login with blank captcha. 400 $sink = $this->redirectEvents(); 401 $result = authenticate_user_login('username1', 'password1', false, $reason, false, ''); 402 $events = $sink->get_events(); 403 $sink->close(); 404 $event = array_pop($events); 405 406 $this->assertFalse($result); 407 $this->assertEquals(AUTH_LOGIN_FAILED_RECAPTCHA, $reason); 408 409 // Test event. 410 $this->assertInstanceOf('\core\event\user_login_failed', $event); 411 $eventdata = $event->get_data(); 412 $this->assertSame($eventdata['other']['username'], 'username1'); 413 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED_RECAPTCHA); 414 $this->assertEventContextNotUsed($event); 415 416 // Login with invalid captcha. 417 $sink = $this->redirectEvents(); 418 $result = authenticate_user_login('username1', 'password1', false, $reason, false, 'invalidcaptcha'); 419 $events = $sink->get_events(); 420 $sink->close(); 421 $event = array_pop($events); 422 423 $this->assertFalse($result); 424 $this->assertEquals(AUTH_LOGIN_FAILED_RECAPTCHA, $reason); 425 426 // Test event. 427 $this->assertInstanceOf('\core\event\user_login_failed', $event); 428 $eventdata = $event->get_data(); 429 $this->assertSame($eventdata['other']['username'], 'username1'); 430 $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED_RECAPTCHA); 431 $this->assertEventContextNotUsed($event); 432 433 // Unset settings. 434 unset($CFG->recaptchapublickey); 435 unset($CFG->recaptchaprivatekey); 436 unset($CFG->enableloginrecaptcha); 437 } 438 439 public function test_user_loggedin_event_exceptions() { 440 try { 441 $event = \core\event\user_loggedin::create(array('objectid' => 1)); 442 $this->fail('\core\event\user_loggedin requires other[\'username\']'); 443 } catch(\Exception $e) { 444 $this->assertInstanceOf('coding_exception', $e); 445 } 446 } 447 448 /** 449 * Test the {@link signup_validate_data()} duplicate email validation. 450 */ 451 public function test_signup_validate_data_same_email() { 452 global $CFG; 453 require_once($CFG->libdir . '/authlib.php'); 454 require_once($CFG->libdir . '/phpmailer/moodle_phpmailer.php'); 455 require_once($CFG->dirroot . '/user/profile/lib.php'); 456 457 $this->resetAfterTest(); 458 459 $CFG->registerauth = 'email'; 460 $CFG->passwordpolicy = false; 461 462 // In this test, we want to check accent-sensitive email search. However, accented email addresses do not pass 463 // the default `validate_email()` and Moodle does not yet provide a CFG switch to allow such emails. So we 464 // inject our own validation method here and revert it back once we are done. This custom validator method is 465 // identical to the default 'php' validator with the only difference: it has the FILTER_FLAG_EMAIL_UNICODE set 466 // so that it allows to use non-ASCII characters in email addresses. 467 $defaultvalidator = \moodle_phpmailer::$validator; 468 \moodle_phpmailer::$validator = function($address) { 469 return (bool) filter_var($address, FILTER_VALIDATE_EMAIL, FILTER_FLAG_EMAIL_UNICODE); 470 }; 471 472 // Check that two users cannot share the same email address if the site is configured so. 473 // Emails in Moodle are supposed to be case-insensitive (and accent-sensitive but accents are not yet supported). 474 $CFG->allowaccountssameemail = false; 475 476 $u1 = $this->getDataGenerator()->create_user([ 477 'username' => 'abcdef', 478 'email' => 'abcdef@example.com', 479 ]); 480 481 $formdata = [ 482 'username' => 'newuser', 483 'firstname' => 'First', 484 'lastname' => 'Last', 485 'password' => 'weak', 486 'email' => 'ABCDEF@example.com', 487 ]; 488 489 $errors = signup_validate_data($formdata, []); 490 $this->assertStringContainsString('This email address is already registered.', $errors['email']); 491 492 // Emails are accent-sensitive though so if we change a -> á in the u1's email, it should pass. 493 // Please note that Moodle does not normally support such emails yet. We test the DB search sensitivity here. 494 $formdata['email'] = 'ábcdef@example.com'; 495 $errors = signup_validate_data($formdata, []); 496 $this->assertArrayNotHasKey('email', $errors); 497 498 // Check that users can share the same email if the site is configured so. 499 $CFG->allowaccountssameemail = true; 500 $formdata['email'] = 'abcdef@example.com'; 501 $errors = signup_validate_data($formdata, []); 502 $this->assertArrayNotHasKey('email', $errors); 503 504 // Restore the original email address validator. 505 \moodle_phpmailer::$validator = $defaultvalidator; 506 } 507 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body