Merge "Add CollationFa"
[lhc/web/wiklou.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 get_class( $this );
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( get_class( $this ) ) )
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 }