3 * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com",
4 * Daniel Cannon (cannon dot danielc at gmail dot com)
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.
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.
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
24 namespace MediaWiki\Api
;
26 use MediaWiki\Auth\AuthenticationRequest
;
27 use MediaWiki\Auth\AuthenticationResponse
;
28 use MediaWiki\Auth\AuthManager
;
29 use MediaWiki\Logger\LoggerFactory
;
30 use MediaWiki\MainConfigNames
;
31 use MediaWiki\Message\Message
;
32 use MediaWiki\Session\SessionManager
;
33 use MediaWiki\User\BotPassword
;
34 use MediaWiki\User\UserIdentityUtils
;
35 use Wikimedia\ParamValidator\ParamValidator
;
38 * Unit to authenticate log-in attempts to the current wiki.
42 class ApiLogin
extends ApiBase
{
44 private AuthManager
$authManager;
46 private UserIdentityUtils
$identityUtils;
49 * @param ApiMain $main
50 * @param string $action
51 * @param AuthManager $authManager
52 * @param UserIdentityUtils $identityUtils IdentityUtils to retrieve account type
54 public function __construct(
57 AuthManager
$authManager,
58 UserIdentityUtils
$identityUtils
60 parent
::__construct( $main, $action, 'lg' );
61 $this->authManager
= $authManager;
62 $this->identityUtils
= $identityUtils;
65 protected function getExtendedDescription() {
66 if ( $this->getConfig()->get( MainConfigNames
::EnableBotPasswords
) ) {
67 return 'apihelp-login-extended-description';
69 return 'apihelp-login-extended-description-nobotpasswords';
74 * Format a message for the response
75 * @param Message|string|array $message
76 * @return string|array
78 private function formatMessage( $message ) {
79 $message = Message
::newFromSpecifier( $message );
80 $errorFormatter = $this->getErrorFormatter();
81 if ( $errorFormatter instanceof ApiErrorFormatter_BackCompat
) {
82 return ApiErrorFormatter
::stripMarkup(
83 $message->useDatabase( false )->inLanguage( 'en' )->text()
86 return $errorFormatter->formatMessage( $message );
91 * Executes the log-in attempt using the parameters passed. If
92 * the log-in succeeds, it attaches a cookie to the session
93 * and outputs the user id, username, and session token. If a
94 * log-in fails, as the result of a bad password, a nonexistent
95 * user, or any other reason, the host is cached with an expiry
96 * and no log-in attempts will be accepted until that expiry
97 * is reached. The expiry is $this->mLoginThrottle.
99 public function execute() {
100 // If we're in a mode that breaks the same-origin policy, no tokens can
102 if ( $this->lacksSameOriginSecurity() ) {
103 $this->getResult()->addValue( null, 'login', [
104 'result' => 'Aborted',
105 'reason' => $this->formatMessage( 'api-login-fail-sameorigin' ),
111 $this->requirePostedParameters( [ 'password', 'token' ] );
113 $params = $this->extractRequestParams();
117 // Make sure session is persisted
118 $session = SessionManager
::getGlobalSession();
121 // Make sure it's possible to log in
122 if ( !$session->canSetUser() ) {
123 $this->getResult()->addValue( null, 'login', [
124 'result' => 'Aborted',
125 'reason' => $this->formatMessage( [
126 'api-login-fail-badsessionprovider',
127 $session->getProvider()->describe( $this->getErrorFormatter()->getLanguage() ),
136 $performer = $this->getUser();
139 $token = $session->getToken( '', 'login' );
140 if ( !$params['token'] ) {
141 $authRes = 'NeedToken';
142 } elseif ( $token->wasNew() ) {
144 $message = ApiMessage
::create( 'authpage-cannot-login-continue', 'sessionlost' );
145 } elseif ( !$token->match( $params['token'] ) ) {
146 $authRes = 'WrongToken';
150 if ( $authRes === false && $this->getConfig()->get( MainConfigNames
::EnableBotPasswords
) ) {
151 $botLoginData = BotPassword
::canonicalizeLoginData( $params['name'], $params['password'] );
152 if ( $botLoginData ) {
153 $status = BotPassword
::login(
154 $botLoginData[0], $botLoginData[1], $this->getRequest()
156 if ( $status->isOK() ) {
157 $session = $status->getValue();
158 $authRes = 'Success';
159 $loginType = 'BotPassword';
161 $status->hasMessage( 'login-throttled' ) ||
162 $status->hasMessage( 'botpasswords-needs-reset' ) ||
163 $status->hasMessage( 'botpasswords-locked' )
166 $message = $status->getMessage();
167 LoggerFactory
::getInstance( 'authentication' )->info(
168 'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' )
172 // For other errors, let's see if it's a valid non-bot login
175 if ( $authRes === false ) {
176 // Simplified AuthManager login, for backwards compatibility
177 $reqs = AuthenticationRequest
::loadRequestsFromSubmission(
178 $this->authManager
->getAuthenticationRequests(
179 AuthManager
::ACTION_LOGIN
,
183 'username' => $params['name'],
184 'password' => $params['password'],
185 'domain' => $params['domain'],
186 'rememberMe' => true,
189 $res = $this->authManager
->beginAuthentication( $reqs, 'null:' );
190 switch ( $res->status
) {
191 case AuthenticationResponse
::PASS
:
192 if ( $this->getConfig()->get( MainConfigNames
::EnableBotPasswords
) ) {
193 $this->addDeprecation( 'apiwarn-deprecation-login-botpw', 'main-account-login' );
195 $this->addDeprecation( 'apiwarn-deprecation-login-nobotpw', 'main-account-login' );
197 $authRes = 'Success';
198 $loginType = 'AuthManager';
201 case AuthenticationResponse
::FAIL
:
202 // Hope it's not a PreAuthenticationProvider that failed...
204 $message = $res->message
;
205 LoggerFactory
::getInstance( 'authentication' )
206 ->info( __METHOD__
. ': Authentication failed: '
207 . $message->inLanguage( 'en' )->plain() );
211 LoggerFactory
::getInstance( 'authentication' )
212 ->info( __METHOD__
. ': Authentication failed due to unsupported response type: '
213 . $res->status
, $this->getAuthenticationResponseLogData( $res ) );
214 $authRes = 'Aborted';
219 $result['result'] = $authRes;
220 switch ( $authRes ) {
222 $user = $session->getUser();
226 $this->getHookRunner()->onUserLoginComplete( $user, $injected_html, true );
228 $result['lguserid'] = $user->getId();
229 $result['lgusername'] = $user->getName();
233 $result['token'] = $token->toString();
234 $this->addDeprecation( 'apiwarn-deprecation-login-token', 'action=login&!lgtoken' );
241 // @phan-suppress-next-next-line PhanTypeMismatchArgumentNullable,PhanPossiblyUndeclaredVariable
242 // message set on error
243 $result['reason'] = $this->formatMessage( $message );
247 $result['reason'] = $this->formatMessage(
248 $this->getConfig()->get( MainConfigNames
::EnableBotPasswords
)
249 ?
'api-login-fail-aborted'
250 : 'api-login-fail-aborted-nobotpw'
254 // @codeCoverageIgnoreStart
257 ApiBase
::dieDebug( __METHOD__
, "Unhandled case value: {$authRes}" );
258 // @codeCoverageIgnoreEnd
261 $this->getResult()->addValue( null, 'login', $result );
263 LoggerFactory
::getInstance( 'authevents' )->info( 'Login attempt', [
265 'successful' => $authRes === 'Success',
266 'accountType' => $this->identityUtils
->getShortUserTypeInternal( $performer ),
267 'loginType' => $loginType,
268 'status' => $authRes,
272 public function isDeprecated() {
273 return !$this->getConfig()->get( MainConfigNames
::EnableBotPasswords
);
276 public function mustBePosted() {
280 public function isReadMode() {
284 public function isWriteMode() {
285 // (T283394) Logging in triggers some database writes, so should be marked appropriately.
289 public function getAllowedParams() {
293 ParamValidator
::PARAM_TYPE
=> 'password',
297 ParamValidator
::PARAM_TYPE
=> 'string',
298 ParamValidator
::PARAM_REQUIRED
=> false, // for BC
299 ParamValidator
::PARAM_SENSITIVE
=> true,
300 ApiBase
::PARAM_HELP_MSG
=> [ 'api-help-param-token', 'login' ],
305 protected function getExamplesMessages() {
307 'action=login&lgname=user&lgpassword=password&lgtoken=123ABC'
308 => 'apihelp-login-example-login',
312 public function getHelpUrls() {
313 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Login';
317 * Turns an AuthenticationResponse into a hash suitable for passing to Logger
318 * @param AuthenticationResponse $response
321 protected function getAuthenticationResponseLogData( AuthenticationResponse
$response ) {
323 'status' => $response->status
,
325 if ( $response->message
) {
326 $ret['responseMessage'] = $response->message
->inLanguage( 'en' )->plain();
329 'neededRequests' => $response->neededRequests
,
330 'createRequest' => $response->createRequest
,
331 'linkRequest' => $response->linkRequest
,
333 foreach ( $reqs as $k => $v ) {
335 $v = is_array( $v ) ?
$v : [ $v ];
336 $reqClasses = array_unique( array_map( 'get_class', $v ) );
338 $ret[$k] = implode( ', ', $reqClasses );
345 /** @deprecated class alias since 1.43 */
346 class_alias( ApiLogin
::class, 'ApiLogin' );