MediaWiki REL1_37
TemporaryPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1<?php
22namespace MediaWiki\Auth;
23
26use SpecialPage;
27use User;
28use Wikimedia\IPUtils;
30
44{
46 protected $emailEnabled = null;
47
49 protected $newPasswordExpiry = null;
50
53
55 protected $allowRequiringEmail = null;
56
59
68 public function __construct( ILoadBalancer $loadBalancer, $params = [] ) {
69 parent::__construct( $params );
70
71 if ( isset( $params['emailEnabled'] ) ) {
72 $this->emailEnabled = (bool)$params['emailEnabled'];
73 }
74 if ( isset( $params['newPasswordExpiry'] ) ) {
75 $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
76 }
77 if ( isset( $params['passwordReminderResendTime'] ) ) {
78 $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
79 }
80 if ( isset( $params['allowRequiringEmailForResets'] ) ) {
81 $this->allowRequiringEmail = $params['allowRequiringEmailForResets'];
82 }
83 $this->loadBalancer = $loadBalancer;
84 }
85
86 protected function postInitSetup() {
87 if ( $this->emailEnabled === null ) {
88 $this->emailEnabled = $this->config->get( 'EnableEmail' );
89 }
90 if ( $this->newPasswordExpiry === null ) {
91 $this->newPasswordExpiry = $this->config->get( 'NewPasswordExpiry' );
92 }
93 if ( $this->passwordReminderResendTime === null ) {
94 $this->passwordReminderResendTime = $this->config->get( 'PasswordReminderResendTime' );
95 }
96 if ( $this->allowRequiringEmail === null ) {
97 $this->allowRequiringEmail = $this->config->get( 'AllowRequiringEmailForResets' );
98 }
99 }
100
101 protected function getPasswordResetData( $username, $data ) {
102 // Always reset
103 return (object)[
104 'msg' => wfMessage( 'resetpass-temp-emailed' ),
105 'hard' => true,
106 ];
107 }
108
109 public function getAuthenticationRequests( $action, array $options ) {
110 switch ( $action ) {
112 return [ new PasswordAuthenticationRequest() ];
113
116
118 if ( isset( $options['username'] ) && $this->emailEnabled ) {
119 // Creating an account for someone else
121 } else {
122 // It's not terribly likely that an anonymous user will
123 // be creating an account for someone else.
124 return [];
125 }
126
129
130 default:
131 return [];
132 }
133 }
134
135 public function beginPrimaryAuthentication( array $reqs ) {
136 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
137 if ( !$req || $req->username === null || $req->password === null ) {
139 }
140
141 $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE );
142 if ( $username === false ) {
144 }
145
146 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
147 $row = $dbr->selectRow(
148 'user',
149 [
150 'user_id', 'user_newpassword', 'user_newpass_time',
151 ],
152 [ 'user_name' => $username ],
153 __METHOD__
154 );
155 if ( !$row ) {
157 }
158
159 $status = $this->checkPasswordValidity( $username, $req->password );
160 if ( !$status->isOK() ) {
161 // Fatal, can't log in
162 return AuthenticationResponse::newFail( $status->getMessage() );
163 }
164
165 $pwhash = $this->getPassword( $row->user_newpassword );
166 if ( !$pwhash->verify( $req->password ) ) {
167 return $this->failResponse( $req );
168 }
169
170 if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
171 return $this->failResponse( $req );
172 }
173
174 // Add an extra log entry since a temporary password is
175 // an unusual way to log in, so its important to keep track
176 // of in case of abuse.
177 $this->logger->info( "{user} successfully logged in using temp password",
178 [
179 'user' => $username,
180 'requestIP' => $this->manager->getRequest()->getIP()
181 ]
182 );
183
184 $this->setPasswordResetFlag( $username, $status );
185
186 return AuthenticationResponse::newPass( $username );
187 }
188
189 public function testUserCanAuthenticate( $username ) {
190 $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE );
191 if ( $username === false ) {
192 return false;
193 }
194
195 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
196 $row = $dbr->selectRow(
197 'user',
198 [ 'user_newpassword', 'user_newpass_time' ],
199 [ 'user_name' => $username ],
200 __METHOD__
201 );
202 if ( !$row ) {
203 return false;
204 }
205
206 if ( $this->getPassword( $row->user_newpassword ) instanceof \InvalidPassword ) {
207 return false;
208 }
209
210 if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
211 return false;
212 }
213
214 return true;
215 }
216
217 public function testUserExists( $username, $flags = User::READ_NORMAL ) {
218 $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE );
219 if ( $username === false ) {
220 return false;
221 }
222
223 list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
224 return (bool)$this->loadBalancer->getConnectionRef( $db )->selectField(
225 [ 'user' ],
226 'user_id',
227 [ 'user_name' => $username ],
228 __METHOD__,
229 $options
230 );
231 }
232
234 AuthenticationRequest $req, $checkData = true
235 ) {
236 if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
237 // We don't really ignore it, but this is what the caller expects.
238 return \StatusValue::newGood( 'ignored' );
239 }
240
241 if ( !$checkData ) {
242 return \StatusValue::newGood();
243 }
244
245 $username = $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE );
246 if ( $username === false ) {
247 return \StatusValue::newGood( 'ignored' );
248 }
249
250 $row = $this->loadBalancer->getConnectionRef( DB_PRIMARY )->selectRow(
251 'user',
252 [ 'user_id', 'user_newpass_time' ],
253 [ 'user_name' => $username ],
254 __METHOD__
255 );
256
257 if ( !$row ) {
258 return \StatusValue::newGood( 'ignored' );
259 }
260
261 $sv = \StatusValue::newGood();
262 if ( $req->password !== null ) {
263 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
264
265 if ( $req->mailpassword ) {
266 if ( !$this->emailEnabled ) {
267 return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
268 }
269
270 // We don't check whether the user has an email address;
271 // that information should not be exposed to the caller.
272
273 // do not allow temporary password creation within
274 // $wgPasswordReminderResendTime from the last attempt
275 if (
276 $this->passwordReminderResendTime
277 && $row->user_newpass_time
278 && time() < (int)wfTimestamp( TS_UNIX, $row->user_newpass_time )
279 + $this->passwordReminderResendTime * 3600
280 ) {
281 // Round the time in hours to 3 d.p., in case someone is specifying
282 // minutes or seconds.
283 return \StatusValue::newFatal( 'throttled-mailpassword',
284 round( $this->passwordReminderResendTime, 3 ) );
285 }
286
287 if ( !$req->caller ) {
288 return \StatusValue::newFatal( 'passwordreset-nocaller' );
289 }
290 if ( !IPUtils::isValid( $req->caller ) ) {
291 $caller = User::newFromName( $req->caller );
292 if ( !$caller ) {
293 return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
294 }
295 }
296 }
297 }
298 return $sv;
299 }
300
302 $username = $req->username !== null ?
303 $this->userNameUtils->getCanonical( $req->username, UserNameUtils::RIGOR_USABLE ) : false;
304 if ( $username === false ) {
305 return;
306 }
307
308 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
309
310 $sendMail = false;
311 if ( $req->action !== AuthManager::ACTION_REMOVE &&
312 get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
313 ) {
314 $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
315 $newpassTime = $dbw->timestamp();
316 $sendMail = $req->mailpassword;
317 } else {
318 // Invalidate the temporary password when any other auth is reset, or when removing
319 $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
320 $newpassTime = null;
321 }
322
323 $dbw->update(
324 'user',
325 [
326 'user_newpassword' => $pwhash->toString(),
327 'user_newpass_time' => $newpassTime,
328 ],
329 [ 'user_name' => $username ],
330 __METHOD__
331 );
332
333 if ( $sendMail ) {
334 // Send email after DB commit
335 $dbw->onTransactionCommitOrIdle(
336 function () use ( $req ) {
338 $this->sendPasswordResetEmail( $req );
339 },
340 __METHOD__
341 );
342 }
343 }
344
345 public function accountCreationType() {
346 return self::TYPE_CREATE;
347 }
348
349 public function testForAccountCreation( $user, $creator, array $reqs ) {
352 $reqs, TemporaryPasswordAuthenticationRequest::class
353 );
354
355 $ret = \StatusValue::newGood();
356 if ( $req ) {
357 if ( $req->mailpassword ) {
358 if ( !$this->emailEnabled ) {
359 $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
360 } elseif ( !$user->getEmail() ) {
361 $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
362 }
363 }
364
365 $ret->merge(
366 $this->checkPasswordValidity( $user->getName(), $req->password )
367 );
368 }
369 return $ret;
370 }
371
372 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
375 $reqs, TemporaryPasswordAuthenticationRequest::class
376 );
377 if ( $req && $req->username !== null && $req->password !== null ) {
378 // Nothing we can do yet, because the user isn't in the DB yet
379 if ( $req->username !== $user->getName() ) {
380 $req = clone $req;
381 $req->username = $user->getName();
382 }
383
384 if ( $req->mailpassword ) {
385 // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
386 $this->manager->setAuthenticationSessionData( 'no-email', true );
387 }
388
389 $ret = AuthenticationResponse::newPass( $req->username );
390 $ret->createRequest = $req;
391 return $ret;
392 }
394 }
395
396 public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
398 $req = $res->createRequest;
399 $mailpassword = $req->mailpassword;
400 $req->mailpassword = false; // providerChangeAuthenticationData would send the wrong email
401
402 // Now that the user is in the DB, set the password on it.
404
405 if ( $mailpassword ) {
406 // Send email after DB commit
407 $this->loadBalancer->getConnectionRef( DB_PRIMARY )->onTransactionCommitOrIdle(
408 function () use ( $user, $creator, $req ) {
409 $this->sendNewAccountEmail( $user, $creator, $req->password );
410 },
411 __METHOD__
412 );
413 }
414
415 return $mailpassword ? 'byemail' : null;
416 }
417
423 protected function isTimestampValid( $timestamp ) {
424 $time = wfTimestampOrNull( TS_MW, $timestamp );
425 if ( $time !== null ) {
426 $expiry = (int)wfTimestamp( TS_UNIX, $time ) + $this->newPasswordExpiry;
427 if ( time() >= $expiry ) {
428 return false;
429 }
430 }
431 return true;
432 }
433
441 protected function sendNewAccountEmail( User $user, User $creatingUser, $password ) {
442 $ip = $creatingUser->getRequest()->getIP();
443 // @codeCoverageIgnoreStart
444 if ( !$ip ) {
445 return \Status::newFatal( 'badipaddress' );
446 }
447 // @codeCoverageIgnoreEnd
448
449 $this->getHookRunner()->onUser__mailPasswordInternal( $creatingUser, $ip, $user );
450
451 $mainPageUrl = \Title::newMainPage()->getCanonicalURL();
452 $userLanguage = $user->getOption( 'language' );
453 $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
454 $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
455 '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
456 ->inLanguage( $userLanguage );
457
458 $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
459
460 // TODO show 'mailerror' message on error, 'accmailtext' success message otherwise?
461 // @codeCoverageIgnoreStart
462 if ( !$status->isGood() ) {
463 $this->logger->warning( 'Could not send account creation email: ' .
464 $status->getWikiText( false, false, 'en' ) );
465 }
466 // @codeCoverageIgnoreEnd
467
468 return $status;
469 }
470
476 $user = User::newFromName( $req->username );
477 if ( !$user ) {
478 return \Status::newFatal( 'noname' );
479 }
480 $userLanguage = $user->getOption( 'language' );
481 $callerIsAnon = IPUtils::isValid( $req->caller );
482 $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
483 $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
484 $req->password )->inLanguage( $userLanguage );
485 $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
486 : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
487 $body = $emailMessage->params( $callerName, $passwordMessage->text(), 1,
488 '<' . \Title::newMainPage()->getCanonicalURL() . '>',
489 round( $this->newPasswordExpiry / 86400 ) )->text();
490
491 if ( $this->allowRequiringEmail && !MediaWikiServices::getInstance()->getUserOptionsLookup()
492 ->getBoolOption( $user, 'requireemail' )
493 ) {
494 $body .= "\n\n";
495 $url = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-personal-email' )
496 ->getCanonicalURL();
497 $body .= wfMessage( 'passwordreset-emailtext-require-email' )
498 ->inLanguage( $userLanguage )
499 ->params( "<$url>" )
500 ->text();
501 }
502
503 $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
504 return $user->sendMail( $emailTitle->text(), $body );
505 }
506}
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.
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.
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...
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
UserNameUtils service.
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,...
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
getRequest()
Get the WebRequest object to use with this object.
Definition User.php:3075
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2116
static newFromName( $name, $validate='valid')
Definition User.php:607
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:2556
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user's account.
Definition User.php:3815
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition defines.php:25
const DB_PRIMARY
Definition defines.php:27