MediaWiki master
AbstractTemporaryPasswordPrimaryAuthenticationProvider.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Auth;
8
18use Wikimedia\IPUtils;
21use Wikimedia\Timestamp\TimestampFormat as TS;
22
36{
38 protected $emailEnabled = null;
39
41 protected $newPasswordExpiry = null;
42
45
48
58 public function __construct(
61 $params = []
62 ) {
63 parent::__construct( $params );
64
65 if ( isset( $params['emailEnabled'] ) ) {
66 $this->emailEnabled = (bool)$params['emailEnabled'];
67 }
68 if ( isset( $params['newPasswordExpiry'] ) ) {
69 $this->newPasswordExpiry = (int)$params['newPasswordExpiry'];
70 }
71 if ( isset( $params['passwordReminderResendTime'] ) ) {
72 $this->passwordReminderResendTime = $params['passwordReminderResendTime'];
73 }
74 $this->dbProvider = $dbProvider;
75 $this->userOptionsLookup = $userOptionsLookup;
76 }
77
78 protected function postInitSetup() {
79 $this->emailEnabled ??= $this->config->get( MainConfigNames::EnableEmail );
80 $this->newPasswordExpiry ??= $this->config->get( MainConfigNames::NewPasswordExpiry );
81 $this->passwordReminderResendTime ??=
83 }
84
86 protected function getPasswordResetData( $username, $data ) {
87 // Always reset
88 return (object)[
89 'msg' => wfMessage( 'resetpass-temp-emailed' ),
90 'hard' => true,
91 ];
92 }
93
95 public function getAuthenticationRequests( $action, array $options ) {
96 switch ( $action ) {
98 return [ new PasswordAuthenticationRequest() ];
99
102
104 // Allow named users creating a new account to email a temporary password to a given address
105 // in case they are creating an account for somebody else.
106 // This isn't a likely scenario for account creations by anonymous or temporary users
107 // and is therefore disabled for them (T328718).
108 if (
109 isset( $options['username'] ) &&
110 !$this->userNameUtils->isTemp( $options['username'] ) &&
111 $this->emailEnabled
112 ) {
114 } else {
115 return [];
116 }
117
120
121 default:
122 return [];
123 }
124 }
125
127 public function beginPrimaryAuthentication( array $reqs ) {
128 $req = AuthenticationRequest::getRequestByClass( $reqs, PasswordAuthenticationRequest::class );
129 if ( !$req || $req->username === null || $req->password === null ) {
131 }
132
133 $username = $this->userNameUtils->getCanonical(
134 $req->username, UserRigorOptions::RIGOR_USABLE );
135 if ( $username === false ) {
137 }
138
139 [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username, IDBAccessObject::READ_LATEST );
140 if ( !$tempPassHash ) {
142 }
143
144 $status = $this->checkPasswordValidity( $username, $req->password );
145 if ( !$status->isOK() ) {
146 return $this->getFatalPasswordErrorResponse( $username, $status );
147 }
148
149 if ( !$tempPassHash->verify( $req->password ) ||
150 !$this->isTimestampValid( $tempPassTime )
151 ) {
152 return $this->failResponse( $req );
153 }
154
155 // Add an extra log entry since a temporary password is
156 // an unusual way to log in, so its important to keep track
157 // of in case of abuse.
158 $this->logger->info( "{user} successfully logged in using temp password",
159 [
160 'provider' => static::class,
161 'user' => $username,
162 'requestIP' => $this->manager->getRequest()->getIP()
163 ]
164 );
165
166 $this->setPasswordResetFlag( $username, $status );
167
168 return AuthenticationResponse::newPass( $username );
169 }
170
172 public function testUserCanAuthenticate( $username ) {
173 $username = $this->userNameUtils->getCanonical( $username, UserRigorOptions::RIGOR_USABLE );
174 if ( $username === false ) {
175 return false;
176 }
177
178 [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username );
179 return $tempPassHash &&
180 !( $tempPassHash instanceof InvalidPassword ) &&
181 $this->isTimestampValid( $tempPassTime );
182 }
183
186 AuthenticationRequest $req, $checkData = true
187 ) {
188 if ( get_class( $req ) !== TemporaryPasswordAuthenticationRequest::class ) {
189 // We don't really ignore it, but this is what the caller expects.
190 return \StatusValue::newGood( 'ignored' );
191 }
192
193 if ( !$checkData ) {
194 return \StatusValue::newGood();
195 }
196
197 $username = $this->userNameUtils->getCanonical(
198 $req->username, UserRigorOptions::RIGOR_USABLE );
199 if ( $username === false ) {
200 return \StatusValue::newGood( 'ignored' );
201 }
202
203 [ $tempPassHash, $tempPassTime ] = $this->getTemporaryPassword( $username, IDBAccessObject::READ_LATEST );
204 if ( !$tempPassHash ) {
205 return \StatusValue::newGood( 'ignored' );
206 }
207
208 $sv = \StatusValue::newGood();
209 if ( $req->password !== null ) {
210 $sv->merge( $this->checkPasswordValidity( $username, $req->password ) );
211
212 if ( $req->mailpassword ) {
213 if ( !$this->emailEnabled ) {
214 return \StatusValue::newFatal( 'passwordreset-emaildisabled' );
215 }
216
217 // We don't check whether the user has an email address;
218 // that information should not be exposed to the caller.
219
220 // do not allow temporary password creation within
221 // $wgPasswordReminderResendTime from the last attempt
222 if (
223 $this->passwordReminderResendTime
224 && $tempPassTime
225 && time() < (int)wfTimestamp( TS::UNIX, $tempPassTime )
226 + $this->passwordReminderResendTime * 3600
227 ) {
228 // Round the time in hours to 3 d.p., in case someone is specifying
229 // minutes or seconds.
230 return \StatusValue::newFatal( 'throttled-mailpassword',
231 round( $this->passwordReminderResendTime, 3 ) );
232 }
233
234 if ( !$req->caller ) {
235 return \StatusValue::newFatal( 'passwordreset-nocaller' );
236 }
237 if ( !IPUtils::isValid( $req->caller ) ) {
238 $caller = User::newFromName( $req->caller );
239 if ( !$caller ) {
240 return \StatusValue::newFatal( 'passwordreset-nosuchcaller', $req->caller );
241 }
242 }
243 }
244 }
245 return $sv;
246 }
247
249 $username = $req->username !== null ?
250 $this->userNameUtils->getCanonical( $req->username, UserRigorOptions::RIGOR_USABLE ) : false;
251 if ( $username === false ) {
252 return;
253 }
254
255 $sendMail = false;
256 if ( $req->action !== AuthManager::ACTION_REMOVE &&
257 get_class( $req ) === TemporaryPasswordAuthenticationRequest::class
258 ) {
259 $tempPassHash = $this->getPasswordFactory()->newFromPlaintext( $req->password );
260 $tempPassTime = wfTimestampNow();
261 $sendMail = $req->mailpassword;
262 // Prevent other temp password providers from sending duplicate emails
263 $req->mailpassword = false;
264 } else {
265 // Invalidate the temporary password when any other auth is reset, or when removing
266 $tempPassHash = PasswordFactory::newInvalidPassword();
267 $tempPassTime = null;
268 }
269
270 $this->setTemporaryPassword( $username, $tempPassHash, $tempPassTime );
271
272 if ( $sendMail ) {
273 $this->maybeSendPasswordResetEmail( $req );
274 }
275 }
276
278 public function accountCreationType() {
279 return self::TYPE_CREATE;
280 }
281
283 public function testForAccountCreation( $user, $creator, array $reqs ) {
286 $reqs, TemporaryPasswordAuthenticationRequest::class
287 );
288
289 $ret = \StatusValue::newGood();
290 if ( $req ) {
291 if ( $req->mailpassword ) {
292 if ( !$this->emailEnabled ) {
293 $ret->merge( \StatusValue::newFatal( 'emaildisabled' ) );
294 } elseif ( !$user->getEmail() ) {
295 $ret->merge( \StatusValue::newFatal( 'noemailcreate' ) );
296 }
297 }
298
299 $ret->merge(
300 $this->checkPasswordValidity( $user->getName(), $req->password )
301 );
302 }
303 return $ret;
304 }
305
307 public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) {
310 $reqs, TemporaryPasswordAuthenticationRequest::class
311 );
312 if ( $req && $req->username !== null && $req->password !== null ) {
313 // Nothing we can do yet, because the user isn't in the DB yet
314 if ( $req->username !== $user->getName() ) {
315 $req = clone $req;
316 $req->username = $user->getName();
317 }
318
319 if ( $req->mailpassword ) {
320 // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail
321 $this->manager->setAuthenticationSessionData( 'no-email', true );
322 }
323
324 $ret = AuthenticationResponse::newPass( $req->username );
325 $ret->createRequest = $req;
326 return $ret;
327 }
329 }
330
332 public function finishAccountCreation( $user, $creator, AuthenticationResponse $res ) {
334 $req = $res->createRequest;
335 $mailpassword = $req->mailpassword;
336 // Prevent providerChangeAuthenticationData() from sending the wrong email
337 $req->mailpassword = false;
338
339 // Now that the user is in the DB, set the password on it.
341
342 if ( $mailpassword ) {
343 $this->maybeSendNewAccountEmail( $user, $creator, $req->password );
344 }
345
346 return $mailpassword ? 'byemail' : null;
347 }
348
355 protected function isTimestampValid( $timestamp ) {
356 $time = wfTimestampOrNull( TS::MW, $timestamp );
357 if ( $time !== null ) {
358 $expiry = (int)wfTimestamp( TS::UNIX, $time ) + $this->newPasswordExpiry;
359 if ( time() >= $expiry ) {
360 return false;
361 }
362 }
363 return true;
364 }
365
377 protected function maybeSendNewAccountEmail( User $user, User $creatingUser, $password ): void {
378 // Send email after DB commit (the callback does not run in case of DB rollback)
379 $this->dbProvider->getPrimaryDatabase()->onTransactionCommitOrIdle(
380 function () use ( $user, $creatingUser, $password ) {
381 $this->sendNewAccountEmail( $user, $creatingUser, $password );
382 },
383 __METHOD__
384 );
385 }
386
395 protected function sendNewAccountEmail( User $user, User $creatingUser, $password ): void {
396 $ip = $creatingUser->getRequest()->getIP();
397 // @codeCoverageIgnoreStart
398 if ( !$ip ) {
399 return;
400 }
401 // @codeCoverageIgnoreEnd
402
403 $this->getHookRunner()->onUser__mailPasswordInternal( $creatingUser, $ip, $user );
404
405 $mainPageUrl = Title::newMainPage()->getCanonicalURL();
406 $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' );
407 $subjectMessage = wfMessage( 'createaccount-title' )->inLanguage( $userLanguage );
408 $bodyMessage = wfMessage( 'createaccount-text', $ip, $user->getName(), $password,
409 '<' . $mainPageUrl . '>', round( $this->newPasswordExpiry / 86400 ) )
410 ->inLanguage( $userLanguage );
411
412 $status = $user->sendMail( $subjectMessage->text(), $bodyMessage->text() );
413
414 // @codeCoverageIgnoreStart
415 if ( !$status->isGood() ) {
416 $this->logger->warning( 'Could not send account creation email: ' .
417 $status->getWikiText( false, false, 'en' ) );
418 }
419 // @codeCoverageIgnoreEnd
420 }
421
431 // Send email after DB commit (the callback does not run in case of DB rollback)
432 $this->dbProvider->getPrimaryDatabase()->onTransactionCommitOrIdle(
433 function () use ( $req ) {
434 $this->sendPasswordResetEmail( $req );
435 },
436 __METHOD__
437 );
438 }
439
447 $user = User::newFromName( $req->username );
448 if ( !$user ) {
449 return;
450 }
451 $userLanguage = $this->userOptionsLookup->getOption( $user, 'language' );
452 $callerIsAnon = IPUtils::isValid( $req->caller );
453 $callerName = $callerIsAnon ? $req->caller : User::newFromName( $req->caller )->getName();
454 $passwordMessage = wfMessage( 'passwordreset-emailelement', $user->getName(),
455 $req->password )->inLanguage( $userLanguage );
456 $emailMessage = wfMessage( $callerIsAnon ? 'passwordreset-emailtext-ip'
457 : 'passwordreset-emailtext-user' )->inLanguage( $userLanguage );
458 $body = $emailMessage->params( $callerName, $passwordMessage->text(), 1,
459 '<' . Title::newMainPage()->getCanonicalURL() . '>',
460 round( $this->newPasswordExpiry / 86400 ) )->text();
461
462 // Hint that the user can choose to require email address to request a temporary password
463 if (
464 !$this->userOptionsLookup->getBoolOption( $user, 'requireemail' )
465 ) {
466 $url = SpecialPage::getTitleFor( 'Preferences', false, 'mw-prefsection-personal-email' )
467 ->getCanonicalURL();
468 $body .= "\n\n" . wfMessage( 'passwordreset-emailtext-require-email' )
469 ->inLanguage( $userLanguage )
470 ->params( "<$url>" )
471 ->text();
472 }
473
474 $emailTitle = wfMessage( 'passwordreset-emailtitle' )->inLanguage( $userLanguage );
475 $user->sendMail( $emailTitle->text(), $body );
476 }
477
494 abstract protected function getTemporaryPassword( string $username, $flags = IDBAccessObject::READ_NORMAL ): array;
495
504 abstract protected function setTemporaryPassword( string $username, Password $tempPassHash, $tempPassTime ): void;
505}
wfTimestampOrNull( $outputtype=TS::UNIX, $ts=null)
Return a formatted timestamp, or null if input is null.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
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.
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.
getFatalPasswordErrorResponse(string $username, Status $status)
Adds user-friendly description to a fatal password validity check error.
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.
sendPasswordResetEmail(TemporaryPasswordAuthenticationRequest $req)
Send an email about the new temporary password.
__construct(IConnectionProvider $dbProvider, UserOptionsLookup $userOptionsLookup, $params=[])
isTimestampValid( $timestamp)
Check that a temporary password is still valid (hasn't expired).
getPasswordResetData( $username, $data)
Get password reset data, if any.to override \stdClass|null { 'hard' => bool, 'msg' => Message }
finishAccountCreation( $user, $creator, AuthenticationResponse $res)
Post-creation callback.Called after the user is added to the database, before secondary authenticatio...
maybeSendPasswordResetEmail(TemporaryPasswordAuthenticationRequest $req)
Wait for the new temporary password to be recorded, and if successful, send an email about it.
setTemporaryPassword(string $username, Password $tempPassHash, $tempPassTime)
Set a temporary password and the time when it was generated.
beginPrimaryAuthentication(array $reqs)
Start an authentication flow.AuthenticationResponse Expected responses:PASS: The user is authenticate...
postInitSetup()
A provider can override this to do any necessary setup after init() is called.
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.passwords)Return StatusValue::newGood( 'ignored' ) if y...
maybeSendNewAccountEmail(User $user, User $creatingUser, $password)
Wait for the new account to be recorded, and if successful, send an email about the new account creat...
beginPrimaryAccountCreation( $user, $creator, array $reqs)
Start an account creation flow.AuthenticationResponse Expected responses:PASS: The user may be create...
testUserCanAuthenticate( $username)
Test whether the named user can authenticate with this provider.Should return true if the provider ha...
getTemporaryPassword(string $username, $flags=IDBAccessObject::READ_NORMAL)
Return a tuple of temporary password and the time when it was generated.
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 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()
Represents an invalid password hash.
Factory class for creating and checking Password objects.
Represents a password hash for use in authentication.
Definition Password.php:52
Parent class for all special pages.
Represents a title within MediaWiki.
Definition Title.php:69
Provides access to user options.
User class for the MediaWiki software.
Definition User.php:130
sendMail( $subject, $body, $from=null, $replyto=null)
Send an e-mail to this user's account.
Definition User.php:2833
getEmail()
Get the user's e-mail address.
Definition User.php:1861
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:1524
Shared interface for rigor levels when dealing with User methods.
Provide primary and replica IDatabase connections.
Interface for database access objects.