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
;
26 use Psr\Log\LoggerAwareInterface
;
27 use Psr\Log\LoggerInterface
;
34 * A SessionProvider provides SessionInfo and support for Session
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.
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.
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
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
68 * Note that many methods that are technically "cannot persist ID" could be
69 * turned into "can persist ID but not changing 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
77 abstract class SessionProvider
implements SessionProviderInterface
, LoggerAwareInterface
{
79 /** @var LoggerInterface */
85 /** @var SessionManager */
88 /** @var int Session priority. Used for the default newSessionInfo(), but
89 * could be used by subclasses too.
94 * @note To fully initialize a SessionProvider, the setLogger(),
95 * setConfig(), and setManager() methods must be called (and should be
96 * called in that order). Failure to do so is liable to cause things to
99 public function __construct() {
100 $this->priority
= SessionInfo
::MIN_PRIORITY +
10;
103 public function setLogger( LoggerInterface
$logger ) {
104 $this->logger
= $logger;
109 * @param Config $config
111 public function setConfig( Config
$config ) {
112 $this->config
= $config;
116 * Set the session manager
117 * @param SessionManager $manager
119 public function setManager( SessionManager
$manager ) {
120 $this->manager
= $manager;
124 * Get the session manager
125 * @return SessionManager
127 public function getManager() {
128 return $this->manager
;
132 * Provide session info for a request
134 * If no session exists for the request, return null. Otherwise return an
135 * SessionInfo object identifying the session.
137 * If multiple SessionProviders provide sessions, the one with highest
138 * priority wins. In case of a tie, an exception is thrown.
139 * SessionProviders are encouraged to make priorities user-configurable
140 * unless only max-priority makes sense.
142 * @warning This will be called early in the MediaWiki setup process,
143 * before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
144 * pieces of the main RequestContext are set up! If you try to use these,
145 * things *will* break.
146 * @note The SessionProvider must not attempt to auto-create users.
147 * MediaWiki will do this later (when it's safe) if the chosen session has
148 * a user with a valid name but no ID.
149 * @protected For use by \MediaWiki\Session\SessionManager only
150 * @param WebRequest $request
151 * @return SessionInfo|null
153 abstract public function provideSessionInfo( WebRequest
$request );
156 * Provide session info for a new, empty session
158 * Return null if such a session cannot be created. This base
159 * implementation assumes that it only makes sense if a session ID can be
160 * persisted and changing users is allowed.
162 * @protected For use by \MediaWiki\Session\SessionManager only
163 * @param string|null $id ID to force for the new session
164 * @return SessionInfo|null
165 * If non-null, must return true for $info->isIdSafe(); pass true for
166 * $data['idIsSafe'] to ensure this.
168 public function newSessionInfo( $id = null ) {
169 if ( $this->canChangeUser() && $this->persistsSessionId() ) {
170 return new SessionInfo( $this->priority
, [
173 'persisted' => false,
181 * Merge saved session provider metadata
183 * The default implementation checks that anything in both arrays is
184 * identical, then returns $providedMetadata.
186 * @protected For use by \MediaWiki\Session\SessionManager only
187 * @param array $savedMetadata Saved provider metadata
188 * @param array $providedMetadata Provided provider metadata
189 * @return array Resulting metadata
190 * @throws MetadataMergeException If the metadata cannot be merged
192 public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
193 foreach ( $providedMetadata as $k => $v ) {
194 if ( array_key_exists( $k, $savedMetadata ) && $savedMetadata[$k] !== $v ) {
195 $e = new MetadataMergeException( "Key \"$k\" changed" );
197 'old_value' => $savedMetadata[$k],
203 return $providedMetadata;
207 * Validate a loaded SessionInfo and refresh provider metadata
209 * This is similar in purpose to the 'SessionCheckInfo' hook, and also
210 * allows for updating the provider metadata. On failure, the provider is
211 * expected to write an appropriate message to its logger.
213 * @protected For use by \MediaWiki\Session\SessionManager only
214 * @param SessionInfo $info
215 * @param WebRequest $request
216 * @param array|null &$metadata Provider metadata, may be altered.
217 * @return bool Return false to reject the SessionInfo after all.
219 public function refreshSessionInfo( SessionInfo
$info, WebRequest
$request, &$metadata ) {
224 * Indicate whether self::persistSession() can save arbitrary session IDs
226 * If false, any session passed to self::persistSession() will have an ID
227 * that was originally provided by self::provideSessionInfo().
229 * If true, the provider may be passed sessions with arbitrary session IDs,
230 * and will be expected to manipulate the request in such a way that future
231 * requests will cause self::provideSessionInfo() to provide a SessionInfo
234 * For example, a session provider for OAuth would function by matching the
235 * OAuth headers to a particular user, and then would use self::hashToSessionId()
236 * to turn the user and OAuth client ID (and maybe also the user token and
237 * client secret) into a session ID, and therefore can't easily assign that
238 * user+client a different ID. Similarly, a session provider for SSL client
239 * certificates would function by matching the certificate to a particular
240 * user, and then would use self::hashToSessionId() to turn the user and
241 * certificate fingerprint into a session ID, and therefore can't easily
242 * assign a different ID either. On the other hand, a provider that saves
243 * the session ID into a cookie can easily just set the cookie to a
246 * @protected For use by \MediaWiki\Session\SessionBackend only
249 abstract public function persistsSessionId();
252 * Indicate whether the user associated with the request can be changed
254 * If false, any session passed to self::persistSession() will have a user
255 * that was originally provided by self::provideSessionInfo(). Further,
256 * self::provideSessionInfo() may only provide sessions that have a user
259 * If true, the provider may be passed sessions with arbitrary users, and
260 * will be expected to manipulate the request in such a way that future
261 * requests will cause self::provideSessionInfo() to provide a SessionInfo
262 * with that ID. This can be as simple as not passing any 'userInfo' into
263 * SessionInfo's constructor, in which case SessionInfo will load the user
264 * from the saved session's metadata.
266 * For example, a session provider for OAuth or SSL client certificates
267 * would function by matching the OAuth headers or certificate to a
268 * particular user, and thus would return false here since it can't
269 * arbitrarily assign those OAuth credentials or that certificate to a
270 * different user. A session provider that shoves information into cookies,
271 * on the other hand, could easily do so.
273 * @protected For use by \MediaWiki\Session\SessionBackend only
276 abstract public function canChangeUser();
279 * Returns the duration (in seconds) for which users will be remembered when
280 * Session::setRememberUser() is set. Null means setting the remember flag will
281 * have no effect (and endpoints should not offer that option).
284 public function getRememberUserDuration() {
289 * Notification that the session ID was reset
291 * No need to persist here, persistSession() will be called if appropriate.
293 * @protected For use by \MediaWiki\Session\SessionBackend only
294 * @param SessionBackend $session Session to persist
295 * @param string $oldId Old session ID
296 * @codeCoverageIgnore
298 public function sessionIdWasReset( SessionBackend
$session, $oldId ) {
302 * Persist a session into a request/response
304 * For example, you might set cookies for the session's ID, user ID, user
305 * name, and user token on the passed request.
307 * To correctly persist a user independently of the session ID, the
308 * provider should persist both the user ID (or name, but preferably the
309 * ID) and the user token. When reading the data from the request, it
310 * should construct a User object from the ID/name and then verify that the
311 * User object's token matches the token included in the request. Should
312 * the tokens not match, an anonymous user *must* be passed to
313 * SessionInfo::__construct().
315 * When persisting a user independently of the session ID,
316 * $session->shouldRememberUser() should be checked first. If this returns
317 * false, the user token *must not* be saved to cookies. The user name
318 * and/or ID may be persisted, and should be used to construct an
319 * unverified UserInfo to pass to SessionInfo::__construct().
321 * A backend that cannot persist sesison ID or user info should implement
324 * @protected For use by \MediaWiki\Session\SessionBackend only
325 * @param SessionBackend $session Session to persist
326 * @param WebRequest $request Request into which to persist the session
328 abstract public function persistSession( SessionBackend
$session, WebRequest
$request );
331 * Remove any persisted session from a request/response
333 * For example, blank and expire any cookies set by self::persistSession().
335 * A backend that cannot persist sesison ID or user info should implement
338 * @protected For use by \MediaWiki\Session\SessionManager only
339 * @param WebRequest $request Request from which to remove any session data
341 abstract public function unpersistSession( WebRequest
$request );
344 * Prevent future sessions for the user
346 * If the provider is capable of returning a SessionInfo with a verified
347 * UserInfo for the named user in some manner other than by validating
348 * against $user->getToken(), steps must be taken to prevent that from
349 * occurring in the future. This might add the username to a blacklist, or
350 * it might just delete whatever authentication credentials would allow
351 * such a session in the first place (e.g. remove all OAuth grants or
352 * delete record of the SSL client certificate).
354 * The intention is that the named account will never again be usable for
355 * normal login (i.e. there is no way to undo the prevention of access).
357 * Note that the passed user name might not exist locally (i.e.
358 * User::idFromName( $username ) === 0); the name should still be
359 * prevented, if applicable.
361 * @protected For use by \MediaWiki\Session\SessionManager only
362 * @param string $username
364 public function preventSessionsForUser( $username ) {
365 if ( !$this->canChangeUser() ) {
366 throw new \
BadMethodCallException(
367 __METHOD__
. ' must be implmented when canChangeUser() is false'
373 * Invalidate existing sessions for a user
375 * If the provider has its own equivalent of CookieSessionProvider's Token
376 * cookie (and doesn't use User::getToken() to implement it), it should
377 * reset whatever token it does use here.
379 * @protected For use by \MediaWiki\Session\SessionManager only
382 public function invalidateSessionsForUser( User
$user ) {
386 * Return the HTTP headers that need varying on.
388 * The return value is such that someone could theoretically do this:
390 * foreach ( $provider->getVaryHeaders() as $header => $options ) {
391 * $outputPage->addVaryHeader( $header, $options );
395 * @protected For use by \MediaWiki\Session\SessionManager only
398 public function getVaryHeaders() {
403 * Return the list of cookies that need varying on.
404 * @protected For use by \MediaWiki\Session\SessionManager only
407 public function getVaryCookies() {
412 * Get a suggested username for the login form
413 * @protected For use by \MediaWiki\Session\SessionBackend only
414 * @param WebRequest $request
415 * @return string|null
417 public function suggestLoginUsername( WebRequest
$request ) {
422 * Fetch the rights allowed the user when the specified session is active.
423 * @param SessionBackend $backend
424 * @return null|string[] Allowed user rights, or null to allow all.
426 public function getAllowedUserRights( SessionBackend
$backend ) {
427 if ( $backend->getProvider() !== $this ) {
428 // Not that this should ever happen...
429 throw new \
InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
436 * @note Only override this if it makes sense to instantiate multiple
437 * instances of the provider. Value returned must be unique across
438 * configured providers. If you override this, you'll likely need to
439 * override self::describeMessage() as well.
442 public function __toString() {
443 return get_class( $this );
447 * Return a Message identifying this session type
449 * This default implementation takes the class name, lowercases it,
450 * replaces backslashes with dashes, and prefixes 'sessionprovider-' to
451 * determine the message key. For example, MediaWiki\Session\CookieSessionProvider
452 * produces 'sessionprovider-mediawiki-session-cookiesessionprovider'.
454 * @note If self::__toString() is overridden, this will likely need to be
455 * overridden as well.
456 * @warning This will be called early during MediaWiki startup. Do not
457 * use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
458 * RequestContext from this method!
461 protected function describeMessage() {
463 'sessionprovider-' . str_replace( '\\', '-', strtolower( get_class( $this ) ) )
467 public function describe( Language
$lang ) {
468 $msg = $this->describeMessage();
469 $msg->inLanguage( $lang );
470 if ( $msg->isDisabled() ) {
471 $msg = wfMessage( 'sessionprovider-generic', (string)$this )->inLanguage( $lang );
473 return $msg->plain();
476 public function whyNoSession() {
481 * Hash data as a session ID
483 * Generally this will only be used when self::persistsSessionId() is false and
484 * the provider has to base the session ID on the verified user's identity
485 * or other static data. The SessionInfo should then typically have the
486 * 'forceUse' flag set to avoid persistent session failure if validation of
487 * the stored data fails.
489 * @param string $data
490 * @param string|null $key Defaults to $this->config->get( 'SecretKey' )
493 final protected function hashToSessionId( $data, $key = null ) {
494 if ( !is_string( $data ) ) {
495 throw new \
InvalidArgumentException(
496 '$data must be a string, ' . gettype( $data ) . ' was passed'
499 if ( $key !== null && !is_string( $key ) ) {
500 throw new \
InvalidArgumentException(
501 '$key must be a string or null, ' . gettype( $key ) . ' was passed'
505 $hash = \MWCryptHash
::hmac( "$this\n$data", $key ?
: $this->config
->get( 'SecretKey' ), false );
506 if ( strlen( $hash ) < 32 ) {
507 // Should never happen, even md5 is 128 bits
508 // @codeCoverageIgnoreStart
509 throw new \
UnexpectedValueException( 'Hash fuction returned less than 128 bits' );
510 // @codeCoverageIgnoreEnd
512 if ( strlen( $hash ) >= 40 ) {
513 $hash = \Wikimedia\base_convert
( $hash, 16, 32, 32 );
515 return substr( $hash, -32 );