MediaWiki  master
TemporaryPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1 <?php
22 namespace MediaWiki\Auth;
23 
24 use SpecialPage;
25 use User;
26 
40 {
42  protected $emailEnabled = null;
43 
45  protected $newPasswordExpiry = null;
46 
48  protected $passwordReminderResendTime = null;
49 
51  protected $allowRequiringEmail = null;
52 
60  public function __construct( $params = [] ) {
61  parent::__construct( $params );
62 
63  if ( isset( $params['emailEnabled'] ) ) {
64  $this->emailEnabled = (bool)$params['emailEnabled'];
65  }
66  if ( isset( $params['newPasswordExpiry'] ) ) {
67  $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
68  }
69  if ( isset( $params['passwordReminderResendTime'] ) ) {
70  $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
71  }
72  if ( isset( $params['allowRequiringEmailForResets'] ) ) {
73  $this->allowRequiringEmail = $params['allowRequiringEmailForResets'];
74  }
75  }
76 
77  public function setConfig( \Config $config ) {
78  parent::setConfig( $config );
79 
80  if ( $this->emailEnabled === null ) {
81  $this->emailEnabled = $this->config->get( 'EnableEmail' );
82  }
83  if ( $this->newPasswordExpiry === null ) {
84  $this->newPasswordExpiry = $this->config->get( 'NewPasswordExpiry' );
85  }
86  if ( $this->passwordReminderResendTime === null ) {
87  $this->passwordReminderResendTime = $this->config->get( 'PasswordReminderResendTime' );
88  }
89  if ( $this->allowRequiringEmail === null ) {
90  $this->allowRequiringEmail = $this->config->get( 'AllowRequiringEmailForResets' );
91  }
92  }
93 
94  protected function getPasswordResetData( $username, $data ) {
95  // Always reset
96  return (object)[
97  'msg' => wfMessage( 'resetpass-temp-emailed' ),
98  'hard' => true,
99  ];
100  }
101 
102  public function getAuthenticationRequests( $action, array $options ) {
103  switch ( $action ) {
105  return [ new PasswordAuthenticationRequest() ];
106 
109 
111  if ( isset( $options['username'] ) && $this->emailEnabled ) {
112  // Creating an account for someone else
114  } else {
115  // It's not terribly likely that an anonymous user will
116  // be creating an account for someone else.
117  return [];
118  }
119 
122 
123  default:
124  return [];
125  }
126  }
127 
128  public function beginPrimaryAuthentication( array $reqs ) {
129  $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
130  if ( !$req || $req->username === null || $req->password === null ) {
132  }
133 
134  $username = User::getCanonicalName( $req->username, 'usable' );
135  if ( $username === false ) {
137  }
138 
139  $dbr = wfGetDB( DB_REPLICA );
140  $row = $dbr->selectRow(
141  'user',
142  [
143  'user_id', 'user_newpassword', 'user_newpass_time',
144  ],
145  [ 'user_name' => $username ],
146  __METHOD__
147  );
148  if ( !$row ) {
150  }
151 
152  $status = $this->checkPasswordValidity( $username, $req->password );
153  if ( !$status->isOK() ) {
154  // Fatal, can't log in
155  return AuthenticationResponse::newFail( $status->getMessage() );
156  }
157 
158  $pwhash = $this->getPassword( $row->user_newpassword );
159  if ( !$pwhash->verify( $req->password ) ) {
160  return $this->failResponse( $req );
161  }
162 
163  if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
164  return $this->failResponse( $req );
165  }
166 
167  // Add an extra log entry since a temporary password is
168  // an unusual way to log in, so its important to keep track
169  // of in case of abuse.
170  $this->logger->info( "{user} successfully logged in using temp password",
171  [
172  'user' => $username,
173  'requestIP' => $this->manager->getRequest()->getIP()
174  ]
175  );
176 
177  $this->setPasswordResetFlag( $username, $status );
178 
179  return AuthenticationResponse::newPass( $username );
180  }
181 
182  public function testUserCanAuthenticate( $username ) {
183  $username = User::getCanonicalName( $username, 'usable' );
184  if ( $username === false ) {
185  return false;
186  }
187 
188  $dbr = wfGetDB( DB_REPLICA );
189  $row = $dbr->selectRow(
190  'user',
191  [ 'user_newpassword', 'user_newpass_time' ],
192  [ 'user_name' => $username ],
193  __METHOD__
194  );
195  if ( !$row ) {
196  return false;
197  }
198 
199  if ( $this->getPassword( $row->user_newpassword ) instanceof \InvalidPassword ) {
200  return false;
201  }
202 
203  if ( !$this->isTimestampValid( $row->user_newpass_time ) ) {
204  return false;
205  }
206 
207  return true;
208  }
209 
210  public function testUserExists( $username, $flags = User::READ_NORMAL ) {
211  $username = User::getCanonicalName( $username, 'usable' );
212  if ( $username === false ) {
213  return false;
214  }
215 
216  list( $db, $options ) = \DBAccessObjectUtils::getDBOptions( $flags );
217  return (bool)wfGetDB( $db )->selectField(
218  [ 'user' ],
219  'user_id',
220  [ 'user_name' => $username ],
221  __METHOD__,
222  $options
223  );
224  }
225 
227  AuthenticationRequest $req, $checkData = true
228  ) {
229  if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
230  // We don't really ignore it, but this is what the caller expects.
231  return \StatusValue::newGood( 'ignored' );
232  }
233 
234  if ( !$checkData ) {
235  return \StatusValue::newGood();
236  }
237 
238  $username = User::getCanonicalName( $req->username, 'usable' );
239  if ( $username === false ) {
240  return \StatusValue::newGood( 'ignored' );
241  }
242 
243  $row = wfGetDB( DB_MASTER )->selectRow(
244  'user',
245  [ 'user_id', 'user_newpass_time' ],
246  [ 'user_name' => $username ],
247  __METHOD__
248  );
249 
250  if ( !$row ) {
251  return \StatusValue::newGood( 'ignored' );
252  }
253 
254  $sv = \StatusValue::newGood();
255  if ( $req->password !== null ) {
256  $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
257 
258  if ( $req->mailpassword ) {
259  if ( !$this->emailEnabled ) {
260  return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
261  }
262 
263  // We don't check whether the user has an email address;
264  // that information should not be exposed to the caller.
265 
266  // do not allow temporary password creation within
267  // $wgPasswordReminderResendTime from the last attempt
268  if (
269  $this->passwordReminderResendTime
270  && $row->user_newpass_time
271  && time() < wfTimestamp( TS_UNIX, $row->user_newpass_time )
272  + $this->passwordReminderResendTime * 3600
273  ) {
274  // Round the time in hours to 3 d.p., in case someone is specifying
275  // minutes or seconds.
276  return \StatusValue::newFatal( 'throttled-mailpassword',
277  round( $this->passwordReminderResendTime, 3 ) );
278  }
279 
280  if ( !$req->caller ) {
281  return \StatusValue::newFatal( 'passwordreset-nocaller' );
282  }
283  if ( !\IP::isValid( $req->caller ) ) {
284  $caller = User::newFromName( $req->caller );
285  if ( !$caller ) {
286  return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
287  }
288  }
289  }
290  }
291  return $sv;
292  }
293 
295  $username = $req->username !== null ? User::getCanonicalName( $req->username, 'usable' ) : false;
296  if ( $username === false ) {
297  return;
298  }
299 
300  $dbw = wfGetDB( DB_MASTER );
301 
302  $sendMail = false;
303  if ( $req->action !== AuthManager::ACTION_REMOVE &&
304  get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
305  ) {
306  $pwhash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
307  $newpassTime = $dbw->timestamp();
308  $sendMail = $req->mailpassword;
309  } else {
310  // Invalidate the temporary password when any other auth is reset, or when removing
311  $pwhash = $this->getPasswordFactory()->newFromCiphertext( null );
312  $newpassTime = null;
313  }
314 
315  $dbw->update(
316  'user',
317  [
318  'user_newpassword' => $pwhash->toString(),
319  'user_newpass_time' => $newpassTime,
320  ],
321  [ 'user_name' => $username ],
322  __METHOD__
323  );
324 
325  if ( $sendMail ) {
326  // Send email after DB commit
327  $dbw->onTransactionCommitOrIdle(
328  function () use ( $req ) {
330  $this->sendPasswordResetEmail( $req );
331  },
332  __METHOD__
333  );
334  }
335  }
336 
337  public function accountCreationType() {
338  return self::TYPE_CREATE;
339  }
340 
341  public function testForAccountCreation( $user, $creator, array $reqs ) {
344  $reqs, TemporaryPasswordAuthenticationRequest::class
345  );
346 
347  $ret = \StatusValue::newGood();
348  if ( $req ) {
349  if ( $req->mailpassword ) {
350  if ( !$this->emailEnabled ) {
351  $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
352  } elseif ( !$user->getEmail() ) {
353  $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
354  }
355  }
356 
357  $ret->merge(
358  $this->checkPasswordValidity( $user->getName(), $req->password )
359  );
360  }
361  return $ret;
362  }
363 
364  public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
367  $reqs, TemporaryPasswordAuthenticationRequest::class
368  );
369  if ( $req && $req->username !== null && $req->password !== null ) {
370  // Nothing we can do yet, because the user isn't in the DB yet
371  if ( $req->username !== $user->getName() ) {
372  $req = clone $req;
373  $req->username = $user->getName();
374  }
375 
376  if ( $req->mailpassword ) {
377  // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
378  $this->manager->setAuthenticationSessionData( 'no-email', true );
379  }
380 
381  $ret = AuthenticationResponse::newPass( $req->username );
382  $ret->createRequest = $req;
383  return $ret;
384  }
386  }
387 
388  public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
390  $req = $res->createRequest;
391  $mailpassword = $req->mailpassword;
392  $req->mailpassword = false; // providerChangeAuthenticationData would send the wrong email
393 
394  // Now that the user is in the DB, set the password on it.
395  $this->providerChangeAuthenticationData( $req );
396 
397  if ( $mailpassword ) {
398  // Send email after DB commit
399  wfGetDB( DB_MASTER )->onTransactionCommitOrIdle(
400  function () use ( $user, $creator, $req ) {
401  $this->sendNewAccountEmail( $user, $creator, $req->password );
402  },
403  __METHOD__
404  );
405  }
406 
407  return $mailpassword ? 'byemail' : null;
408  }
409 
415  protected function isTimestampValid( $timestamp ) {
416  $time = wfTimestampOrNull( TS_MW, $timestamp );
417  if ( $time !== null ) {
418  $expiry = wfTimestamp( TS_UNIX, $time ) + $this->newPasswordExpiry;
419  if ( time() >= $expiry ) {
420  return false;
421  }
422  }
423  return true;
424  }
425 
433  protected function sendNewAccountEmail( User $user, User $creatingUser, $password ) {
434  $ip = $creatingUser->getRequest()->getIP();
435  // @codeCoverageIgnoreStart
436  if ( !$ip ) {
437  return \Status::newFatal( 'badipaddress' );
438  }
439  // @codeCoverageIgnoreEnd
440 
441  \Hooks::run( 'User::mailPasswordInternal', [ &$creatingUser, &$ip, &$user ] );
442 
443  $mainPageUrl = \Title::newMainPage()->getCanonicalURL();
444  $userLanguage = $user->getOption( 'language' );
445  $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
446  $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
447  '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
448  ->inLanguage( $userLanguage );
449 
450  $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
451 
452  // TODO show 'mailerror' message on error, 'accmailtext' success message otherwise?
453  // @codeCoverageIgnoreStart
454  if ( !$status->isGood() ) {
455  $this->logger->warning( 'Could not send account creation email: ' .
456  $status->getWikiText( false, false, 'en' ) );
457  }
458  // @codeCoverageIgnoreEnd
459 
460  return $status;
461  }
462 
468  $user = User::newFromName( $req->username );
469  if ( !$user ) {
470  return \Status::newFatal( 'noname' );
471  }
472  $userLanguage = $user->getOption( 'language' );
473  $callerIsAnon = \IP::isValid( $req->caller );
474  $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
475  $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
476  $req->password )->inLanguage( $userLanguage );
477  $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
478  : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
479  $body = $emailMessage->params( $callerName, $passwordMessage->text(), 1,
480  '<' . \Title::newMainPage()->getCanonicalURL() . '>',
481  round( $this->newPasswordExpiry / 86400 ) )->text();
482 
483  if ( $this->allowRequiringEmail && !$user->getBoolOption( 'requireemail' ) ) {
484  $body .= "\n\n";
485  $url = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-personal-email' )
486  ->getFullURL();
487  $body .= wfMessage( 'passwordreset-emailtext-require-email' )
488  ->inLanguage( $userLanguage )
489  ->params( "<$url>" )
490  ->text();
491  }
492 
493  $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
494  return $user->sendMail( $emailTitle->text(), $body );
495  }
496 }
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:69
checkPasswordValidity( $username, $password)
Check that the password is valid.
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:646
providerChangeAuthenticationData(AuthenticationRequest $req)
Change or remove authentication data (e.g.
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user&#39;s current setting for a given option.
Definition: User.php:2969
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user&#39;s account.
Definition: User.php:4548
Represents an invalid password hash.
string $action
Cache what action this request is.
Definition: MediaWiki.php:42
This is a value object to hold authentication response data.
sendNewAccountEmail(User $user, User $creatingUser, $password)
Send an email about the new account creation and the temporary password.
testForAccountCreation( $user, $creator, array $reqs)
Determine whether an account creation may begin.
wfTimestampOrNull( $outputtype=TS_UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
finishAccountCreation( $user, $creator, AuthenticationResponse $res)
Post-creation callback.
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
getName()
Get the user name, or the IP of an anonymous user.
Definition: User.php:2283
setPasswordResetFlag( $username, Status $status, $data=null)
Check if the password should be reset.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
static getCanonicalName( $name, $validate='valid')
Given unvalidated user input, return a canonical username, or false if the username is invalid...
Definition: User.php:1194
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
testUserExists( $username, $flags=User::READ_NORMAL)
Test whether the named user exists.
Interface for configuration instances.
Definition: Config.php:28
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
static isValid( $ip)
Validate an IP address.
Definition: IP.php:111
const ACTION_CHANGE
Change a user&#39;s credentials.
A primary authentication provider that uses the temporary password field in the &#39;user&#39; table...
Basic framework for a primary authentication provider that uses passwords.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
This represents the intention to set a temporary password for the user.
This is a value object for authentication requests with a username and password.
providerAllowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
getRequest()
Get the WebRequest object to use with this object.
Definition: User.php:3702
getAuthenticationRequests( $action, array $options)
Return the applicable list of AuthenticationRequests.Possible values for $action depend on whether th...
static newRandom()
Return an instance with a new, random password.
const ACTION_REMOVE
Remove a user&#39;s credentials.
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const ACTION_CREATE
Create a new user.
Definition: AuthManager.php:93
const DB_REPLICA
Definition: defines.php:25
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
failResponse(PasswordAuthenticationRequest $req)
Return the appropriate response for failure.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
Definition: AuthManager.php:87
isTimestampValid( $timestamp)
Check that a temporary password is still valid (hasn&#39;t expired).
This is a value object for authentication requests.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200