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