[ 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 System.Webauthn 6 * 7 * @copyright (C) 2020 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 */ 10 11 namespace Joomla\Plugin\System\Webauthn\PluginTraits; 12 13 use Exception; 14 use Joomla\CMS\Authentication\Authentication; 15 use Joomla\CMS\Authentication\AuthenticationResponse; 16 use Joomla\CMS\Event\Plugin\System\Webauthn\AjaxLogin; 17 use Joomla\CMS\Factory; 18 use Joomla\CMS\Language\Text; 19 use Joomla\CMS\Log\Log; 20 use Joomla\CMS\Plugin\PluginHelper; 21 use Joomla\CMS\Uri\Uri; 22 use Joomla\CMS\User\User; 23 use Joomla\CMS\User\UserFactoryInterface; 24 use RuntimeException; 25 use Throwable; 26 27 // phpcs:disable PSR1.Files.SideEffects 28 \defined('_JEXEC') or die; 29 // phpcs:enable PSR1.Files.SideEffects 30 31 /** 32 * Ajax handler for akaction=login 33 * 34 * Verifies the response received from the browser and logs in the user 35 * 36 * @since 4.0.0 37 */ 38 trait AjaxHandlerLogin 39 { 40 /** 41 * Returns the public key set for the user and a unique challenge in a Public Key Credential Request encoded as 42 * JSON. 43 * 44 * @param AjaxLogin $event The event we are handling 45 * 46 * @return void 47 * 48 * @since 4.0.0 49 */ 50 public function onAjaxWebauthnLogin(AjaxLogin $event): void 51 { 52 $session = $this->getApplication()->getSession(); 53 $returnUrl = $session->get('plg_system_webauthn.returnUrl', Uri::base()); 54 $userId = $session->get('plg_system_webauthn.userId', 0); 55 56 try { 57 $credentialRepository = $this->authenticationHelper->getCredentialsRepository(); 58 59 // No user ID: no username was provided and the resident credential refers to an unknown user handle. DIE! 60 if (empty($userId)) { 61 Log::add('Cannot determine the user ID', Log::NOTICE, 'webauthn.system'); 62 63 throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); 64 } 65 66 // Do I have a valid user? 67 $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); 68 69 if ($user->id != $userId) { 70 $message = sprintf('User #%d does not exist', $userId); 71 Log::add($message, Log::NOTICE, 'webauthn.system'); 72 73 throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); 74 } 75 76 // Validate the authenticator response and get the user handle 77 $userHandle = $this->getUserHandleFromResponse($user); 78 79 if (is_null($userHandle)) { 80 Log::add('Cannot retrieve the user handle from the request; the browser did not assert our request.', Log::NOTICE, 'webauthn.system'); 81 82 throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); 83 } 84 85 // Does the user handle match the user ID? This should never trigger by definition of the login check. 86 $validUserHandle = $credentialRepository->getHandleFromUserId($userId); 87 88 if ($userHandle != $validUserHandle) { 89 $message = sprintf('Invalid user handle; expected %s, got %s', $validUserHandle, $userHandle); 90 Log::add($message, Log::NOTICE, 'webauthn.system'); 91 92 throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); 93 } 94 95 // Make sure the user exists 96 $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); 97 98 if ($user->id != $userId) { 99 $message = sprintf('Invalid user ID; expected %d, got %d', $userId, $user->id); 100 Log::add($message, Log::NOTICE, 'webauthn.system'); 101 102 throw new RuntimeException(Text::_('PLG_SYSTEM_WEBAUTHN_ERR_CREATE_INVALID_LOGIN_REQUEST')); 103 } 104 105 // Login the user 106 Log::add("Logging in the user", Log::INFO, 'webauthn.system'); 107 $this->loginUser((int) $userId); 108 } catch (Throwable $e) { 109 $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); 110 111 $response = $this->getAuthenticationResponseObject(); 112 $response->status = Authentication::STATUS_UNKNOWN; 113 $response->error_message = $e->getMessage(); 114 115 Log::add(sprintf("Received login failure. Message: %s", $e->getMessage()), Log::ERROR, 'webauthn.system'); 116 117 // This also enqueues the login failure message for display after redirection. Look for JLog in that method. 118 $this->processLoginFailure($response, null, 'system'); 119 } finally { 120 /** 121 * This code needs to run no matter if the login succeeded or failed. It prevents replay attacks and takes 122 * the user back to the page they started from. 123 */ 124 125 // Remove temporary information for security reasons 126 $session->set('plg_system_webauthn.publicKeyCredentialRequestOptions', null); 127 $session->set('plg_system_webauthn.returnUrl', null); 128 $session->set('plg_system_webauthn.userId', null); 129 130 // Redirect back to the page we were before. 131 $this->getApplication()->redirect($returnUrl); 132 } 133 } 134 135 /** 136 * Logs in a user to the site, bypassing the authentication plugins. 137 * 138 * @param int $userId The user ID to log in 139 * 140 * @return void 141 * @throws Exception 142 * @since 4.2.0 143 */ 144 private function loginUser(int $userId): void 145 { 146 // Trick the class auto-loader into loading the necessary classes 147 class_exists('Joomla\\CMS\\Authentication\\Authentication', true); 148 149 // Fake a successful login message 150 $isAdmin = $this->getApplication()->isClient('administrator'); 151 $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($userId); 152 153 // Does the user account have a pending activation? 154 if (!empty($user->activation)) { 155 throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); 156 } 157 158 // Is the user account blocked? 159 if ($user->block) { 160 throw new RuntimeException(Text::_('JGLOBAL_AUTH_ACCESS_DENIED')); 161 } 162 163 $statusSuccess = Authentication::STATUS_SUCCESS; 164 165 $response = $this->getAuthenticationResponseObject(); 166 $response->status = $statusSuccess; 167 $response->username = $user->username; 168 $response->fullname = $user->name; 169 $response->error_message = ''; 170 $response->language = $user->getParam('language'); 171 $response->type = 'Passwordless'; 172 173 if ($isAdmin) { 174 $response->language = $user->getParam('admin_language'); 175 } 176 177 /** 178 * Set up the login options. 179 * 180 * The 'remember' element forces the use of the Remember Me feature when logging in with Webauthn, as the 181 * users would expect. 182 * 183 * The 'action' element is actually required by plg_user_joomla. It is the core ACL action the logged in user 184 * must be allowed for the login to succeed. Please note that front-end and back-end logins use a different 185 * action. This allows us to provide the WebAuthn button on both front- and back-end and be sure that if a 186 * used with no backend access tries to use it to log in Joomla! will just slap him with an error message about 187 * insufficient privileges - the same thing that'd happen if you tried to use your front-end only username and 188 * password in a back-end login form. 189 */ 190 $options = [ 191 'remember' => true, 192 'action' => 'core.login.site', 193 ]; 194 195 if ($isAdmin) { 196 $options['action'] = 'core.login.admin'; 197 } 198 199 // Run the user plugins. They CAN block login by returning boolean false and setting $response->error_message. 200 PluginHelper::importPlugin('user'); 201 $eventClassName = self::getEventClassByEventName('onUserLogin'); 202 $event = new $eventClassName('onUserLogin', [(array) $response, $options]); 203 $result = $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); 204 $results = !isset($result['result']) || \is_null($result['result']) ? [] : $result['result']; 205 206 // If there is no boolean FALSE result from any plugin the login is successful. 207 if (in_array(false, $results, true) === false) { 208 // Set the user in the session, letting Joomla! know that we are logged in. 209 $this->getApplication()->getSession()->set('user', $user); 210 211 // Trigger the onUserAfterLogin event 212 $options['user'] = $user; 213 $options['responseType'] = $response->type; 214 215 // The user is successfully logged in. Run the after login events 216 $eventClassName = self::getEventClassByEventName('onUserAfterLogin'); 217 $event = new $eventClassName('onUserAfterLogin', [$options]); 218 $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); 219 220 return; 221 } 222 223 // If we are here the plugins marked a login failure. Trigger the onUserLoginFailure Event. 224 $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); 225 $event = new $eventClassName('onUserLoginFailure', [(array) $response]); 226 $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); 227 228 // Log the failure 229 Log::add($response->error_message, Log::WARNING, 'jerror'); 230 231 // Throw an exception to let the caller know that the login failed 232 throw new RuntimeException($response->error_message); 233 } 234 235 /** 236 * Returns a (blank) Joomla! authentication response 237 * 238 * @return AuthenticationResponse 239 * 240 * @since 4.2.0 241 */ 242 private function getAuthenticationResponseObject(): AuthenticationResponse 243 { 244 // Force the class auto-loader to load the JAuthentication class 245 class_exists('Joomla\\CMS\\Authentication\\Authentication', true); 246 247 return new AuthenticationResponse(); 248 } 249 250 /** 251 * Have Joomla! process a login failure 252 * 253 * @param AuthenticationResponse $response The Joomla! auth response object 254 * 255 * @return boolean 256 * 257 * @since 4.2.0 258 */ 259 private function processLoginFailure(AuthenticationResponse $response): bool 260 { 261 // Import the user plugin group. 262 PluginHelper::importPlugin('user'); 263 264 // Trigger onUserLoginFailure Event. 265 Log::add('Calling onUserLoginFailure plugin event', Log::INFO, 'plg_system_webauthn'); 266 267 $eventClassName = self::getEventClassByEventName('onUserLoginFailure'); 268 $event = new $eventClassName('onUserLoginFailure', [(array) $response]); 269 $this->getApplication()->getDispatcher()->dispatch($event->getName(), $event); 270 271 // If status is success, any error will have been raised by the user plugin 272 $expectedStatus = Authentication::STATUS_SUCCESS; 273 274 if ($response->status !== $expectedStatus) { 275 Log::add('The login failure has been logged in Joomla\'s error log', Log::INFO, 'webauthn.system'); 276 277 // Everything logged in the 'jerror' category ends up being enqueued in the application message queue. 278 Log::add($response->error_message, Log::WARNING, 'jerror'); 279 } else { 280 $message = 'A login failure was caused by a third party user plugin but it did not return any' . 281 'further information.'; 282 Log::add($message, Log::WARNING, 'webauthn.system'); 283 } 284 285 return false; 286 } 287 288 /** 289 * Validate the authenticator response sent to us by the browser. 290 * 291 * @param User $user The user we are trying to log in. 292 * 293 * @return string|null The user handle or null 294 * 295 * @throws Exception 296 * @since 4.2.0 297 */ 298 private function getUserHandleFromResponse(User $user): ?string 299 { 300 // Retrieve data from the request and session 301 $pubKeyCredentialSource = $this->authenticationHelper->validateAssertionResponse( 302 $this->getApplication()->input->getBase64('data', ''), 303 $user 304 ); 305 306 return $pubKeyCredentialSource ? $pubKeyCredentialSource->getUserHandle() : null; 307 } 308 }
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 |