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