Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4160062
SessionProvider.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
SessionProvider.php
View Options
<?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
Details
Attached
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)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment