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