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