3 * MediaWiki session provider base class
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.
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.
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
24 namespace MediaWiki\Session
;
27 use InvalidArgumentException
;
28 use MediaWiki\Api\ApiUsageException
;
29 use MediaWiki\Config\Config
;
30 use MediaWiki\Context\RequestContext
;
31 use MediaWiki\HookContainer\HookContainer
;
32 use MediaWiki\HookContainer\HookRunner
;
33 use MediaWiki\Language\Language
;
34 use MediaWiki\MainConfigNames
;
35 use MediaWiki\Message\Message
;
36 use MediaWiki\Request\WebRequest
;
37 use MediaWiki\User\User
;
38 use MediaWiki\User\UserNameUtils
;
40 use Psr\Log\LoggerInterface
;
42 use Wikimedia\Message\MessageParam
;
43 use Wikimedia\Message\MessageSpecifier
;
46 * A SessionProvider provides SessionInfo and support for Session
48 * A SessionProvider is responsible for taking a WebRequest and determining
49 * the authenticated session that it's a part of. It does this by returning an
50 * SessionInfo object with basic information about the session it thinks is
51 * associated with the request, namely the session ID and possibly the
52 * authenticated user the session belongs to.
54 * The SessionProvider also provides for updating the WebResponse with
55 * information necessary to provide the client with data that the client will
56 * send with later requests, and for populating the Vary and Key headers with
57 * the data necessary to correctly vary the cache on these client requests.
59 * An important part of the latter is indicating whether it even *can* tell the
60 * client to include such data in future requests, via the persistsSessionId()
61 * and canChangeUser() methods. The cases are (in order of decreasing
63 * - Cannot persist ID, no changing User: The request identifies and
64 * authenticates a particular local user, and the client cannot be
65 * instructed to include an arbitrary session ID with future requests. For
66 * example, OAuth or SSL certificate auth.
67 * - Can persist ID and can change User: The client can be instructed to
68 * return at least one piece of arbitrary data, that being the session ID.
69 * The user identity might also be given to the client, otherwise it's saved
70 * in the session data. For example, cookie-based sessions.
71 * - Can persist ID but no changing User: The request uniquely identifies and
72 * authenticates a local user, and the client can be instructed to return an
73 * arbitrary session ID with future requests. For example, HTTP Digest
74 * authentication might somehow use the 'opaque' field as a session ID
75 * (although getting MediaWiki to return 401 responses without breaking
76 * other stuff might be a challenge).
77 * - Cannot persist ID but can change User: I can't think of a way this
80 * Note that many methods that are technically "cannot persist ID" could be
81 * turned into "can persist ID but not change User" using a session cookie,
82 * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
83 * session cookie names should be used for different providers to avoid
89 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
91 abstract class SessionProvider
implements Stringable
, SessionProviderInterface
{
93 protected LoggerInterface
$logger;
94 protected Config
$config;
95 protected SessionManager
$manager;
96 private HookContainer
$hookContainer;
97 private HookRunner
$hookRunner;
98 protected UserNameUtils
$userNameUtils;
100 /** @var int Session priority. Used for the default newSessionInfo(), but
101 * could be used by subclasses too.
108 public function __construct() {
109 $this->priority
= SessionInfo
::MIN_PRIORITY +
10;
113 * Initialise with dependencies of a SessionProvider
116 * @internal In production code SessionManager will initialize the
117 * SessionProvider, in tests SessionProviderTestTrait must be used.
119 public function init(
120 LoggerInterface
$logger,
122 SessionManager
$manager,
123 HookContainer
$hookContainer,
124 UserNameUtils
$userNameUtils
126 $this->logger
= $logger;
127 $this->config
= $config;
128 $this->manager
= $manager;
129 $this->hookContainer
= $hookContainer;
130 $this->hookRunner
= new HookRunner( $hookContainer );
131 $this->userNameUtils
= $userNameUtils;
132 $this->postInitSetup();
136 * A provider can override this to do any necessary setup after init()
140 * @stable to override
142 protected function postInitSetup() {
146 * Sets a logger instance on the object.
148 * @deprecated since 1.37. For extension-defined session providers
149 * that were using this method to trigger other work, please override
150 * SessionProvider::postInitSetup instead. If your extension
151 * was using this to explicitly change the logger of an existing
152 * SessionProvider object, please file a report on phabricator
153 * - there is no non-deprecated way to do this anymore.
154 * @param LoggerInterface $logger
156 public function setLogger( LoggerInterface
$logger ) {
157 wfDeprecated( __METHOD__
, '1.37' );
158 $this->logger
= $logger;
164 * @deprecated since 1.37. For extension-defined session providers
165 * that were using this method to trigger other work, please override
166 * SessionProvider::postInitSetup instead. If your extension
167 * was using this to explicitly change the Config of an existing
168 * SessionProvider object, please file a report on phabricator
169 * - there is no non-deprecated way to do this anymore.
170 * @param Config $config
172 public function setConfig( Config
$config ) {
173 wfDeprecated( __METHOD__
, '1.37' );
174 $this->config
= $config;
183 protected function getConfig() {
184 return $this->config
;
188 * Set the session manager
190 * @deprecated since 1.37. For extension-defined session providers
191 * that were using this method to trigger other work, please override
192 * SessionProvider::postInitSetup instead. If your extension
193 * was using this to explicitly change the SessionManager of an existing
194 * SessionProvider object, please file a report on phabricator
195 * - there is no non-deprecated way to do this anymore.
196 * @param SessionManager $manager
198 public function setManager( SessionManager
$manager ) {
199 wfDeprecated( __METHOD__
, '1.37' );
200 $this->manager
= $manager;
204 * Get the session manager
205 * @return SessionManager
207 public function getManager() {
208 return $this->manager
;
213 * @deprecated since 1.37. For extension-defined session providers
214 * that were using this method to trigger other work, please override
215 * SessionProvider::postInitSetup instead. If your extension
216 * was using this to explicitly change the HookContainer of an existing
217 * SessionProvider object, please file a report on phabricator
218 * - there is no non-deprecated way to do this anymore.
219 * @param HookContainer $hookContainer
221 public function setHookContainer( $hookContainer ) {
222 wfDeprecated( __METHOD__
, '1.37' );
223 $this->hookContainer
= $hookContainer;
224 $this->hookRunner
= new HookRunner( $hookContainer );
228 * Get the HookContainer
230 * @return HookContainer
232 protected function getHookContainer(): HookContainer
{
233 return $this->hookContainer
;
239 * @internal This is for use by core only. Hook interfaces may be removed
244 protected function getHookRunner(): HookRunner
{
245 return $this->hookRunner
;
249 * Provide session info for a request
251 * If no session exists for the request, return null. Otherwise return an
252 * SessionInfo object identifying the session.
254 * If multiple SessionProviders provide sessions, the one with highest
255 * priority wins. In case of a tie, an exception is thrown.
256 * SessionProviders are encouraged to make priorities user-configurable
257 * unless only max-priority makes sense.
259 * @warning This will be called early in the MediaWiki setup process,
260 * before $wgUser, $wgLang, $wgOut, $wgTitle, the global parser, and
261 * corresponding pieces of the main RequestContext are set up! If you try
262 * to use these, things *will* break.
263 * @note The SessionProvider must not attempt to auto-create users.
264 * MediaWiki will do this later (when it's safe) if the chosen session has
265 * a user with a valid name but no ID.
266 * @note For use by \MediaWiki\Session\SessionManager only
267 * @param WebRequest $request
268 * @return SessionInfo|null
270 abstract public function provideSessionInfo( WebRequest
$request );
273 * Provide session info for a new, empty session
275 * Return null if such a session cannot be created. This base
276 * implementation assumes that it only makes sense if a session ID can be
277 * persisted and changing users is allowed.
278 * @stable to override
280 * @note For use by \MediaWiki\Session\SessionManager only
281 * @param string|null $id ID to force for the new session
282 * @return SessionInfo|null
283 * If non-null, must return true for $info->isIdSafe(); pass true for
284 * $data['idIsSafe'] to ensure this.
286 public function newSessionInfo( $id = null ) {
287 if ( $this->canChangeUser() && $this->persistsSessionId() ) {
288 return new SessionInfo( $this->priority
, [
291 'persisted' => false,
299 * Merge saved session provider metadata
301 * This method will be used to compare the metadata returned by
302 * provideSessionInfo() with the saved metadata (which has been returned by
303 * provideSessionInfo() the last time the session was saved), and merge the two
304 * into the new saved metadata, or abort if the current request is not a valid
305 * continuation of the session.
307 * The default implementation checks that anything in both arrays is
308 * identical, then returns $providedMetadata.
309 * @stable to override
311 * @note For use by \MediaWiki\Session\SessionManager only
312 * @param array $savedMetadata Saved provider metadata
313 * @param array $providedMetadata Provided provider metadata (from the SessionInfo)
314 * @return array Resulting metadata
315 * @throws MetadataMergeException If the metadata cannot be merged.
316 * Such exceptions will be handled by SessionManager and are a safe way of rejecting
317 * a suspicious or incompatible session. The provider is expected to write an
318 * appropriate message to its logger.
320 public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
321 foreach ( $providedMetadata as $k => $v ) {
322 if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
323 $e = new MetadataMergeException( "Key \"$k\" changed" );
325 'old_value' => $savedMetadata[$k],
331 return $providedMetadata;
335 * Validate a loaded SessionInfo and refresh provider metadata
337 * This is similar in purpose to the 'SessionCheckInfo' hook, and also
338 * allows for updating the provider metadata. On failure, the provider is
339 * expected to write an appropriate message to its logger.
340 * @stable to override
342 * @note For use by \MediaWiki\Session\SessionManager only
343 * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here.
344 * @param WebRequest $request
345 * @param array|null &$metadata Provider metadata, may be altered.
346 * @return bool Return false to reject the SessionInfo after all.
348 public function refreshSessionInfo( SessionInfo
$info, WebRequest
$request, &$metadata ) {
353 * Indicate whether self::persistSession() can save arbitrary session IDs
355 * If false, any session passed to self::persistSession() will have an ID
356 * that was originally provided by self::provideSessionInfo().
358 * If true, the provider may be passed sessions with arbitrary session IDs,
359 * and will be expected to manipulate the request in such a way that future
360 * requests will cause self::provideSessionInfo() to provide a SessionInfo
363 * For example, a session provider for OAuth would function by matching the
364 * OAuth headers to a particular user, and then would use self::hashToSessionId()
365 * to turn the user and OAuth client ID (and maybe also the user token and
366 * client secret) into a session ID, and therefore can't easily assign that
367 * user+client a different ID. Similarly, a session provider for SSL client
368 * certificates would function by matching the certificate to a particular
369 * user, and then would use self::hashToSessionId() to turn the user and
370 * certificate fingerprint into a session ID, and therefore can't easily
371 * assign a different ID either. On the other hand, a provider that saves
372 * the session ID into a cookie can easily just set the cookie to a
375 * @note For use by \MediaWiki\Session\SessionBackend only
378 abstract public function persistsSessionId();
381 * Indicate whether the user associated with the request can be changed
383 * If false, any session passed to self::persistSession() will have a user
384 * that was originally provided by self::provideSessionInfo(). Further,
385 * self::provideSessionInfo() may only provide sessions that have a user
388 * If true, the provider may be passed sessions with arbitrary users, and
389 * will be expected to manipulate the request in such a way that future
390 * requests will cause self::provideSessionInfo() to provide a SessionInfo
391 * with that ID. This can be as simple as not passing any 'userInfo' into
392 * SessionInfo's constructor, in which case SessionInfo will load the user
393 * from the saved session's metadata.
395 * For example, a session provider for OAuth or SSL client certificates
396 * would function by matching the OAuth headers or certificate to a
397 * particular user, and thus would return false here since it can't
398 * arbitrarily assign those OAuth credentials or that certificate to a
399 * different user. A session provider that shoves information into cookies,
400 * on the other hand, could easily do so.
402 * @note For use by \MediaWiki\Session\SessionBackend only
405 abstract public function canChangeUser();
408 * Returns the duration (in seconds) for which users will be remembered when
409 * Session::setRememberUser() is set. Null means setting the remember flag will
410 * have no effect (and endpoints should not offer that option).
411 * @stable to override
414 public function getRememberUserDuration() {
419 * Notification that the session ID was reset
421 * No need to persist here, persistSession() will be called if appropriate.
422 * @stable to override
424 * @note For use by \MediaWiki\Session\SessionBackend only
425 * @param SessionBackend $session Session to persist
426 * @param string $oldId Old session ID
427 * @codeCoverageIgnore
429 public function sessionIdWasReset( SessionBackend
$session, $oldId ) {
433 * Persist a session into a request/response
435 * For example, you might set cookies for the session's ID, user ID, user
436 * name, and user token on the passed request.
438 * To correctly persist a user independently of the session ID, the
439 * provider should persist both the user ID (or name, but preferably the
440 * ID) and the user token. When reading the data from the request, it
441 * should construct a User object from the ID/name and then verify that the
442 * User object's token matches the token included in the request. Should
443 * the tokens not match, an anonymous user *must* be passed to
444 * SessionInfo::__construct().
446 * When persisting a user independently of the session ID,
447 * $session->shouldRememberUser() should be checked first. If this returns
448 * false, the user token *must not* be saved to cookies. The user name
449 * and/or ID may be persisted, and should be used to construct an
450 * unverified UserInfo to pass to SessionInfo::__construct().
452 * A backend that cannot persist session ID or user info should implement
455 * @note For use by \MediaWiki\Session\SessionBackend only
456 * @param SessionBackend $session Session to persist
457 * @param WebRequest $request Request into which to persist the session
459 abstract public function persistSession( SessionBackend
$session, WebRequest
$request );
462 * Remove any persisted session from a request/response
464 * For example, blank and expire any cookies set by self::persistSession().
466 * A backend that cannot persist session ID or user info should implement
469 * @note For use by \MediaWiki\Session\SessionManager only
470 * @param WebRequest $request Request from which to remove any session data
472 abstract public function unpersistSession( WebRequest
$request );
475 * Prevent future sessions for the user
477 * If the provider is capable of returning a SessionInfo with a verified
478 * UserInfo for the named user in some manner other than by validating
479 * against $user->getToken(), steps must be taken to prevent that from
480 * occurring in the future. This might add the username to a list, or
481 * it might just delete whatever authentication credentials would allow
482 * such a session in the first place (e.g. remove all OAuth grants or
483 * delete record of the SSL client certificate).
485 * The intention is that the named account will never again be usable for
486 * normal login (i.e. there is no way to undo the prevention of access).
488 * Note that the passed user name might not exist locally (i.e.
489 * UserIdentity::getId() === 0); the name should still be
490 * prevented, if applicable.
492 * @stable to override
493 * @note For use by \MediaWiki\Session\SessionManager only
494 * @param string $username
496 public function preventSessionsForUser( $username ) {
497 if ( !$this->canChangeUser() ) {
498 throw new \
BadMethodCallException(
499 __METHOD__
. ' must be implemented when canChangeUser() is false'
505 * Invalidate existing sessions for a user
507 * If the provider has its own equivalent of CookieSessionProvider's Token
508 * cookie (and doesn't use User::getToken() to implement it), it should
509 * reset whatever token it does use here.
511 * @stable to override
512 * @note For use by \MediaWiki\Session\SessionManager only
515 public function invalidateSessionsForUser( User
$user ) {
519 * Return the HTTP headers that need varying on.
521 * The return value is such that someone could theoretically do this:
523 * foreach ( $provider->getVaryHeaders() as $header => $_ ) {
524 * $outputPage->addVaryHeader( $header );
528 * @stable to override
529 * @note For use by \MediaWiki\Session\SessionManager only
530 * @return array<string,null>
532 public function getVaryHeaders() {
537 * Return the list of cookies that need varying on.
538 * @stable to override
539 * @note For use by \MediaWiki\Session\SessionManager only
542 public function getVaryCookies() {
547 * Get a suggested username for the login form
548 * @stable to override
549 * @note For use by \MediaWiki\Session\SessionBackend only
550 * @param WebRequest $request
551 * @return string|null
553 public function suggestLoginUsername( WebRequest
$request ) {
558 * Fetch the rights allowed the user when the specified session is active.
560 * This is mainly meant for allowing the user to restrict access to the account
561 * by certain methods; you probably want to use this with GrantsInfo. The returned
562 * rights will be intersected with the user's actual rights.
564 * @stable to override
565 * @param SessionBackend $backend
566 * @return null|string[] Allowed user rights, or null to allow all.
568 public function getAllowedUserRights( SessionBackend
$backend ) {
569 if ( $backend->getProvider() !== $this ) {
570 // Not that this should ever happen...
571 throw new InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
578 * Fetch any restrictions imposed on logins or actions when this
582 * @stable to override
583 * @return MWRestrictions|null
585 public function getRestrictions( ?
array $providerMetadata ): ?MWRestrictions
{
590 * @note Only override this if it makes sense to instantiate multiple
591 * instances of the provider. Value returned must be unique across
592 * configured providers. If you override this, you'll likely need to
593 * override self::describeMessage() as well.
596 public function __toString() {
597 return static::class;
601 * Return a Message identifying this session type
603 * This default implementation takes the class name, lowercases it,
604 * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
605 * determine the message key. For example, MediaWiki\Session\CookieSessionProvider
606 * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
608 * @stable to override
609 * @note If self::__toString() is overridden, this will likely need to be
610 * overridden as well.
611 * @warning This will be called early during MediaWiki startup. Do not
612 * use $wgUser, $wgLang, $wgOut, the global Parser, or their equivalents via
613 * RequestContext from this method!
616 protected function describeMessage() {
618 'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) )
624 * @stable to override
626 public function describe( Language
$lang ) {
627 $msg = $this->describeMessage();
628 $msg->inLanguage( $lang );
629 if ( $msg->isDisabled() ) {
630 $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
632 return $msg->plain();
637 * @stable to override
639 public function whyNoSession() {
644 * Most session providers require protection against CSRF attacks (usually via CSRF tokens)
646 * @stable to override
649 public function safeAgainstCsrf() {
654 * Returns true if this provider is exempt from autocreate user permissions check
656 * By default returns false, meaning this provider respects the normal rights
657 * of anonymous user creation. When true the permission checks will be bypassed
658 * and the user will always be created (subject to other limitations, like read
661 * @stable to override
664 public function canAlwaysAutocreate(): bool {
669 * Hash data as a session ID
671 * Generally this will only be used when self::persistsSessionId() is false and
672 * the provider has to base the session ID on the verified user's identity
673 * or other static data. The SessionInfo should then typically have the
674 * 'forceUse' flag set to avoid persistent session failure if validation of
675 * the stored data fails.
677 * @param string $data
678 * @param string|null $key Defaults to $this->getConfig()->get( MainConfigNames::SecretKey )
681 final protected function hashToSessionId( $data, $key = null ) {
682 if ( !is_string( $data ) ) {
683 throw new InvalidArgumentException(
684 '$data must be a string, ' . get_debug_type( $data ) . ' was passed'
687 if ( $key !== null && !is_string( $key ) ) {
688 throw new InvalidArgumentException(
689 '$key must be a string or null, ' . get_debug_type( $key ) . ' was passed'
693 $hash = \MWCryptHash
::hmac( "$this\n$data",
694 $key ?
: $this->getConfig()->get( MainConfigNames
::SecretKey
), false );
695 if ( strlen( $hash ) < 32 ) {
696 // Should never happen, even md5 is 128 bits
697 // @codeCoverageIgnoreStart
698 throw new \
UnexpectedValueException( 'Hash function returned less than 128 bits' );
699 // @codeCoverageIgnoreEnd
701 if ( strlen( $hash ) >= 40 ) {
702 $hash = \Wikimedia\base_convert
( $hash, 16, 32, 32 );
704 return substr( $hash, -32 );
708 * Throw an exception, later. Needed because during session initialization the framework
709 * isn't quite ready to handle an exception.
711 * This should be called from provideSessionInfo() to fail in
712 * a user-friendly way when a session mechanism is used in a way it's not supposed to be used
713 * (e.g. invalid credentials or a non-API request when the session provider only supports
714 * API requests), and the returned SessionInfo should be returned by provideSessionInfo().
716 * @param string $key Key for the error message
717 * @phpcs:ignore Generic.Files.LineLength
718 * @param MessageParam|MessageSpecifier|string|int|float|list<MessageParam|MessageSpecifier|string|int|float> ...$params
719 * See Message::params()
720 * @return SessionInfo An anonymous session info with maximum priority, to force an
721 * anonymous session in case throwing the exception doesn't happen.
723 protected function makeException( $key, ...$params ): SessionInfo
{
724 $msg = wfMessage( $key, $params );
726 if ( defined( 'MW_API' ) ) {
727 $this->hookContainer
->register(
729 // @phan-suppress-next-line PhanPluginNeverReturnFunction Closures should not get doc
730 static function () use ( $msg ) {
731 throw ApiUsageException
::newWithMessage( null, $msg );
734 } elseif ( defined( 'MW_REST_API' ) ) {
735 // There are no suitable hooks in the REST API (T252591)
737 $this->hookContainer
->register(
739 // @phan-suppress-next-line PhanPluginNeverReturnFunction Closures should not get doc
740 static function () use ( $msg ) {
741 RequestContext
::getMain()->getOutput()->setStatusCode( 400 );
742 throw new ErrorPageError( 'errorpagetitle', $msg );
745 // Disable file cache, which would be looked up before the BeforeInitialize hook call.
746 $this->hookContainer
->register(
747 'HTMLFileCache__useFileCache',
754 $id = $this->hashToSessionId( 'bogus' );
755 return new SessionInfo( SessionInfo
::MAX_PRIORITY
, [
758 'userInfo' => UserInfo
::newAnonymous(),
759 'persisted' => false,