123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- <?php
- /**
- * MediaWiki session provider base class
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Session
- */
- namespace MediaWiki\Session;
- use Psr\Log\LoggerAwareInterface;
- use Psr\Log\LoggerInterface;
- use Config;
- use Language;
- use User;
- use WebRequest;
- /**
- * A SessionProvider provides SessionInfo and support for Session
- *
- * A SessionProvider is responsible for taking a WebRequest and determining
- * the authenticated session that it's a part of. It does this by returning an
- * SessionInfo object with basic information about the session it thinks is
- * associated with the request, namely the session ID and possibly the
- * authenticated user the session belongs to.
- *
- * The SessionProvider also provides for updating the WebResponse with
- * information necessary to provide the client with data that the client will
- * send with later requests, and for populating the Vary and Key headers with
- * the data necessary to correctly vary the cache on these client requests.
- *
- * An important part of the latter is indicating whether it even *can* tell the
- * client to include such data in future requests, via the persistsSessionId()
- * and canChangeUser() methods. The cases are (in order of decreasing
- * commonness):
- * - Cannot persist ID, no changing User: The request identifies and
- * authenticates a particular local user, and the client cannot be
- * instructed to include an arbitrary session ID with future requests. For
- * example, OAuth or SSL certificate auth.
- * - Can persist ID and can change User: The client can be instructed to
- * return at least one piece of arbitrary data, that being the session ID.
- * The user identity might also be given to the client, otherwise it's saved
- * in the session data. For example, cookie-based sessions.
- * - Can persist ID but no changing User: The request uniquely identifies and
- * authenticates a local user, and the client can be instructed to return an
- * arbitrary session ID with future requests. For example, HTTP Digest
- * authentication might somehow use the 'opaque' field as a session ID
- * (although getting MediaWiki to return 401 responses without breaking
- * other stuff might be a challenge).
- * - Cannot persist ID but can change User: I can't think of a way this
- * would make sense.
- *
- * Note that many methods that are technically "cannot persist ID" could be
- * turned into "can persist ID but not change User" using a session cookie,
- * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
- * session cookie names should be used for different providers to avoid
- * collisions.
- *
- * @ingroup Session
- * @since 1.27
- * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
- */
- abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
- /** @var LoggerInterface */
- protected $logger;
- /** @var Config */
- protected $config;
- /** @var SessionManager */
- protected $manager;
- /** @var int Session priority. Used for the default newSessionInfo(), but
- * could be used by subclasses too.
- */
- protected $priority;
- /**
- * @note To fully initialize a SessionProvider, the setLogger(),
- * setConfig(), and setManager() methods must be called (and should be
- * called in that order). Failure to do so is liable to cause things to
- * fail unexpectedly.
- */
- public function __construct() {
- $this->priority = SessionInfo::MIN_PRIORITY + 10;
- }
- public function setLogger( LoggerInterface $logger ) {
- $this->logger = $logger;
- }
- /**
- * Set configuration
- * @param Config $config
- */
- public function setConfig( Config $config ) {
- $this->config = $config;
- }
- /**
- * Set the session manager
- * @param SessionManager $manager
- */
- public function setManager( SessionManager $manager ) {
- $this->manager = $manager;
- }
- /**
- * Get the session manager
- * @return SessionManager
- */
- public function getManager() {
- return $this->manager;
- }
- /**
- * Provide session info for a request
- *
- * If no session exists for the request, return null. Otherwise return an
- * SessionInfo object identifying the session.
- *
- * If multiple SessionProviders provide sessions, the one with highest
- * priority wins. In case of a tie, an exception is thrown.
- * SessionProviders are encouraged to make priorities user-configurable
- * unless only max-priority makes sense.
- *
- * @warning This will be called early in the MediaWiki setup process,
- * before $wgUser, $wgLang, $wgOut, $wgTitle, the global parser, and
- * corresponding pieces of the main RequestContext are set up! If you try
- * to use these, things *will* break.
- * @note The SessionProvider must not attempt to auto-create users.
- * MediaWiki will do this later (when it's safe) if the chosen session has
- * a user with a valid name but no ID.
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param WebRequest $request
- * @return SessionInfo|null
- */
- abstract public function provideSessionInfo( WebRequest $request );
- /**
- * Provide session info for a new, empty session
- *
- * Return null if such a session cannot be created. This base
- * implementation assumes that it only makes sense if a session ID can be
- * persisted and changing users is allowed.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param string|null $id ID to force for the new session
- * @return SessionInfo|null
- * If non-null, must return true for $info->isIdSafe(); pass true for
- * $data['idIsSafe'] to ensure this.
- */
- public function newSessionInfo( $id = null ) {
- if ( $this->canChangeUser() && $this->persistsSessionId() ) {
- return new SessionInfo( $this->priority, [
- 'id' => $id,
- 'provider' => $this,
- 'persisted' => false,
- 'idIsSafe' => true,
- ] );
- }
- return null;
- }
- /**
- * Merge saved session provider metadata
- *
- * This method will be used to compare the metadata returned by
- * provideSessionInfo() with the saved metadata (which has been returned by
- * provideSessionInfo() the last time the session was saved), and merge the two
- * into the new saved metadata, or abort if the current request is not a valid
- * continuation of the session.
- *
- * The default implementation checks that anything in both arrays is
- * identical, then returns $providedMetadata.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param array $savedMetadata Saved provider metadata
- * @param array $providedMetadata Provided provider metadata (from the SessionInfo)
- * @return array Resulting metadata
- * @throws MetadataMergeException If the metadata cannot be merged.
- * Such exceptions will be handled by SessionManager and are a safe way of rejecting
- * a suspicious or incompatible session. The provider is expected to write an
- * appropriate message to its logger.
- */
- public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
- foreach ( $providedMetadata as $k => $v ) {
- if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
- $e = new MetadataMergeException( "Key \"$k\" changed" );
- $e->setContext( [
- 'old_value' => $savedMetadata[$k],
- 'new_value' => $v,
- ] );
- throw $e;
- }
- }
- return $providedMetadata;
- }
- /**
- * Validate a loaded SessionInfo and refresh provider metadata
- *
- * This is similar in purpose to the 'SessionCheckInfo' hook, and also
- * allows for updating the provider metadata. On failure, the provider is
- * expected to write an appropriate message to its logger.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here.
- * @param WebRequest $request
- * @param array|null &$metadata Provider metadata, may be altered.
- * @return bool Return false to reject the SessionInfo after all.
- */
- public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
- return true;
- }
- /**
- * Indicate whether self::persistSession() can save arbitrary session IDs
- *
- * If false, any session passed to self::persistSession() will have an ID
- * that was originally provided by self::provideSessionInfo().
- *
- * If true, the provider may be passed sessions with arbitrary session IDs,
- * and will be expected to manipulate the request in such a way that future
- * requests will cause self::provideSessionInfo() to provide a SessionInfo
- * with that ID.
- *
- * For example, a session provider for OAuth would function by matching the
- * OAuth headers to a particular user, and then would use self::hashToSessionId()
- * to turn the user and OAuth client ID (and maybe also the user token and
- * client secret) into a session ID, and therefore can't easily assign that
- * user+client a different ID. Similarly, a session provider for SSL client
- * certificates would function by matching the certificate to a particular
- * user, and then would use self::hashToSessionId() to turn the user and
- * certificate fingerprint into a session ID, and therefore can't easily
- * assign a different ID either. On the other hand, a provider that saves
- * the session ID into a cookie can easily just set the cookie to a
- * different value.
- *
- * @protected For use by \MediaWiki\Session\SessionBackend only
- * @return bool
- */
- abstract public function persistsSessionId();
- /**
- * Indicate whether the user associated with the request can be changed
- *
- * If false, any session passed to self::persistSession() will have a user
- * that was originally provided by self::provideSessionInfo(). Further,
- * self::provideSessionInfo() may only provide sessions that have a user
- * already set.
- *
- * If true, the provider may be passed sessions with arbitrary users, and
- * will be expected to manipulate the request in such a way that future
- * requests will cause self::provideSessionInfo() to provide a SessionInfo
- * with that ID. This can be as simple as not passing any 'userInfo' into
- * SessionInfo's constructor, in which case SessionInfo will load the user
- * from the saved session's metadata.
- *
- * For example, a session provider for OAuth or SSL client certificates
- * would function by matching the OAuth headers or certificate to a
- * particular user, and thus would return false here since it can't
- * arbitrarily assign those OAuth credentials or that certificate to a
- * different user. A session provider that shoves information into cookies,
- * on the other hand, could easily do so.
- *
- * @protected For use by \MediaWiki\Session\SessionBackend only
- * @return bool
- */
- abstract public function canChangeUser();
- /**
- * Returns the duration (in seconds) for which users will be remembered when
- * Session::setRememberUser() is set. Null means setting the remember flag will
- * have no effect (and endpoints should not offer that option).
- * @return int|null
- */
- public function getRememberUserDuration() {
- return null;
- }
- /**
- * Notification that the session ID was reset
- *
- * No need to persist here, persistSession() will be called if appropriate.
- *
- * @protected For use by \MediaWiki\Session\SessionBackend only
- * @param SessionBackend $session Session to persist
- * @param string $oldId Old session ID
- * @codeCoverageIgnore
- */
- public function sessionIdWasReset( SessionBackend $session, $oldId ) {
- }
- /**
- * Persist a session into a request/response
- *
- * For example, you might set cookies for the session's ID, user ID, user
- * name, and user token on the passed request.
- *
- * To correctly persist a user independently of the session ID, the
- * provider should persist both the user ID (or name, but preferably the
- * ID) and the user token. When reading the data from the request, it
- * should construct a User object from the ID/name and then verify that the
- * User object's token matches the token included in the request. Should
- * the tokens not match, an anonymous user *must* be passed to
- * SessionInfo::__construct().
- *
- * When persisting a user independently of the session ID,
- * $session->shouldRememberUser() should be checked first. If this returns
- * false, the user token *must not* be saved to cookies. The user name
- * and/or ID may be persisted, and should be used to construct an
- * unverified UserInfo to pass to SessionInfo::__construct().
- *
- * A backend that cannot persist sesison ID or user info should implement
- * this as a no-op.
- *
- * @protected For use by \MediaWiki\Session\SessionBackend only
- * @param SessionBackend $session Session to persist
- * @param WebRequest $request Request into which to persist the session
- */
- abstract public function persistSession( SessionBackend $session, WebRequest $request );
- /**
- * Remove any persisted session from a request/response
- *
- * For example, blank and expire any cookies set by self::persistSession().
- *
- * A backend that cannot persist sesison ID or user info should implement
- * this as a no-op.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param WebRequest $request Request from which to remove any session data
- */
- abstract public function unpersistSession( WebRequest $request );
- /**
- * Prevent future sessions for the user
- *
- * If the provider is capable of returning a SessionInfo with a verified
- * UserInfo for the named user in some manner other than by validating
- * against $user->getToken(), steps must be taken to prevent that from
- * occurring in the future. This might add the username to a blacklist, or
- * it might just delete whatever authentication credentials would allow
- * such a session in the first place (e.g. remove all OAuth grants or
- * delete record of the SSL client certificate).
- *
- * The intention is that the named account will never again be usable for
- * normal login (i.e. there is no way to undo the prevention of access).
- *
- * Note that the passed user name might not exist locally (i.e.
- * User::idFromName( $username ) === 0); the name should still be
- * prevented, if applicable.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param string $username
- */
- public function preventSessionsForUser( $username ) {
- if ( !$this->canChangeUser() ) {
- throw new \BadMethodCallException(
- __METHOD__ . ' must be implemented when canChangeUser() is false'
- );
- }
- }
- /**
- * Invalidate existing sessions for a user
- *
- * If the provider has its own equivalent of CookieSessionProvider's Token
- * cookie (and doesn't use User::getToken() to implement it), it should
- * reset whatever token it does use here.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @param User $user
- */
- public function invalidateSessionsForUser( User $user ) {
- }
- /**
- * Return the HTTP headers that need varying on.
- *
- * The return value is such that someone could theoretically do this:
- * @code
- * foreach ( $provider->getVaryHeaders() as $header => $options ) {
- * $outputPage->addVaryHeader( $header, $options );
- * }
- * @endcode
- *
- * Note that the $options parameter to addVaryHeader has been deprecated
- * since 1.34, and should be `null` or an empty array.
- *
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @return array
- */
- public function getVaryHeaders() {
- return [];
- }
- /**
- * Return the list of cookies that need varying on.
- * @protected For use by \MediaWiki\Session\SessionManager only
- * @return string[]
- */
- public function getVaryCookies() {
- return [];
- }
- /**
- * Get a suggested username for the login form
- * @protected For use by \MediaWiki\Session\SessionBackend only
- * @param WebRequest $request
- * @return string|null
- */
- public function suggestLoginUsername( WebRequest $request ) {
- return null;
- }
- /**
- * Fetch the rights allowed the user when the specified session is active.
- *
- * This is mainly meant for allowing the user to restrict access to the account
- * by certain methods; you probably want to use this with MWGrants. The returned
- * rights will be intersected with the user's actual rights.
- *
- * @param SessionBackend $backend
- * @return null|string[] Allowed user rights, or null to allow all.
- */
- public function getAllowedUserRights( SessionBackend $backend ) {
- if ( $backend->getProvider() !== $this ) {
- // Not that this should ever happen...
- throw new \InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
- }
- return null;
- }
- /**
- * @note Only override this if it makes sense to instantiate multiple
- * instances of the provider. Value returned must be unique across
- * configured providers. If you override this, you'll likely need to
- * override self::describeMessage() as well.
- * @return string
- */
- public function __toString() {
- return static::class;
- }
- /**
- * Return a Message identifying this session type
- *
- * This default implementation takes the class name, lowercases it,
- * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
- * determine the message key. For example, MediaWiki\Session\CookieSessionProvider
- * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
- *
- * @note If self::__toString() is overridden, this will likely need to be
- * overridden as well.
- * @warning This will be called early during MediaWiki startup. Do not
- * use $wgUser, $wgLang, $wgOut, the global Parser, or their equivalents via
- * RequestContext from this method!
- * @return \Message
- */
- protected function describeMessage() {
- return wfMessage(
- 'sessionprovider-' . str_replace( '\\', '-', strtolower( static::class ) )
- );
- }
- public function describe( Language $lang ) {
- $msg = $this->describeMessage();
- $msg->inLanguage( $lang );
- if ( $msg->isDisabled() ) {
- $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
- }
- return $msg->plain();
- }
- public function whyNoSession() {
- return null;
- }
- /**
- * Hash data as a session ID
- *
- * Generally this will only be used when self::persistsSessionId() is false and
- * the provider has to base the session ID on the verified user's identity
- * or other static data. The SessionInfo should then typically have the
- * 'forceUse' flag set to avoid persistent session failure if validation of
- * the stored data fails.
- *
- * @param string $data
- * @param string|null $key Defaults to $this->config->get( 'SecretKey' )
- * @return string
- */
- final protected function hashToSessionId( $data, $key = null ) {
- if ( !is_string( $data ) ) {
- throw new \InvalidArgumentException(
- '$data must be a string, ' . gettype( $data ) . ' was passed'
- );
- }
- if ( $key !== null && !is_string( $key ) ) {
- throw new \InvalidArgumentException(
- '$key must be a string or null, ' . gettype( $key ) . ' was passed'
- );
- }
- $hash = \MWCryptHash::hmac( "$this\n$data", $key ?: $this->config->get( 'SecretKey' ), false );
- if ( strlen( $hash ) < 32 ) {
- // Should never happen, even md5 is 128 bits
- // @codeCoverageIgnoreStart
- throw new \UnexpectedValueException( 'Hash function returned less than 128 bits' );
- // @codeCoverageIgnoreEnd
- }
- if ( strlen( $hash ) >= 40 ) {
- $hash = \Wikimedia\base_convert( $hash, 16, 32, 32 );
- }
- return substr( $hash, -32 );
- }
- }
|