MediaWiki REL1_39
TemporaryPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Auth;
23
27use SpecialPage;
28use User;
29use Wikimedia\IPUtils;
31
45{
47 protected $emailEnabled = null;
48
50 protected $newPasswordExpiry = null;
51
54
56 protected $allowRequiringEmail = null;
57
59 private $loadBalancer;
60
62 private $userOptionsLookup;
63
73 public function __construct(
74 ILoadBalancer $loadBalancer,
75 UserOptionsLookup $userOptionsLookup,
76 $params = []
77 ) {
78 parent::__construct( $params );
79
80 if ( isset( $params['emailEnabled'] ) ) {
81 $this->emailEnabled = (bool)$params['emailEnabled'];
82 }
83 if ( isset( $params['newPasswordExpiry'] ) ) {
84 $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
85 }
86 if ( isset( $params['passwordReminderResendTime'] ) ) {
87 $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
88 }
89 if ( isset( $params['allowRequiringEmailForResets'] ) ) {
90 $this->allowRequiringEmail = $params['allowRequiringEmailForResets'];
91 }
92 $this->loadBalancer = $loadBalancer;
93 $this->userOptionsLookup = $userOptionsLookup;
94 }
95
96 protected function postInitSetup() {
97 if ( $this->emailEnabled === null ) {
98 $this->emailEnabled = $this->config->get( MainConfigNames::EnableEmail );
99 }
100 if ( $this->newPasswordExpiry === null ) {
101 $this->newPasswordExpiry = $this->config->get( MainConfigNames::NewPasswordExpiry );
102 }
103 if ( $this->passwordReminderResendTime === null ) {
104 $this->passwordReminderResendTime =
106 }
107 if ( $this->allowRequiringEmail === null ) {
108 $this->allowRequiringEmail =
110 }
111 }
112
113 protected function getPasswordResetData( $username, $data ) {
114 // Always reset
115 return (object)[
116 'msg' => wfMessage( 'resetpass-temp-emailed' ),
117 'hard' => true,
118 ];
119 }
120
121 public function getAuthenticationRequests( $action, array $options ) {
122 switch ( $action ) {
124 return [ new PasswordAuthenticationRequest() ];
125
128
130 if ( isset( $options['username'] ) && $this->emailEnabled ) {
131 // Creating an account for someone else
133 } else {
134 // It's not terribly likely that an anonymous user will
135 // be creating an account for someone else.
136 return [];
137 }
138
141
142 default:
143 return [];
144 }
145 }
146
147 public function beginPrimaryAuthentication( array $reqs ) {
148 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
149 if ( !$req || $req->username === null || $req->password === null ) {
151 }
152
153 $username = $this->userNameUtils->getCanonical(
154 $req->username, UserRigorOptions::RIGOR_USABLE );
155 if ( $username === false ) {
157 }
158
159 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
160 $row = $dbr->selectRow(
161 'user',
162 [
163 'user_id', 'user_newpassword', 'user_newpass_time',
164 ],
165 [ 'user_name' => $username ],
166 __METHOD__
167 );
168 if ( !$row ) {
170 }
171
172 $status = $this->checkPasswordValidity( $username, $req->password );
173 if ( !$status->isOK() ) {
174 // Fatal, can't log in
175 return AuthenticationResponse::newFail( $status->getMessage() );
176 }
177
178 $pwhash = $this->getPassword( $row->user_newpassword );
179 if ( !$pwhash->verify( $req->password ) ) {
180 return $this->failResponse( $req );
181 }
182
183 if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
184 return $this->failResponse( $req );
185 }
186
187 // Add an extra log entry since a temporary password is
188 // an unusual way to log in, so its important to keep track
189 // of in case of abuse.
190 $this->logger->info( "{user} successfully logged in using temp password",
191 [
192 'user' => $username,
193 'requestIP' => $this->manager->getRequest()->getIP()
194 ]
195 );
196
197 $this->setPasswordResetFlag( $username, $status );
198
199 return AuthenticationResponse::newPass( $username );
200 }
201
202 public function testUserCanAuthenticate( $username ) {
203 $username = $this->userNameUtils->getCanonical( $username, UserRigorOptions::RIGOR_USABLE );
204 if ( $username === false ) {
205 return false;
206 }
207
208 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
209 $row = $dbr->selectRow(
210 'user',
211 [ 'user_newpassword', 'user_newpass_time' ],
212 [ 'user_name' => $username ],
213 __METHOD__
214 );
215 if ( !$row ) {
216 return false;
217 }
218
219 if ( $this->getPassword( $row->user_newpassword ) instanceof \InvalidPassword ) {
220 return false;
221 }
222
223 if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
224 return false;
225 }
226
227 return true;
228 }
229
230 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
231 $username = $this->userNameUtils->getCanonical( $username, UserRigorOptions::RIGOR_USABLE );
232 if ( $username === false ) {
233 return false;
234 }
235
236 list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
237 return (bool)$this->loadBalancer->getConnectionRef( $db )->selectField(
238 [ 'user' ],
239 'user_id',
240 [ 'user_name' => $username ],
241 __METHOD__,
242 $options
243 );
244 }
245
247 AuthenticationRequest $req, $checkData = true
248 ) {
249 if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
250 // We don't really ignore it, but this is what the caller expects.
251 return \StatusValue::newGood( 'ignored' );
252 }
253
254 if ( !$checkData ) {
255 return \StatusValue::newGood();
256 }
257
258 $username = $this->userNameUtils->getCanonical(
259 $req->username, UserRigorOptions::RIGOR_USABLE );
260 if ( $username === false ) {
261 return \StatusValue::newGood( 'ignored' );
262 }
263
264 $row = $this->loadBalancer->getConnectionRef( DB_PRIMARY )->selectRow(
265 'user',
266 [ 'user_id', 'user_newpass_time' ],
267 [ 'user_name' => $username ],
268 __METHOD__
269 );
270
271 if ( !$row ) {
272 return \StatusValue::newGood( 'ignored' );
273 }
274
275 $sv = \StatusValue::newGood();
276 if ( $req->password !== null ) {
277 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
278
279 if ( $req->mailpassword ) {
280 if ( !$this->emailEnabled ) {
281 return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
282 }
283
284 // We don't check whether the user has an email address;
285 // that information should not be exposed to the caller.
286
287 // do not allow temporary password creation within
288 // $wgPasswordReminderResendTime from the last attempt
289 if (
290 $this->passwordReminderResendTime
291 && $row->user_newpass_time
292 && time() < (int)wfTimestamp( TS_UNIX, $row->user_newpass_time )
293 + $this->passwordReminderResendTime * 3600
294 ) {
295 // Round the time in hours to 3 d.p., in case someone is specifying
296 // minutes or seconds.
297 return \StatusValue::newFatal( 'throttled-mailpassword',
298 round( $this->passwordReminderResendTime, 3 ) );
299 }
300
301 if ( !$req->caller ) {
302 return \StatusValue::newFatal( 'passwordreset-nocaller' );
303 }
304 if ( !IPUtils::isValid( $req->caller ) ) {
305 $caller = User::newFromName( $req->caller );
306 if ( !$caller ) {
307 return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
308 }
309 }
310 }
311 }
312 return $sv;
313 }
314
316 $username = $req->username !== null ?
317 $this->userNameUtils->getCanonical( $req->username, UserRigorOptions::RIGOR_USABLE ) : false;
318 if ( $username === false ) {
319 return;
320 }
321
322 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
323
324 $sendMail = false;
325 if ( $req->action !== AuthManager::ACTION_REMOVE &&
326 get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
327 ) {
328 $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
329 $newpassTime = $dbw->timestamp();
330 $sendMail = $req->mailpassword;
331 } else {
332 // Invalidate the temporary password when any other auth is reset, or when removing
333 $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
334 $newpassTime = null;
335 }
336
337 $dbw->update(
338 'user',
339 [
340 'user_newpassword' => $pwhash->toString(),
341 'user_newpass_time' => $newpassTime,
342 ],
343 [ 'user_name' => $username ],
344 __METHOD__
345 );
346
347 if ( $sendMail ) {
348 // Send email after DB commit
349 $dbw->onTransactionCommitOrIdle(
350 function () use ( $req ) {
352 $this->sendPasswordResetEmail( $req );
353 },
354 __METHOD__
355 );
356 }
357 }
358
359 public function accountCreationType() {
360 return self::TYPE_CREATE;
361 }
362
363 public function testForAccountCreation( $user, $creator, array $reqs ) {
366 $reqs, TemporaryPasswordAuthenticationRequest::class
367 );
368
369 $ret = \StatusValue::newGood();
370 if ( $req ) {
371 if ( $req->mailpassword ) {
372 if ( !$this->emailEnabled ) {
373 $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
374 } elseif ( !$user->getEmail() ) {
375 $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
376 }
377 }
378
379 $ret->merge(
380 $this->checkPasswordValidity( $user->getName(), $req->password )
381 );
382 }
383 return $ret;
384 }
385
386 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
389 $reqs, TemporaryPasswordAuthenticationRequest::class
390 );
391 if ( $req && $req->username !== null && $req->password !== null ) {
392 // Nothing we can do yet, because the user isn't in the DB yet
393 if ( $req->username !== $user->getName() ) {
394 $req = clone $req;
395 $req->username = $user->getName();
396 }
397
398 if ( $req->mailpassword ) {
399 // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
400 $this->manager->setAuthenticationSessionData( 'no-email', true );
401 }
402
403 $ret = AuthenticationResponse::newPass( $req->username );
404 $ret->createRequest = $req;
405 return $ret;
406 }
408 }
409
410 public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
412 $req = $res->createRequest;
413 $mailpassword = $req->mailpassword;
414 $req->mailpassword = false; // providerChangeAuthenticationData would send the wrong email
415
416 // Now that the user is in the DB, set the password on it.
418
419 if ( $mailpassword ) {
420 // Send email after DB commit
421 $this->loadBalancer->getConnectionRef( DB_PRIMARY )->onTransactionCommitOrIdle(
422 function () use ( $user, $creator, $req ) {
423 $this->sendNewAccountEmail( $user, $creator, $req->password );
424 },
425 __METHOD__
426 );
427 }
428
429 return $mailpassword ? 'byemail' : null;
430 }
431
437 protected function isTimestampValid( $timestamp ) {
438 $time = wfTimestampOrNull( TS_MW, $timestamp );
439 if ( $time !== null ) {
440 $expiry = (int)wfTimestamp( TS_UNIX, $time ) + $this->newPasswordExpiry;
441 if ( time() >= $expiry ) {
442 return false;
443 }
444 }
445 return true;
446 }
447
455 protected function sendNewAccountEmail( User $user, User $creatingUser, $password ) {
456 $ip = $creatingUser->getRequest()->getIP();
457 // @codeCoverageIgnoreStart
458 if ( !$ip ) {
459 return \Status::newFatal( 'badipaddress' );
460 }
461 // @codeCoverageIgnoreEnd
462
463 $this->getHookRunner()->onUser__mailPasswordInternal( $creatingUser, $ip, $user );
464
465 $mainPageUrl = \Title::newMainPage()->getCanonicalURL();
466 $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' );
467 $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
468 $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
469 '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
470 ->inLanguage( $userLanguage );
471
472 $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
473
474 // TODO show 'mailerror' message on error, 'accmailtext' success message otherwise?
475 // @codeCoverageIgnoreStart
476 if ( !$status->isGood() ) {
477 $this->logger->warning( 'Could not send account creation email: ' .
478 $status->getWikiText( false, false, 'en' ) );
479 }
480 // @codeCoverageIgnoreEnd
481
482 return $status;
483 }
484
490 $user = User::newFromName( $req->username );
491 if ( !$user ) {
492 return \Status::newFatal( 'noname' );
493 }
494 $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' );
495 $callerIsAnon = IPUtils::isValid( $req->caller );
496 $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
497 $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
498 $req->password )->inLanguage( $userLanguage );
499 $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
500 : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
501 $body = $emailMessage->params( $callerName, $passwordMessage->text(), 1,
502 '<' . \Title::newMainPage()->getCanonicalURL() . '>',
503 round( $this->newPasswordExpiry / 86400 ) )->text();
504
505 if ( $this->allowRequiringEmail && !$this->userOptionsLookup
506 ->getBoolOption( $user, 'requireemail' )
507 ) {
508 $body .= "\n\n";
509 $url = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-personal-email' )
510 ->getCanonicalURL();
511 $body .= wfMessage( 'passwordreset-emailtext-require-email' )
512 ->inLanguage( $userLanguage )
513 ->params( "<$url>" )
514 ->text();
515 }
516
517 $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
518 return $user->sendMail( $emailTitle->text(), $body );
519 }
520}
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Represents an invalid password hash.
Basic framework for a primary authentication provider that uses passwords.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
setPasswordResetFlag( $username, Status $status, $data=null)
Check if the password should be reset.
const ACTION_CHANGE
Change a user's credentials.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
const ACTION_CREATE
Create a new user.
This is a value object for authentication requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
This is a value object to hold authentication response data.
static newFail(Message $msg, array $failReasons=[])
This is a value object for authentication requests with a username and password.
This represents the intention to set a temporary password for the user.
static newRandom()
Return an instance with a new, random password.
A primary authentication provider that uses the temporary password field in the 'user' table.
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
sendNewAccountEmail(User $user, User $creatingUser, $password)
Send an email about the new account creation and the temporary password.
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
finishAccountCreation( $user, $creator, AuthenticationResponse $res)
Post-creation callback.Called after the user is added to the database, before secondary authenticatio...
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.Called from AuthManager::beginAccountCreation()No nee...
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
__construct(ILoadBalancer $loadBalancer, UserOptionsLookup $userOptionsLookup, $params=[])
isTimestampValid( $timestamp)
Check that a temporary password is still valid (hasn't expired).
postInitSetup()
A provider can override this to do any necessary setup after init() is called.
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.Should return true if the provider ha...
A class containing constants representing the names of configuration variables.
const PasswordReminderResendTime
Name constant for the PasswordReminderResendTime setting, for use with Config::get()
const EnableEmail
Name constant for the EnableEmail setting, for use with Config::get()
const NewPasswordExpiry
Name constant for the NewPasswordExpiry setting, for use with Config::get()
const AllowRequiringEmailForResets
Name constant for the AllowRequiringEmailForResets setting, for use with Config::get()
Provides access to user options.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
internal since 1.36
Definition User.php:70
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:2415
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1679
static newFromName( $name, $validate='valid')
Definition User.php:598
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user's account.
Definition User.php:3056
Shared interface for rigor levels when dealing with User methods.
Create and track the database connections and transactions for a given database cluster.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28