Page MenuHomeWickedGov Phorge

SessionProvider.php
No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None

SessionProvider.php

<?php
namespace MediaWiki\Extension\OAuth;
use Exception;
use GuzzleHttp\Psr7\ServerRequest;
use InvalidArgumentException;
use MediaWiki\Api\ApiBase;
use MediaWiki\Api\ApiMessage;
use MediaWiki\Api\Hook\ApiCheckCanExecuteHook;
use MediaWiki\Context\RequestContext;
use MediaWiki\Extension\OAuth\Backend\Consumer;
use MediaWiki\Extension\OAuth\Backend\ConsumerAcceptance;
use MediaWiki\Extension\OAuth\Backend\MWOAuthException;
use MediaWiki\Extension\OAuth\Backend\MWOAuthRequest;
use MediaWiki\Extension\OAuth\Backend\Utils;
use MediaWiki\Extension\OAuth\Repository\AccessTokenRepository;
use MediaWiki\Hook\MarkPatrolledHook;
use MediaWiki\Hook\RecentChange_saveHook;
use MediaWiki\Linker\Linker;
use MediaWiki\MediaWikiServices;
use MediaWiki\Message\Message;
use MediaWiki\RecentChanges\RecentChange;
use MediaWiki\Request\WebRequest;
use MediaWiki\Session\ImmutableSessionProviderWithCookie;
use MediaWiki\Session\SessionBackend;
use MediaWiki\Session\SessionInfo;
use MediaWiki\Session\SessionManager;
use MediaWiki\Session\UserInfo;
use MediaWiki\Title\Title;
use MediaWiki\User\User;
use MediaWiki\User\UserIdentity;
use MediaWiki\WikiMap\WikiMap;
use MWRestrictions;
use Wikimedia\Rdbms\DBError;
/**
* Session provider for OAuth
*
* This is a fairly standard ImmutableSessionProviderWithCookie implementation:
* the user identity is determined by the OAuth headers included in the
* request. But since we want to make sure to fail the request when OAuth
* headers are present but invalid, this takes the somewhat unusual step of
* returning a bogus SessionInfo and then hooking ApiBeforeMain to throw a
* fatal exception after MediaWiki is ready to handle it.
*
* It also takes advantage of the getAllowedUserRights() method for authz
* purposes (limiting the rights to those included in the grant), and
* registers some hooks to tag actions made via the provider.
*/
class SessionProvider
extends ImmutableSessionProviderWithCookie
implements ApiCheckCanExecuteHook, RecentChange_saveHook, MarkPatrolledHook
{
public function __construct( array $params = [] ) {
parent::__construct( $params );
}
protected function postInitSetup() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'ApiCheckCanExecute', $this );
$hookContainer->register( 'RecentChange_save', $this );
$hookContainer->register( 'MarkPatrolled', $this );
}
/** @inheritDoc */
public function provideSessionInfo( WebRequest $request ) {
$oauthVersion = $this->getOAuthVersionFromRequest( $request );
if ( $oauthVersion === null ) {
// Not an OAuth request
return null;
}
// OAuth is restricted to be API-only.
if ( !defined( 'MW_API' ) && !defined( 'MW_REST_API' ) ) {
$globalRequest = RequestContext::getMain()->getRequest();
if ( $request !== $globalRequest ) {
// We are looking at something other than the global request. No easy way to
// find out the title, and showing an error should be handled in the global
// request anyway. Bail out.
return null;
}
// The global Title object is not set up yet.
$title = Title::newFromText( $request->getText( 'title' ) );
if ( $title && $title->isSpecial( 'OAuth' ) ) {
// Some Special:OAuth subpages expect an OAuth request header, but process it
// manually, not via SessionManager. We mustn't break those.
// TODO: this can probably be limited to /token and /identify
return null;
}
return $this->makeException( 'mwoauth-not-api' );
}
$logData = [
'clientip' => $request->getIP(),
'user' => false,
'consumer' => '',
'result' => 'fail',
];
$dbr = Utils::getCentralDB( DB_REPLICA );
$access = null;
try {
if ( $oauthVersion === Consumer::OAUTH_VERSION_2 ) {
$resourceServer = ResourceServer::factory();
$accessTokenKey = $this->verifyOAuth2Request( $resourceServer, $request );
$accessTokenRepo = new AccessTokenRepository( $this->config->get( 'CanonicalServer' ) );
$accessId = $accessTokenRepo->getApprovalId( $accessTokenKey );
if ( $accessId === 0 ) {
if (
$resourceServer->getUser()->getId() === 0 &&
$resourceServer->getClient()->getOwnerOnly() === false
) {
// This tell us, with good degree of certainty, that the AT
// was issued to a machine and represents no particular user
$access = ConsumerAcceptance::newFromArray( [
'id' => null,
'wiki' => $resourceServer->getClient()->getWiki(),
'userId' => 0,
'consumerId' => $resourceServer->getClient()->getId(),
'accessToken' => '',
'accessSecret' => '',
'grants' => $resourceServer->getClient()->getGrants(),
'accepted' => wfTimestampNow(),
'oauth_version' => Consumer::OAUTH_VERSION_2
] );
}
} else {
$access = ConsumerAcceptance::newFromId(
Utils::getCentralDB( DB_REPLICA ), $accessId
);
}
if ( !$access ) {
$logData['consumer'] = $resourceServer->getClient()->getConsumerKey();
throw new MWOAuthException( 'mwoauth-oauth2-error-create-at-no-user-approval' );
}
// Set the scopes that are verified for this request
$access->setField( 'grants', array_keys( $resourceServer->getScopes() ) );
} else {
$server = Utils::newMWOAuthServer();
$oauthRequest = MWOAuthRequest::fromRequest( $request );
$logData['consumer'] = $oauthRequest->getConsumerKey();
[ , $accessToken ] = $server->verify_request( $oauthRequest );
$accessTokenKey = $accessToken->key;
$access = ConsumerAcceptance::newFromToken( $dbr, $accessTokenKey );
}
} catch ( Exception $ex ) {
$this->logger->info( 'Bad OAuth request from {ip}', $logData + [ 'exception' => $ex ] );
return $this->makeException( 'mwoauth-invalid-authorization', $ex->getMessage() );
}
$logData['user'] = Utils::getCentralUserNameFromId( $access->getUserId(), 'raw' );
$wiki = WikiMap::getCurrentWikiId();
// Access token is for this wiki
if ( $access->getWiki() !== '*' && $access->getWiki() !== $wiki ) {
$this->logger->debug( 'OAuth request for wrong wiki from user {user}', $logData );
return $this->makeException( 'mwoauth-invalid-authorization-wrong-wiki', $wiki );
}
// There exists a local user
$localUser = Utils::getLocalUserFromCentralId( $access->getUserId() );
if ( !$localUser ) {
$localUser = User::newFromId( 0 );
}
// If there is an actual approval, but user bound to it does not exist
if ( $access->getId() > 0 && $localUser->getId() === 0 ) {
$this->logger->debug( 'OAuth request for invalid or non-local user {user}', $logData );
return $this->makeException( 'mwoauth-invalid-authorization-invalid-user',
Message::rawParam( Linker::makeExternalLink(
'https://www.mediawiki.org/wiki/Help:OAuth/Errors#E008',
'E008',
true
) )
);
}
if ( $localUser->isLocked() ) {
$this->logger->debug( 'OAuth request for locked user {user}', $logData );
return $this->makeException( 'mwoauth-invalid-authorization-blocked-user' );
}
if ( $this->config->get( 'BlockDisablesLogin' ) ) {
$block = MediaWikiServices::getInstance()->getBlockManager()
->getBlock( $localUser, null );
if ( $block && $block->isSitewide() ) {
$this->logger->debug( 'OAuth request for blocked user {user}', $logData );
return $this->makeException( 'mwoauth-invalid-authorization-blocked-user' );
}
}
// The consumer is approved or owned by $localUser, and is for this wiki.
$consumer = Consumer::newFromId( $dbr, $access->getConsumerId() );
if ( !$consumer->isUsableBy( $localUser ) ) {
$this->logger->debug(
'OAuth request for consumer {consumer} not approved by user {user}', $logData
);
return $this->makeException( 'mwoauth-invalid-authorization-not-approved',
$consumer->getName() );
} elseif ( $consumer->getWiki() !== '*' && $consumer->getWiki() !== $wiki ) {
$this->logger->debug( 'OAuth request for consumer {consumer} to incorrect wiki', $logData );
return $this->makeException( 'mwoauth-invalid-authorization-wrong-wiki', $wiki );
}
// Ok, use this user!
if ( $this->sessionCookieName === null ) {
// We're not configured to use cookies, so concatenate some of the
// internal consumer-acceptance state to generate an ID.
$id = $this->hashToSessionId( implode( "\n", [
$access->getId(),
$access->getWiki(),
$access->getUserId(),
$access->getConsumerId(),
$access->getAccepted(),
$wiki,
] ) );
$persisted = false;
$forceUse = true;
} else {
$id = $this->getSessionIdFromCookie( $request );
$persisted = $id !== null;
$forceUse = false;
}
$logData['result'] = 'success';
$this->logger->debug( 'OAuth request for consumer {consumer} by user {user}', $logData );
return new SessionInfo( SessionInfo::MAX_PRIORITY, [
'provider' => $this,
'id' => $id,
'userInfo' => UserInfo::newFromUser( $localUser, true ),
'persisted' => $persisted,
'forceUse' => $forceUse,
'metadata' => [
'oauthVersion' => $oauthVersion,
'consumerId' => $consumer->getOwnerOnly() ? null : $consumer->getId(),
'key' => $accessTokenKey,
'rights' => MediaWikiServices::getInstance()
->getGrantsInfo()
->getGrantRights( $access->getGrants() ),
'restrictions' => $consumer->getRestrictions()->toJson(),
],
] );
}
/**
* Determine OAuth version of the request
*
* @param WebRequest $request
* @return int|null if request is not using OAuth header
*/
private function getOAuthVersionFromRequest( WebRequest $request ) {
if ( Utils::hasOAuthHeaders( $request ) ) {
return Consumer::OAUTH_VERSION_1;
}
if ( ResourceServer::isOAuth2Request( $request ) ) {
return Consumer::OAUTH_VERSION_2;
}
return null;
}
/**
* @param ResourceServer &$resourceServer
* @param WebRequest $request
* @return string
* @throws MWOAuthException
*/
private function verifyOAuth2Request( ResourceServer &$resourceServer, WebRequest $request ) {
$request = ServerRequest::fromGlobals()->withHeader(
'authorization',
$request->getHeader( 'authorization' )
);
$response = new Response();
$valid = false;
$resourceServer->verify(
$request,
$response,
static function ( $request, $response ) use ( &$valid ) {
$valid = true;
}
);
if ( $valid ) {
return $resourceServer->getAccessTokenId();
}
throw new MWOAuthException( 'mwoauth-oauth2-invalid-access-token' );
}
/** @inheritDoc */
public function preventSessionsForUser( $username ) {
$id = Utils::getCentralIdFromUserName( $username );
$dbw = Utils::getCentralDB( DB_PRIMARY );
$dbw->startAtomic( __METHOD__ );
try {
// Remove any approvals for the user's consumers before deleting them
$dbw->deleteJoin(
'oauth_accepted_consumer',
'oauth_registered_consumer',
'oaac_consumer_id',
'oarc_id',
[ 'oarc_user_id' => $id ],
__METHOD__
);
$dbw->newDeleteQueryBuilder()
->deleteFrom( 'oauth_registered_consumer' )
->where( [ 'oarc_user_id' => $id ] )
->caller( __METHOD__ )
->execute();
// Remove any approvals by this user, too
$dbw->newDeleteQueryBuilder()
->deleteFrom( 'oauth_accepted_consumer' )
->where( [ 'oaac_user_id' => $id ] )
->caller( __METHOD__ )
->execute();
} catch ( DBError $e ) {
$dbw->rollback( __METHOD__ );
throw $e;
}
$dbw->endAtomic( __METHOD__ );
}
/** @inheritDoc */
public function getVaryHeaders() {
return [
'Authorization' => null,
];
}
/**
* Fetch the access data, if any, for this user-session
* @param UserIdentity|null $userIdentity
* @return array|null
*/
private function getSessionData( ?UserIdentity $userIdentity = null ) {
if ( $userIdentity ) {
$user = User::newFromIdentity( $userIdentity );
$session = $user->getRequest()->getSession();
if ( $session->getProvider() === $this &&
$user->equals( $session->getUser() )
) {
return $session->getProviderMetadata();
}
} else {
$session = SessionManager::getGlobalSession();
if ( $session->getProvider() === $this ) {
return $session->getProviderMetadata();
}
}
return null;
}
/** @inheritDoc */
public function getAllowedUserRights( SessionBackend $backend ) {
if ( $backend->getProvider() !== $this ) {
throw new InvalidArgumentException( 'Backend\'s provider isn\'t $this' );
}
$data = $backend->getProviderMetadata();
if ( $data ) {
return $data['rights'];
}
// Should never happen
$this->logger->debug( __METHOD__ . ': No provider metadata, returning no rights allowed' );
return [];
}
public function getRestrictions( ?array $data ): ?MWRestrictions {
if ( $data && isset( $data['restrictions'] ) && is_string( $data['restrictions'] ) ) {
try {
return MWRestrictions::newFromJson( $data['restrictions'] );
} catch ( \InvalidArgumentException $e ) {
$this->logger->warning( __METHOD__ . ': Failed to parse restrictions: {restrictions}', [
'restrictions' => $data['restrictions']
] );
return null;
}
}
return null;
}
/**
* Disable certain API modules when used with OAuth
*
* @param ApiBase $module
* @param UserIdentity $userIdentity
* @param string|array &$message
* @return bool
*/
public function onApiCheckCanExecute( $module, $userIdentity, &$message ) {
global $wgMWOauthDisabledApiModules;
if ( !$this->getSessionData( $userIdentity ) ) {
return true;
}
foreach ( $wgMWOauthDisabledApiModules as $badModule ) {
if ( $module instanceof $badModule ) {
$message = ApiMessage::create(
[ 'mwoauth-api-module-disabled', $module->getModuleName() ],
'mwoauth-api-module-disabled'
);
return false;
}
}
return true;
}
/**
* Record the fact that OAuth was used for anything added to RecentChanges.
*
* @param RecentChange $rc
* @return bool true
*/
public function onRecentChange_save( $rc ) {
$consumerId = $this->getPublicConsumerId( $rc->getPerformerIdentity() );
if ( $consumerId !== null ) {
$rc->addTags( Utils::getTagName( $consumerId ) );
}
return true;
}
/**
* Get the consumer ID of the non-owner-only OAuth consumer associated with this user, or null.
* @param UserIdentity|null $userIdentity
* @return int|null
*/
protected function getPublicConsumerId( ?UserIdentity $userIdentity = null ) {
$data = $this->getSessionData( $userIdentity );
if ( $data && isset( $data['consumerId'] ) ) {
return $data['consumerId'];
}
return null;
}
/**
* Record the fact that OAuth was used for marking an existing RecentChange as patrolled.
* (RecentChange::doMarkPatrolled() does not use RecentChange::save()
* and therefore bypasses the above hook handler.)
*
* @param int $rcid
* @param User $user
* @param bool $wcOnlySysopsCanPatrol
* @param bool $auto
* @param string[] &$tags
*
* @return bool true
*/
public function onMarkPatrolled(
$rcid,
$user,
$wcOnlySysopsCanPatrol,
$auto,
&$tags
) {
$consumerId = $this->getPublicConsumerId( $user );
if ( $consumerId !== null ) {
$tags[] = Utils::getTagName( $consumerId );
}
return true;
}
/**
* OAuth tokens already protect against CSRF. CSRF tokens are not required.
*
* @return bool true
*/
public function safeAgainstCsrf() {
return true;
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 20 2026, 00:13 (9 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
25/0d/70a22c0ed7ab7840dcfbc0b0a99b
Default Alt Text
SessionProvider.php (15 KB)

Event Timeline