1<?php 2/** 3 * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com", 4 * Daniel Cannon (cannon dot danielc at gmail dot com) 5 * 6 * This program is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License along 17 * with this program; if not, write to the Free Software Foundation, Inc., 18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 * http://www.gnu.org/copyleft/gpl.html 20 * 21 * @file 22 */ 23 24use MediaWiki\Auth\AuthenticationRequest; 25use MediaWiki\Auth\AuthenticationResponse; 26use MediaWiki\Auth\AuthManager; 27use MediaWiki\Logger\LoggerFactory; 28 29/** 30 * Unit to authenticate log-in attempts to the current wiki. 31 * 32 * @ingroup API 33 */ 34class ApiLogin extends ApiBase { 35 36 /** @var AuthManager */ 37 private $authManager; 38 39 /** 40 * @param ApiMain $main 41 * @param string $action 42 * @param AuthManager $authManager 43 */ 44 public function __construct( 45 ApiMain $main, 46 $action, 47 AuthManager $authManager 48 ) { 49 parent::__construct( $main, $action, 'lg' ); 50 $this->authManager = $authManager; 51 } 52 53 protected function getExtendedDescription() { 54 if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) { 55 return 'apihelp-login-extended-description'; 56 } else { 57 return 'apihelp-login-extended-description-nobotpasswords'; 58 } 59 } 60 61 /** 62 * Format a message for the response 63 * @param Message|string|array $message 64 * @return string|array 65 */ 66 private function formatMessage( $message ) { 67 $message = Message::newFromSpecifier( $message ); 68 $errorFormatter = $this->getErrorFormatter(); 69 if ( $errorFormatter instanceof ApiErrorFormatter_BackCompat ) { 70 return ApiErrorFormatter::stripMarkup( 71 $message->useDatabase( false )->inLanguage( 'en' )->text() 72 ); 73 } else { 74 return $errorFormatter->formatMessage( $message ); 75 } 76 } 77 78 /** 79 * Executes the log-in attempt using the parameters passed. If 80 * the log-in succeeds, it attaches a cookie to the session 81 * and outputs the user id, username, and session token. If a 82 * log-in fails, as the result of a bad password, a nonexistent 83 * user, or any other reason, the host is cached with an expiry 84 * and no log-in attempts will be accepted until that expiry 85 * is reached. The expiry is $this->mLoginThrottle. 86 */ 87 public function execute() { 88 // If we're in a mode that breaks the same-origin policy, no tokens can 89 // be obtained 90 if ( $this->lacksSameOriginSecurity() ) { 91 $this->getResult()->addValue( null, 'login', [ 92 'result' => 'Aborted', 93 'reason' => $this->formatMessage( 'api-login-fail-sameorigin' ), 94 ] ); 95 96 return; 97 } 98 99 $this->requirePostedParameters( [ 'password', 'token' ] ); 100 101 $params = $this->extractRequestParams(); 102 103 $result = []; 104 105 // Make sure session is persisted 106 $session = MediaWiki\Session\SessionManager::getGlobalSession(); 107 $session->persist(); 108 109 // Make sure it's possible to log in 110 if ( !$session->canSetUser() ) { 111 $this->getResult()->addValue( null, 'login', [ 112 'result' => 'Aborted', 113 'reason' => $this->formatMessage( [ 114 'api-login-fail-badsessionprovider', 115 $session->getProvider()->describe( $this->getErrorFormatter()->getLanguage() ), 116 ] ) 117 ] ); 118 119 return; 120 } 121 122 $authRes = false; 123 $loginType = 'N/A'; 124 125 // Check login token 126 $token = $session->getToken( '', 'login' ); 127 if ( !$params['token'] ) { 128 $authRes = 'NeedToken'; 129 } elseif ( $token->wasNew() ) { 130 $authRes = 'Failed'; 131 $message = ApiMessage::create( 'authpage-cannot-login-continue', 'sessionlost' ); 132 } elseif ( !$token->match( $params['token'] ) ) { 133 $authRes = 'WrongToken'; 134 } 135 136 // Try bot passwords 137 if ( 138 $authRes === false && $this->getConfig()->get( 'EnableBotPasswords' ) && 139 ( $botLoginData = BotPassword::canonicalizeLoginData( $params['name'], $params['password'] ) ) 140 ) { 141 $status = BotPassword::login( 142 $botLoginData[0], $botLoginData[1], $this->getRequest() 143 ); 144 if ( $status->isOK() ) { 145 $session = $status->getValue(); 146 $authRes = 'Success'; 147 $loginType = 'BotPassword'; 148 } elseif ( 149 $status->hasMessage( 'login-throttled' ) || 150 $status->hasMessage( 'botpasswords-needs-reset' ) || 151 $status->hasMessage( 'botpasswords-locked' ) 152 ) { 153 $authRes = 'Failed'; 154 $message = $status->getMessage(); 155 LoggerFactory::getInstance( 'authentication' )->info( 156 'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' ) 157 ); 158 } 159 // For other errors, let's see if it's a valid non-bot login 160 } 161 162 if ( $authRes === false ) { 163 // Simplified AuthManager login, for backwards compatibility 164 $reqs = AuthenticationRequest::loadRequestsFromSubmission( 165 $this->authManager->getAuthenticationRequests( 166 AuthManager::ACTION_LOGIN, 167 $this->getUser() 168 ), 169 [ 170 'username' => $params['name'], 171 'password' => $params['password'], 172 'domain' => $params['domain'], 173 'rememberMe' => true, 174 ] 175 ); 176 $res = $this->authManager->beginAuthentication( $reqs, 'null:' ); 177 switch ( $res->status ) { 178 case AuthenticationResponse::PASS: 179 if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) { 180 $this->addDeprecation( 'apiwarn-deprecation-login-botpw', 'main-account-login' ); 181 } else { 182 $this->addDeprecation( 'apiwarn-deprecation-login-nobotpw', 'main-account-login' ); 183 } 184 $authRes = 'Success'; 185 $loginType = 'AuthManager'; 186 break; 187 188 case AuthenticationResponse::FAIL: 189 // Hope it's not a PreAuthenticationProvider that failed... 190 $authRes = 'Failed'; 191 $message = $res->message; 192 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) 193 ->info( __METHOD__ . ': Authentication failed: ' 194 . $message->inLanguage( 'en' )->plain() ); 195 break; 196 197 default: 198 \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) 199 ->info( __METHOD__ . ': Authentication failed due to unsupported response type: ' 200 . $res->status, $this->getAuthenticationResponseLogData( $res ) ); 201 $authRes = 'Aborted'; 202 break; 203 } 204 } 205 206 $result['result'] = $authRes; 207 switch ( $authRes ) { 208 case 'Success': 209 $user = $session->getUser(); 210 211 // Deprecated hook 212 $injected_html = ''; 213 $this->getHookRunner()->onUserLoginComplete( $user, $injected_html, true ); 214 215 $result['lguserid'] = $user->getId(); 216 $result['lgusername'] = $user->getName(); 217 break; 218 219 case 'NeedToken': 220 $result['token'] = $token->toString(); 221 $this->addDeprecation( 'apiwarn-deprecation-login-token', 'action=login&!lgtoken' ); 222 break; 223 224 case 'WrongToken': 225 break; 226 227 case 'Failed': 228 $result['reason'] = $this->formatMessage( $message ); 229 break; 230 231 case 'Aborted': 232 $result['reason'] = $this->formatMessage( 233 $this->getConfig()->get( 'EnableBotPasswords' ) 234 ? 'api-login-fail-aborted' 235 : 'api-login-fail-aborted-nobotpw' 236 ); 237 break; 238 239 // @codeCoverageIgnoreStart 240 // Unreachable 241 default: 242 ApiBase::dieDebug( __METHOD__, "Unhandled case value: {$authRes}" ); 243 // @codeCoverageIgnoreEnd 244 } 245 246 $this->getResult()->addValue( null, 'login', $result ); 247 248 LoggerFactory::getInstance( 'authevents' )->info( 'Login attempt', [ 249 'event' => 'login', 250 'successful' => $authRes === 'Success', 251 'loginType' => $loginType, 252 'status' => $authRes, 253 ] ); 254 } 255 256 public function isDeprecated() { 257 return !$this->getConfig()->get( 'EnableBotPasswords' ); 258 } 259 260 public function mustBePosted() { 261 return true; 262 } 263 264 public function isReadMode() { 265 return false; 266 } 267 268 public function getAllowedParams() { 269 return [ 270 'name' => null, 271 'password' => [ 272 ApiBase::PARAM_TYPE => 'password', 273 ], 274 'domain' => null, 275 'token' => [ 276 ApiBase::PARAM_TYPE => 'string', 277 ApiBase::PARAM_REQUIRED => false, // for BC 278 ApiBase::PARAM_SENSITIVE => true, 279 ApiBase::PARAM_HELP_MSG => [ 'api-help-param-token', 'login' ], 280 ], 281 ]; 282 } 283 284 protected function getExamplesMessages() { 285 return [ 286 'action=login&lgname=user&lgpassword=password&lgtoken=123ABC' 287 => 'apihelp-login-example-login', 288 ]; 289 } 290 291 public function getHelpUrls() { 292 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login'; 293 } 294 295 /** 296 * Turns an AuthenticationResponse into a hash suitable for passing to Logger 297 * @param AuthenticationResponse $response 298 * @return array 299 */ 300 protected function getAuthenticationResponseLogData( AuthenticationResponse $response ) { 301 $ret = [ 302 'status' => $response->status, 303 ]; 304 if ( $response->message ) { 305 $ret['responseMessage'] = $response->message->inLanguage( 'en' )->plain(); 306 } 307 $reqs = [ 308 'neededRequests' => $response->neededRequests, 309 'createRequest' => $response->createRequest, 310 'linkRequest' => $response->linkRequest, 311 ]; 312 foreach ( $reqs as $k => $v ) { 313 if ( $v ) { 314 $v = is_array( $v ) ? $v : [ $v ]; 315 $reqClasses = array_unique( array_map( 'get_class', $v ) ); 316 sort( $reqClasses ); 317 $ret[$k] = implode( ', ', $reqClasses ); 318 } 319 } 320 return $ret; 321 } 322} 323