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