[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package Joomla.Plugin 5 * @subpackage Authentication.cookie 6 * 7 * @copyright (C) 2013 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 10 * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace 11 */ 12 13 use Joomla\CMS\Authentication\Authentication; 14 use Joomla\CMS\Filter\InputFilter; 15 use Joomla\CMS\Language\Text; 16 use Joomla\CMS\Log\Log; 17 use Joomla\CMS\Plugin\CMSPlugin; 18 use Joomla\CMS\User\User; 19 use Joomla\CMS\User\UserHelper; 20 21 // phpcs:disable PSR1.Files.SideEffects 22 \defined('_JEXEC') or die; 23 // phpcs:enable PSR1.Files.SideEffects 24 25 /** 26 * Joomla Authentication plugin 27 * 28 * @since 3.2 29 * @note Code based on http://jaspan.com/improved_persistent_login_cookie_best_practice 30 * and http://fishbowl.pastiche.org/2004/01/19/persistent_login_cookie_best_practice/ 31 */ 32 class PlgAuthenticationCookie extends CMSPlugin 33 { 34 /** 35 * Application object 36 * 37 * @var \Joomla\CMS\Application\CMSApplication 38 * @since 3.2 39 */ 40 protected $app; 41 42 /** 43 * Database object 44 * 45 * @var \Joomla\Database\DatabaseDriver 46 * @since 3.2 47 */ 48 protected $db; 49 50 /** 51 * Reports the privacy related capabilities for this plugin to site administrators. 52 * 53 * @return array 54 * 55 * @since 3.9.0 56 */ 57 public function onPrivacyCollectAdminCapabilities() 58 { 59 $this->loadLanguage(); 60 61 return array( 62 Text::_('PLG_AUTHENTICATION_COOKIE') => array( 63 Text::_('PLG_AUTHENTICATION_COOKIE_PRIVACY_CAPABILITY_COOKIE'), 64 ), 65 ); 66 } 67 68 /** 69 * This method should handle any authentication and report back to the subject 70 * 71 * @param array $credentials Array holding the user credentials 72 * @param array $options Array of extra options 73 * @param object &$response Authentication response object 74 * 75 * @return boolean 76 * 77 * @since 3.2 78 */ 79 public function onUserAuthenticate($credentials, $options, &$response) 80 { 81 // No remember me for admin 82 if ($this->app->isClient('administrator')) { 83 return false; 84 } 85 86 // Get cookie 87 $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent(); 88 $cookieValue = $this->app->input->cookie->get($cookieName); 89 90 // Try with old cookieName (pre 3.6.0) if not found 91 if (!$cookieValue) { 92 $cookieName = UserHelper::getShortHashedUserAgent(); 93 $cookieValue = $this->app->input->cookie->get($cookieName); 94 } 95 96 if (!$cookieValue) { 97 return false; 98 } 99 100 $cookieArray = explode('.', $cookieValue); 101 102 // Check for valid cookie value 103 if (count($cookieArray) !== 2) { 104 // Destroy the cookie in the browser. 105 $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', '')); 106 Log::add('Invalid cookie detected.', Log::WARNING, 'error'); 107 108 return false; 109 } 110 111 $response->type = 'Cookie'; 112 113 // Filter series since we're going to use it in the query 114 $filter = new InputFilter(); 115 $series = $filter->clean($cookieArray[1], 'ALNUM'); 116 $now = time(); 117 118 // Remove expired tokens 119 $query = $this->db->getQuery(true) 120 ->delete($this->db->quoteName('#__user_keys')) 121 ->where($this->db->quoteName('time') . ' < :now') 122 ->bind(':now', $now); 123 124 try { 125 $this->db->setQuery($query)->execute(); 126 } catch (RuntimeException $e) { 127 // We aren't concerned with errors from this query, carry on 128 } 129 130 // Find the matching record if it exists. 131 $query = $this->db->getQuery(true) 132 ->select($this->db->quoteName(['user_id', 'token', 'series', 'time'])) 133 ->from($this->db->quoteName('#__user_keys')) 134 ->where($this->db->quoteName('series') . ' = :series') 135 ->where($this->db->quoteName('uastring') . ' = :uastring') 136 ->order($this->db->quoteName('time') . ' DESC') 137 ->bind(':series', $series) 138 ->bind(':uastring', $cookieName); 139 140 try { 141 $results = $this->db->setQuery($query)->loadObjectList(); 142 } catch (RuntimeException $e) { 143 $response->status = Authentication::STATUS_FAILURE; 144 145 return false; 146 } 147 148 if (count($results) !== 1) { 149 // Destroy the cookie in the browser. 150 $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', '')); 151 $response->status = Authentication::STATUS_FAILURE; 152 153 return false; 154 } 155 156 // We have a user with one cookie with a valid series and a corresponding record in the database. 157 if (!UserHelper::verifyPassword($cookieArray[0], $results[0]->token)) { 158 /* 159 * This is a real attack! 160 * Either the series was guessed correctly or a cookie was stolen and used twice (once by attacker and once by victim). 161 * Delete all tokens for this user! 162 */ 163 $query = $this->db->getQuery(true) 164 ->delete($this->db->quoteName('#__user_keys')) 165 ->where($this->db->quoteName('user_id') . ' = :userid') 166 ->bind(':userid', $results[0]->user_id); 167 168 try { 169 $this->db->setQuery($query)->execute(); 170 } catch (RuntimeException $e) { 171 // Log an alert for the site admin 172 Log::add( 173 sprintf('Failed to delete cookie token for user %s with the following error: %s', $results[0]->user_id, $e->getMessage()), 174 Log::WARNING, 175 'security' 176 ); 177 } 178 179 // Destroy the cookie in the browser. 180 $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', '')); 181 182 // Issue warning by email to user and/or admin? 183 Log::add(Text::sprintf('PLG_AUTHENTICATION_COOKIE_ERROR_LOG_LOGIN_FAILED', $results[0]->user_id), Log::WARNING, 'security'); 184 $response->status = Authentication::STATUS_FAILURE; 185 186 return false; 187 } 188 189 // Make sure there really is a user with this name and get the data for the session. 190 $query = $this->db->getQuery(true) 191 ->select($this->db->quoteName(['id', 'username', 'password'])) 192 ->from($this->db->quoteName('#__users')) 193 ->where($this->db->quoteName('username') . ' = :userid') 194 ->where($this->db->quoteName('requireReset') . ' = 0') 195 ->bind(':userid', $results[0]->user_id); 196 197 try { 198 $result = $this->db->setQuery($query)->loadObject(); 199 } catch (RuntimeException $e) { 200 $response->status = Authentication::STATUS_FAILURE; 201 202 return false; 203 } 204 205 if ($result) { 206 // Bring this in line with the rest of the system 207 $user = User::getInstance($result->id); 208 209 // Set response data. 210 $response->username = $result->username; 211 $response->email = $user->email; 212 $response->fullname = $user->name; 213 $response->password = $result->password; 214 $response->language = $user->getParam('language'); 215 216 // Set response status. 217 $response->status = Authentication::STATUS_SUCCESS; 218 $response->error_message = ''; 219 } else { 220 $response->status = Authentication::STATUS_FAILURE; 221 $response->error_message = Text::_('JGLOBAL_AUTH_NO_USER'); 222 } 223 } 224 225 /** 226 * We set the authentication cookie only after login is successfully finished. 227 * We set a new cookie either for a user with no cookies or one 228 * where the user used a cookie to authenticate. 229 * 230 * @param array $options Array holding options 231 * 232 * @return boolean True on success 233 * 234 * @since 3.2 235 */ 236 public function onUserAfterLogin($options) 237 { 238 // No remember me for admin 239 if ($this->app->isClient('administrator')) { 240 return false; 241 } 242 243 if (isset($options['responseType']) && $options['responseType'] === 'Cookie') { 244 // Logged in using a cookie 245 $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent(); 246 247 // We need the old data to get the existing series 248 $cookieValue = $this->app->input->cookie->get($cookieName); 249 250 // Try with old cookieName (pre 3.6.0) if not found 251 if (!$cookieValue) { 252 $oldCookieName = UserHelper::getShortHashedUserAgent(); 253 $cookieValue = $this->app->input->cookie->get($oldCookieName); 254 255 // Destroy the old cookie in the browser 256 $this->app->input->cookie->set($oldCookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', '')); 257 } 258 259 $cookieArray = explode('.', $cookieValue); 260 261 // Filter series since we're going to use it in the query 262 $filter = new InputFilter(); 263 $series = $filter->clean($cookieArray[1], 'ALNUM'); 264 } elseif (!empty($options['remember'])) { 265 // Remember checkbox is set 266 $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent(); 267 268 // Create a unique series which will be used over the lifespan of the cookie 269 $unique = false; 270 $errorCount = 0; 271 272 do { 273 $series = UserHelper::genRandomPassword(20); 274 $query = $this->db->getQuery(true) 275 ->select($this->db->quoteName('series')) 276 ->from($this->db->quoteName('#__user_keys')) 277 ->where($this->db->quoteName('series') . ' = :series') 278 ->bind(':series', $series); 279 280 try { 281 $results = $this->db->setQuery($query)->loadResult(); 282 283 if ($results === null) { 284 $unique = true; 285 } 286 } catch (RuntimeException $e) { 287 $errorCount++; 288 289 // We'll let this query fail up to 5 times before giving up, there's probably a bigger issue at this point 290 if ($errorCount === 5) { 291 return false; 292 } 293 } 294 } while ($unique === false); 295 } else { 296 return false; 297 } 298 299 // Get the parameter values 300 $lifetime = $this->params->get('cookie_lifetime', 60) * 24 * 60 * 60; 301 $length = $this->params->get('key_length', 16); 302 303 // Generate new cookie 304 $token = UserHelper::genRandomPassword($length); 305 $cookieValue = $token . '.' . $series; 306 307 // Overwrite existing cookie with new value 308 $this->app->input->cookie->set( 309 $cookieName, 310 $cookieValue, 311 time() + $lifetime, 312 $this->app->get('cookie_path', '/'), 313 $this->app->get('cookie_domain', ''), 314 $this->app->isHttpsForced(), 315 true 316 ); 317 318 $query = $this->db->getQuery(true); 319 320 if (!empty($options['remember'])) { 321 $future = (time() + $lifetime); 322 323 // Create new record 324 $query 325 ->insert($this->db->quoteName('#__user_keys')) 326 ->set($this->db->quoteName('user_id') . ' = :userid') 327 ->set($this->db->quoteName('series') . ' = :series') 328 ->set($this->db->quoteName('uastring') . ' = :uastring') 329 ->set($this->db->quoteName('time') . ' = :time') 330 ->bind(':userid', $options['user']->username) 331 ->bind(':series', $series) 332 ->bind(':uastring', $cookieName) 333 ->bind(':time', $future); 334 } else { 335 // Update existing record with new token 336 $query 337 ->update($this->db->quoteName('#__user_keys')) 338 ->where($this->db->quoteName('user_id') . ' = :userid') 339 ->where($this->db->quoteName('series') . ' = :series') 340 ->where($this->db->quoteName('uastring') . ' = :uastring') 341 ->bind(':userid', $options['user']->username) 342 ->bind(':series', $series) 343 ->bind(':uastring', $cookieName); 344 } 345 346 $hashedToken = UserHelper::hashPassword($token); 347 348 $query->set($this->db->quoteName('token') . ' = :token') 349 ->bind(':token', $hashedToken); 350 351 try { 352 $this->db->setQuery($query)->execute(); 353 } catch (RuntimeException $e) { 354 return false; 355 } 356 357 return true; 358 } 359 360 /** 361 * This is where we delete any authentication cookie when a user logs out 362 * 363 * @param array $options Array holding options (length, timeToExpiration) 364 * 365 * @return boolean True on success 366 * 367 * @since 3.2 368 */ 369 public function onUserAfterLogout($options) 370 { 371 // No remember me for admin 372 if ($this->app->isClient('administrator')) { 373 return false; 374 } 375 376 $cookieName = 'joomla_remember_me_' . UserHelper::getShortHashedUserAgent(); 377 $cookieValue = $this->app->input->cookie->get($cookieName); 378 379 // There are no cookies to delete. 380 if (!$cookieValue) { 381 return true; 382 } 383 384 $cookieArray = explode('.', $cookieValue); 385 386 // Filter series since we're going to use it in the query 387 $filter = new InputFilter(); 388 $series = $filter->clean($cookieArray[1], 'ALNUM'); 389 390 // Remove the record from the database 391 $query = $this->db->getQuery(true) 392 ->delete($this->db->quoteName('#__user_keys')) 393 ->where($this->db->quoteName('series') . ' = :series') 394 ->bind(':series', $series); 395 396 try { 397 $this->db->setQuery($query)->execute(); 398 } catch (RuntimeException $e) { 399 // We aren't concerned with errors from this query, carry on 400 } 401 402 // Destroy the cookie 403 $this->app->input->cookie->set($cookieName, '', 1, $this->app->get('cookie_path', '/'), $this->app->get('cookie_domain', '')); 404 405 return true; 406 } 407 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |