SessionProvider.php 20 KB

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