]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/session/SessionProvider.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / session / SessionProvider.php
1 <?php
2 /**
3  * MediaWiki session provider base class
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup Session
22  */
23
24 namespace MediaWiki\Session;
25
26 use Psr\Log\LoggerAwareInterface;
27 use Psr\Log\LoggerInterface;
28 use Config;
29 use Language;
30 use User;
31 use WebRequest;
32
33 /**
34  * A SessionProvider provides SessionInfo and support for Session
35  *
36  * A SessionProvider is responsible for taking a WebRequest and determining
37  * the authenticated session that it's a part of. It does this by returning an
38  * SessionInfo object with basic information about the session it thinks is
39  * associated with the request, namely the session ID and possibly the
40  * authenticated user the session belongs to.
41  *
42  * The SessionProvider also provides for updating the WebResponse with
43  * information necessary to provide the client with data that the client will
44  * send with later requests, and for populating the Vary and Key headers with
45  * the data necessary to correctly vary the cache on these client requests.
46  *
47  * An important part of the latter is indicating whether it even *can* tell the
48  * client to include such data in future requests, via the persistsSessionId()
49  * and canChangeUser() methods. The cases are (in order of decreasing
50  * commonness):
51  *  - Cannot persist ID, no changing User: The request identifies and
52  *    authenticates a particular local user, and the client cannot be
53  *    instructed to include an arbitrary session ID with future requests. For
54  *    example, OAuth or SSL certificate auth.
55  *  - Can persist ID and can change User: The client can be instructed to
56  *    return at least one piece of arbitrary data, that being the session ID.
57  *    The user identity might also be given to the client, otherwise it's saved
58  *    in the session data. For example, cookie-based sessions.
59  *  - Can persist ID but no changing User: The request uniquely identifies and
60  *    authenticates a local user, and the client can be instructed to return an
61  *    arbitrary session ID with future requests. For example, HTTP Digest
62  *    authentication might somehow use the 'opaque' field as a session ID
63  *    (although getting MediaWiki to return 401 responses without breaking
64  *    other stuff might be a challenge).
65  *  - Cannot persist ID but can change User: I can't think of a way this
66  *    would make sense.
67  *
68  * Note that many methods that are technically "cannot persist ID" could be
69  * turned into "can persist ID but not change User" using a session cookie,
70  * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
71  * session cookie names should be used for different providers to avoid
72  * collisions.
73  *
74  * @ingroup Session
75  * @since 1.27
76  * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
77  */
78 abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
79
80         /** @var LoggerInterface */
81         protected $logger;
82
83         /** @var Config */
84         protected $config;
85
86         /** @var SessionManager */
87         protected $manager;
88
89         /** @var int Session priority. Used for the default newSessionInfo(), but
90          * could be used by subclasses too.
91          */
92         protected $priority;
93
94         /**
95          * @note To fully initialize a SessionProvider, the setLogger(),
96          *  setConfig(), and setManager() methods must be called (and should be
97          *  called in that order). Failure to do so is liable to cause things to
98          *  fail unexpectedly.
99          */
100         public function __construct() {
101                 $this->priority = SessionInfo::MIN_PRIORITY + 10;
102         }
103
104         public function setLogger( LoggerInterface $logger ) {
105                 $this->logger = $logger;
106         }
107
108         /**
109          * Set configuration
110          * @param Config $config
111          */
112         public function setConfig( Config $config ) {
113                 $this->config = $config;
114         }
115
116         /**
117          * Set the session manager
118          * @param SessionManager $manager
119          */
120         public function setManager( SessionManager $manager ) {
121                 $this->manager = $manager;
122         }
123
124         /**
125          * Get the session manager
126          * @return SessionManager
127          */
128         public function getManager() {
129                 return $this->manager;
130         }
131
132         /**
133          * Provide session info for a request
134          *
135          * If no session exists for the request, return null. Otherwise return an
136          * SessionInfo object identifying the session.
137          *
138          * If multiple SessionProviders provide sessions, the one with highest
139          * priority wins. In case of a tie, an exception is thrown.
140          * SessionProviders are encouraged to make priorities user-configurable
141          * unless only max-priority makes sense.
142          *
143          * @warning This will be called early in the MediaWiki setup process,
144          *  before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
145          *  pieces of the main RequestContext are set up! If you try to use these,
146          *  things *will* break.
147          * @note The SessionProvider must not attempt to auto-create users.
148          *  MediaWiki will do this later (when it's safe) if the chosen session has
149          *  a user with a valid name but no ID.
150          * @protected For use by \MediaWiki\Session\SessionManager only
151          * @param WebRequest $request
152          * @return SessionInfo|null
153          */
154         abstract public function provideSessionInfo( WebRequest $request );
155
156         /**
157          * Provide session info for a new, empty session
158          *
159          * Return null if such a session cannot be created. This base
160          * implementation assumes that it only makes sense if a session ID can be
161          * persisted and changing users is allowed.
162          *
163          * @protected For use by \MediaWiki\Session\SessionManager only
164          * @param string|null $id ID to force for the new session
165          * @return SessionInfo|null
166          *  If non-null, must return true for $info->isIdSafe(); pass true for
167          *  $data['idIsSafe'] to ensure this.
168          */
169         public function newSessionInfo( $id = null ) {
170                 if ( $this->canChangeUser() && $this->persistsSessionId() ) {
171                         return new SessionInfo( $this->priority, [
172                                 'id' => $id,
173                                 'provider' => $this,
174                                 'persisted' => false,
175                                 'idIsSafe' => true,
176                         ] );
177                 }
178                 return null;
179         }
180
181         /**
182          * Merge saved session provider metadata
183          *
184          * This method will be used to compare the metadata returned by
185          * provideSessionInfo() with the saved metadata (which has been returned by
186          * provideSessionInfo() the last time the session was saved), and merge the two
187          * into the new saved metadata, or abort if the current request is not a valid
188          * continuation of the session.
189          *
190          * The default implementation checks that anything in both arrays is
191          * identical, then returns $providedMetadata.
192          *
193          * @protected For use by \MediaWiki\Session\SessionManager only
194          * @param array $savedMetadata Saved provider metadata
195          * @param array $providedMetadata Provided provider metadata (from the SessionInfo)
196          * @return array Resulting metadata
197          * @throws MetadataMergeException If the metadata cannot be merged.
198          *  Such exceptions will be handled by SessionManager and are a safe way of rejecting
199          *  a suspicious or incompatible session. The provider is expected to write an
200          *  appropriate message to its logger.
201          */
202         public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
203                 foreach ( $providedMetadata as $k => $v ) {
204                         if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
205                                 $e = new MetadataMergeException( "Key \"$k\" changed" );
206                                 $e->setContext( [
207                                         'old_value' => $savedMetadata[$k],
208                                         'new_value' => $v,
209                                 ] );
210                                 throw $e;
211                         }
212                 }
213                 return $providedMetadata;
214         }
215
216         /**
217          * Validate a loaded SessionInfo and refresh provider metadata
218          *
219          * This is similar in purpose to the 'SessionCheckInfo' hook, and also
220          * allows for updating the provider metadata. On failure, the provider is
221          * expected to write an appropriate message to its logger.
222          *
223          * @protected For use by \MediaWiki\Session\SessionManager only
224          * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here.
225          * @param WebRequest $request
226          * @param array|null &$metadata Provider metadata, may be altered.
227          * @return bool Return false to reject the SessionInfo after all.
228          */
229         public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
230                 return true;
231         }
232
233         /**
234          * Indicate whether self::persistSession() can save arbitrary session IDs
235          *
236          * If false, any session passed to self::persistSession() will have an ID
237          * that was originally provided by self::provideSessionInfo().
238          *
239          * If true, the provider may be passed sessions with arbitrary session IDs,
240          * and will be expected to manipulate the request in such a way that future
241          * requests will cause self::provideSessionInfo() to provide a SessionInfo
242          * with that ID.
243          *
244          * For example, a session provider for OAuth would function by matching the
245          * OAuth headers to a particular user, and then would use self::hashToSessionId()
246          * to turn the user and OAuth client ID (and maybe also the user token and
247          * client secret) into a session ID, and therefore can't easily assign that
248          * user+client a different ID. Similarly, a session provider for SSL client
249          * certificates would function by matching the certificate to a particular
250          * user, and then would use self::hashToSessionId() to turn the user and
251          * certificate fingerprint into a session ID, and therefore can't easily
252          * assign a different ID either. On the other hand, a provider that saves
253          * the session ID into a cookie can easily just set the cookie to a
254          * different value.
255          *
256          * @protected For use by \MediaWiki\Session\SessionBackend only
257          * @return bool
258          */
259         abstract public function persistsSessionId();
260
261         /**
262          * Indicate whether the user associated with the request can be changed
263          *
264          * If false, any session passed to self::persistSession() will have a user
265          * that was originally provided by self::provideSessionInfo(). Further,
266          * self::provideSessionInfo() may only provide sessions that have a user
267          * already set.
268          *
269          * If true, the provider may be passed sessions with arbitrary users, and
270          * will be expected to manipulate the request in such a way that future
271          * requests will cause self::provideSessionInfo() to provide a SessionInfo
272          * with that ID. This can be as simple as not passing any 'userInfo' into
273          * SessionInfo's constructor, in which case SessionInfo will load the user
274          * from the saved session's metadata.
275          *
276          * For example, a session provider for OAuth or SSL client certificates
277          * would function by matching the OAuth headers or certificate to a
278          * particular user, and thus would return false here since it can't
279          * arbitrarily assign those OAuth credentials or that certificate to a
280          * different user. A session provider that shoves information into cookies,
281          * on the other hand, could easily do so.
282          *
283          * @protected For use by \MediaWiki\Session\SessionBackend only
284          * @return bool
285          */
286         abstract public function canChangeUser();
287
288         /**
289          * Returns the duration (in seconds) for which users will be remembered when
290          * Session::setRememberUser() is set. Null means setting the remember flag will
291          * have no effect (and endpoints should not offer that option).
292          * @return int|null
293          */
294         public function getRememberUserDuration() {
295                 return null;
296         }
297
298         /**
299          * Notification that the session ID was reset
300          *
301          * No need to persist here, persistSession() will be called if appropriate.
302          *
303          * @protected For use by \MediaWiki\Session\SessionBackend only
304          * @param SessionBackend $session Session to persist
305          * @param string $oldId Old session ID
306          * @codeCoverageIgnore
307          */
308         public function sessionIdWasReset( SessionBackend $session, $oldId ) {
309         }
310
311         /**
312          * Persist a session into a request/response
313          *
314          * For example, you might set cookies for the session's ID, user ID, user
315          * name, and user token on the passed request.
316          *
317          * To correctly persist a user independently of the session ID, the
318          * provider should persist both the user ID (or name, but preferably the
319          * ID) and the user token. When reading the data from the request, it
320          * should construct a User object from the ID/name and then verify that the
321          * User object's token matches the token included in the request. Should
322          * the tokens not match, an anonymous user *must* be passed to
323          * SessionInfo::__construct().
324          *
325          * When persisting a user independently of the session ID,
326          * $session->shouldRememberUser() should be checked first. If this returns
327          * false, the user token *must not* be saved to cookies. The user name
328          * and/or ID may be persisted, and should be used to construct an
329          * unverified UserInfo to pass to SessionInfo::__construct().
330          *
331          * A backend that cannot persist sesison ID or user info should implement
332          * this as a no-op.
333          *
334          * @protected For use by \MediaWiki\Session\SessionBackend only
335          * @param SessionBackend $session Session to persist
336          * @param WebRequest $request Request into which to persist the session
337          */
338         abstract public function persistSession( SessionBackend $session, WebRequest $request );
339
340         /**
341          * Remove any persisted session from a request/response
342          *
343          * For example, blank and expire any cookies set by self::persistSession().
344          *
345          * A backend that cannot persist sesison ID or user info should implement
346          * this as a no-op.
347          *
348          * @protected For use by \MediaWiki\Session\SessionManager only
349          * @param WebRequest $request Request from which to remove any session data
350          */
351         abstract public function unpersistSession( WebRequest $request );
352
353         /**
354          * Prevent future sessions for the user
355          *
356          * If the provider is capable of returning a SessionInfo with a verified
357          * UserInfo for the named user in some manner other than by validating
358          * against $user->getToken(), steps must be taken to prevent that from
359          * occurring in the future. This might add the username to a blacklist, or
360          * it might just delete whatever authentication credentials would allow
361          * such a session in the first place (e.g. remove all OAuth grants or
362          * delete record of the SSL client certificate).
363          *
364          * The intention is that the named account will never again be usable for
365          * normal login (i.e. there is no way to undo the prevention of access).
366          *
367          * Note that the passed user name might not exist locally (i.e.
368          * User::idFromName( $username ) === 0); the name should still be
369          * prevented, if applicable.
370          *
371          * @protected For use by \MediaWiki\Session\SessionManager only
372          * @param string $username
373          */
374         public function preventSessionsForUser( $username ) {
375                 if ( !$this->canChangeUser() ) {
376                         throw new \BadMethodCallException(
377                                 __METHOD__ . ' must be implmented when canChangeUser() is false'
378                         );
379                 }
380         }
381
382         /**
383          * Invalidate existing sessions for a user
384          *
385          * If the provider has its own equivalent of CookieSessionProvider's Token
386          * cookie (and doesn't use User::getToken() to implement it), it should
387          * reset whatever token it does use here.
388          *
389          * @protected For use by \MediaWiki\Session\SessionManager only
390          * @param User $user
391          */
392         public function invalidateSessionsForUser( User $user ) {
393         }
394
395         /**
396          * Return the HTTP headers that need varying on.
397          *
398          * The return value is such that someone could theoretically do this:
399          * @code
400          * foreach ( $provider->getVaryHeaders() as $header => $options ) {
401          *   $outputPage->addVaryHeader( $header, $options );
402          * }
403          * @endcode
404          *
405          * @protected For use by \MediaWiki\Session\SessionManager only
406          * @return array
407          */
408         public function getVaryHeaders() {
409                 return [];
410         }
411
412         /**
413          * Return the list of cookies that need varying on.
414          * @protected For use by \MediaWiki\Session\SessionManager only
415          * @return string[]
416          */
417         public function getVaryCookies() {
418                 return [];
419         }
420
421         /**
422          * Get a suggested username for the login form
423          * @protected For use by \MediaWiki\Session\SessionBackend only
424          * @param WebRequest $request
425          * @return string|null
426          */
427         public function suggestLoginUsername( WebRequest $request ) {
428                 return null;
429         }
430
431         /**
432          * Fetch the rights allowed the user when the specified session is active.
433          *
434          * This is mainly meant for allowing the user to restrict access to the account
435          * by certain methods; you probably want to use this with MWGrants. The returned
436          * rights will be intersected with the user's actual rights.
437          *
438          * @param SessionBackend $backend
439          * @return null|string[] Allowed user rights, or null to allow all.
440          */
441         public function getAllowedUserRights( SessionBackend $backend ) {
442                 if ( $backend->getProvider() !== $this ) {
443                         // Not that this should ever happen...
444                         throw new \InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
445                 }
446
447                 return null;
448         }
449
450         /**
451          * @note Only override this if it makes sense to instantiate multiple
452          *  instances of the provider. Value returned must be unique across
453          *  configured providers. If you override this, you'll likely need to
454          *  override self::describeMessage() as well.
455          * @return string
456          */
457         public function __toString() {
458                 return static::class;
459         }
460
461         /**
462          * Return a Message identifying this session type
463          *
464          * This default implementation takes the class name, lowercases it,
465          * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
466          * determine the message key. For example, MediaWiki\Session\CookieSessionProvider
467          * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
468          *
469          * @note If self::__toString() is overridden, this will likely need to be
470          *  overridden as well.
471          * @warning This will be called early during MediaWiki startup. Do not
472          *  use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
473          *  RequestContext from this method!
474          * @return \Message
475          */
476         protected function describeMessage() {
477                 return wfMessage(
478                         'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) )
479                 );
480         }
481
482         public function describe( Language $lang ) {
483                 $msg = $this->describeMessage();
484                 $msg->inLanguage( $lang );
485                 if ( $msg->isDisabled() ) {
486                         $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
487                 }
488                 return $msg->plain();
489         }
490
491         public function whyNoSession() {
492                 return null;
493         }
494
495         /**
496          * Hash data as a session ID
497          *
498          * Generally this will only be used when self::persistsSessionId() is false and
499          * the provider has to base the session ID on the verified user's identity
500          * or other static data. The SessionInfo should then typically have the
501          * 'forceUse' flag set to avoid persistent session failure if validation of
502          * the stored data fails.
503          *
504          * @param string $data
505          * @param string|null $key Defaults to $this->config->get( 'SecretKey' )
506          * @return string
507          */
508         final protected function hashToSessionId( $data, $key = null ) {
509                 if ( !is_string( $data ) ) {
510                         throw new \InvalidArgumentException(
511                                 '$data must be a string, ' . gettype( $data ) . ' was passed'
512                         );
513                 }
514                 if ( $key !== null && !is_string( $key ) ) {
515                         throw new \InvalidArgumentException(
516                                 '$key must be a string or null, ' . gettype( $key ) . ' was passed'
517                         );
518                 }
519
520                 $hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
521                 if ( strlen( $hash ) < 32 ) {
522                         // Should never happen, even md5 is 128 bits
523                         // @codeCoverageIgnoreStart
524                         throw new \UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
525                         // @codeCoverageIgnoreEnd
526                 }
527                 if ( strlen( $hash ) >= 40 ) {
528                         $hash = \Wikimedia\base_convert( $hash, 16, 32, 32 );
529                 }
530                 return substr( $hash, -32 );
531         }
532
533 }