This page explains how to develop applications that can integrate with a wiki runningExtension:OAuth (an extension which turns MediaWiki into anOAuth server) to securely request permission to act on the user's behalf.
OAuth allows an application to request permission from a user to act through that user's wiki account, without knowing the user's password, and without being able to do everything the user could (e.g. the app might be able to edit articles but not delete them, so even users with extended permissions can safely use OAuth-enabled tools).
This happens via theOAuth 2.0 orOAuth 1.0a protocol, and consists of three stages:
OAuth is a widely used open standard (you can see it on sites like Google or Facebook or GitHub, e.g. when using accounts at those sites to log in elsewhere).
For a slightly larger nutshell on OAuth 1.0a, seethese slides.
The registration is basically the same for OAuth 1.0a and OAuth 2, with the difference being only the presence of a few form fields. The remaining parts differ significantly depending on the OAuth version.Generally, OAuth 2 is recommended if available in your environment.
To register a new OAuth application, submit the form atSpecial:OAuthConsumerRegistration/propose
.Try to give sufficient information about the application for admins and users to decide whether it can be trusted.URLs to places with more information (such as the application itself, the source code or on-wiki documentation) can be useful.For OAuth applications to be used on Wikimedia projects, seeapp guidelines.
Besides the descriptive ones, the fields have the following meaning:
This consumer is for use only by<user>
: whether you useowner-only consumers (which do not need to be reviewed or authorised but are only usable by yourself).callback URL
(irrelevant to owner-only consumers): the URL where the user returns after authorisation is checked against this. (This is an extra layer of security against an attacker trying to steal credentials during authorisation.) If theuse as prefix
option is enabled, the URL must start with this (the check is dumb so make sure to add at least a/
after the domain name), otherwise it must be an exact match. If you are developing/testing your local machine, specifyLocalhost in this field (e.g.http://localhost:8080/
).Applicable project
(for wiki farms only): project(s) on which to use your application. You can limit the application to a single wiki or have it work everywhere.Types of grants
/Applicable grants
: the permissions needed by your application (be conservative). The actual permissions will be an intersection of this and what permissions the user has. Currently, the user must authorise grouped permissions, not individual ones (T59505).Allowed IP ranges
: an optional specification of the IP ranges that you use, where OAuth requests not matching this will be rejected. This is an extra layer of security against an attacker stealing your application's credentials and trying to impersonate it. This is one of the few settings that you'll be able to change later.Public RSA key
(OAuth 1.0a only): public key used by your application for signing requests. You can just leave this empty (most applications do) to use a slightly simpler shared-secret mechanism instead. This is one of the few settings that you'll be able to change later. After registration, you'll receive the credentials needed to use OAuth. You will be able to use it immediately with your own user account (this is meant for testing); others will only be able to use it once it is approved by an administrator.If you have changed your mind, you can disable the application underm:Special:OAuthConsumerRegistration/list
.The list of applications (approved or otherwise) is public and can be browsed atSpecial:OAuthListConsumers
.
When registering the application, you receive two pieces of credentials: the application token (a public ID for the application) and the application secret (sort of like a password).
To be able to identify a user or make API requests in their name, you need to get another set of credentials (these ones specific to that user): the access token and access secret.[1]
To get them, you need to go through the authorisation process, which consists of three steps:[2]
Special:OAuth/initiate
, signed with the application key and secret, with the callback URL (where the user will be sent after a successful authorisation) passed as theoauth_callback
query parameter (if you have set a constant URL at registration, the value of the parametermust beoob
).[3] If you are successful, the response will be a JSON object withtoken
andkey
fields—the request token and request secret. (If not, it will have anerror
field.)Special:OAuth/authorize
, with the application token and request token passed as query parameters (oauth_consumer_key
andoauth_token
, respectively).[4] The user will see an authorisation dialog with some basic information about the application and the list of grants, and can decide to authorise or cancel.oauth_verifier
will contain the verification code that you can use to exchange the request token and secret for the access token and secret. To do this, send a request toSpecial:OAuth/token
[3] which includes theoauth_verifier
parameter you just received and is signed with the application token and secret and the request token and secret. The response will contain the access token/secret (in the same format as the request token/secret in step 1).The access token and secret is what you'll need to sign API requests.The request token and secret is not useful anymore and can be discarded.The access token will remain valid indefinitely, unless the user revokes it.(If you prefer not to store it, you can just repeat the authorisation process at any time though.)
Applications which only need minimal privileges (have been registered asUser identity verification only) can use/authenticate
instead of/authorize
in step 2.This works the same way, but the user will only see the authorisation dialog if they have not authorised this application before; otherwise the authorisation will silently succeed.
Chances are whatever language/framework you are using will have a library to support this procedure, so you don't have to implement it manually—each step will be a single function call.See below for examples.
To take advantage of the authorisation, requests have to be signed with the application token/secret and access token/secret.When that's successfully done, the wiki will treat the request as if it was made by the authorising user.Only API requests can be made via OAuth, with one exception (see next section).Certain API modules which would not make sense with OAuth (such as login/logout) or would allow privilege escalation (such as the centralauthtoken API) are disabled.
Applications registered asUser identity verification only cannot use the API at all.
The OAuth extension includes a custom protocol (similar to OpenID Connect) for authenticating the user.To use this, send a signed OAuth request toSpecial:OAuth/identify
:[3]the response will be aJWT (a signed JSON object) including the name of the user, theircentral ID (under the keysub
) and various other information (such as their user groups and whether they are blocked; also the email address if the application was registered with the right grant type).This is more secure than using the API (e.g. the userinfo module) for authentication, which could be subject to man-in-the-middle attacks; always use this if you need to identify a user!Also, make sure you properly validate the JWT (there are many libraries which can help with that).You should check each of the following: the issuer (iss
) matches the domain name of the wiki, the audience (aud
) matches your application key, the issued-at time (iat
) is in the past and reasonably close to current time, the expiration time (exp
) is in the future, the nonce (nonce
) matches the one you sent in the request.
Steps 1 and 3 of the authorisation process require signing the request; API requests andSpecial:OAuth/identify
must likewise be signed.The signing process is detailed insection 9 of the OAuth spec, but it is cumbersome to implement by hand andmany libraries are available.You can find code samples and an overview of how to do it by hand in theowner-only consumer documentation.(That is for signing with the consumer token/secret and access token/secret.Modify as appropriate to sign with the consumer token/secret and request token/secret (authorisation step 3) or consumer token/secret only (authorisation step 1).)
When registering the application, you receive two pieces of credentials: the client application key (a public ID for the application, also called the client ID or consumer key) and the client application secret (a confidential password).To be able to identify a user or make API requests in their name, you need to get another credential (this one specific to that user): the access token. To get it, you need to go through thethe OAuth 2 Authorization Code flow, which consists of two steps:[5]
oauth2/authorize
under the wiki's REST endpoint (usuallyrest.php
), withresponse_type=code
and the consumer key (also called the client application key) as theclient_id
, possibly astate
if you want, and optionally theredirect_uri
(if yes, it must be the same as in your application request). If your consumer is non-confidential, you'll also need to include aPKCE code challenge (code_challenge
andcode_challenge_method=S256
). The user will see an authorisation dialog with some basic information about the application and the list of grants, and can decide to authorise or cancel.code
will contain the authorisation code that you can use to fetch the access token. To do this, send a POST request tooauth2/access_token
under the wiki's REST endpoint (usuallyrest.php
), includinggrant_type=authorization_code
, thecode
parameter you just received, your client authentication (typically asclient_id
and, for confidential clients,client_secret
), theredirect_uri
if and only if you specified it in the previous step (must be the same value for both), and if non-confidential the PKCEcode_verifier
andcode_challenge_method
. The response will contain the access token and a refresh token.The access token is what you'll need to send future API requests.The refresh token can be used tofetch a new access token if the original access token expires.If you prefer not to store either token, you can just repeat the authorisation process at any time.(Note that non-confidential clients can currently only use refresh tokens using their client secret keys, not using their client IDs only, seeT323855.)
Chances are whatever language/framework you are using will have a library to support this procedure so you don't have to implement it manually - each step will be a single function call.See below for examples.
To take advantage of the authorisation, requests have to include the access token.When that's successfully done, the wiki will treat the request as if it was made by the authorising user.Only API requests can be made via OAuth 2.Certain API modules which would not make sense with OAuth (such as login/logout) or would allow privilege escalation (such as the centralauthtoken API) are disabled.If the access token is used to fetch a CSRF token or other tokens, the access token must still be passed (as a header) with requests that use those tokens.
Applications which need minimal privileges (have been registered asUser identity verification only) cannot use the API at all.
API requests includingrest.php/oauth2/resource/profile
must be authenticated with an HTTP Authorization header containing the access token, like
Authorization: Bearer abcde....6789
The OAuth extension includes a somewhat incomplete[6] implementation of OpenID Connect for authenticating the user.To use this, send an authenticated OAuth GET request to theoauth2/resource/profile
API (MediaWiki's implementation of what the OIDC spec calls the UserInfo enpoint) under the wiki's REST endpoint (usuallyrest.php
); the response will include the name of the user and various other information. Note that the GET request must use the HTTPAuthorization
header, not a query string token.
You can register an OAuth application onbeta meta and test your code against that.
If you want to improve the extension itself, or debug protocol issues in detail, OAuth is available in theMediaWiki-Vagrant development environment.Add theoauth
role, and your local wiki will be able to authorise OAuth apps.
$vagrantroles enableoauth$vagrantprovision
Once the code is nearly ready, you can register an OAuth application on the real wiki.You will be able to test it with the same user account used for registering, even before it gets reviewed by admins.
If you are creating an application for Wikimedia projects, consider hosting it atWikimedia Toolforge, a free tool forge and hosting platform for Wikimedia-related services.
OAuth Hello World – easy to understand demo application written in PHP without any libraries.
PHP application using classes from the OAuth extension codebase.(TODO convert this to actually use oauthclient-php! Probably just a bunch ofuse
declarations.)
Before Starting:
$opensslgenrsa-outappkey.pem4096$opensslrsa-inappkey.pem-pubout>appkey.pub
PHP source code |
---|
<?phpif(PHP_SAPI!=='cli'){die("CLI-only test script\n");}/** * A basic client for overall testing */functionwfDebugLog($method,$msg){//echo "[$method] $msg\n";}require'OAuth.php';require'MWOAuthSignatureMethod.php';$consumerKey='';#$consumerSecret = ''; // We don't need this, since we're using RSA, except to validate the /identify call$privateKey=file_get_contents('appkey.pem');$baseurl='http://<wiki>/wiki/Special:OAuth';$endpoint_req=$baseurl.'/initiate?format=json&oauth_callback=oob';// format=json makes php a little easier$endpoint_acc=$baseurl.'/token?format=json';$endpoint_id=$baseurl.'/identify';$c=newOAuthConsumer($consumerKey,$privateKey);// Make sure we sign title and format$parsed=parse_url($endpoint_req);$extraSignedParams=array();parse_str($parsed['query'],$extraSignedParams);$extraSignedParams['title']='Special:OAuth/initiate';$init_req=OAuthRequest::from_consumer_and_token($c,// OAuthConsumer for your appNULL,// User token, NULL for calls to initiate"GET",// http method$endpoint_req,// endpoint url (this is signed)$extraSignedParams// extra parameters we want to sign (must include title));$rsa_method=newMWOAuthSignatureMethod_RSA_SHA1(newOAuthDataStore(),$privateKey);$init_req->sign_request($rsa_method,// OAuthSignatureMethod$c,// OAuthConsumer for your appNULL// User token, NULL for calls to initiate);echo"Getting request token with:$init_req\n";$ch=curl_init();curl_setopt($ch,CURLOPT_URL,(string)$init_req);// Pass OAuth in GET paramscurl_setopt($ch,CURLOPT_HTTPGET,true);curl_setopt($ch,CURLOPT_HEADER,false);curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);$data=curl_exec($ch);if(!$data){'Curl error: '.curl_error($ch);}echo"Returned:$data\n\n";$requestToken=json_decode($data);print"Visit$baseurl/authorize?oauth_token={$requestToken->key}&oauth_consumer_key=$consumerKey\n";// ACCESS TOKENprint"Enter the verification code:\n";$fh=fopen("php://stdin","r");$line=fgets($fh);$rc=newOAuthToken($requestToken->key,$requestToken->secret);$parsed=parse_url($endpoint_acc);parse_str($parsed['query'],$params);$params['oauth_verifier']=trim($line);$params['title']='Special:OAuth/token';$acc_req=OAuthRequest::from_consumer_and_token($c,$rc,"GET",$endpoint_acc,$params);$acc_req->sign_request($rsa_method,$c,$rc);echo"Calling:$acc_req\n";unset($ch);$ch=curl_init();curl_setopt($ch,CURLOPT_URL,$endpoint_acc);curl_setopt($ch,CURLOPT_HEADER,0);curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);curl_setopt($ch,CURLOPT_HTTPHEADER,array($acc_req->to_header()));// Set the Authorization Header$data=curl_exec($ch);if(!$data){'Curl error: '.curl_error($ch);}echo"Returned:$data\n\n";$acc=json_decode($data);$accessToken=newOAuthToken($acc->key,$acc->secret);/** * Insecurely call the api for information about the user. A MITM can * forge a response from the server, so don't rely on this for identity! */$apiurl='http://<wiki>/w/api.php';$apiParams=array('action'=>'query','meta'=>'userinfo','uiprop'=>'rights','format'=>'json',);$api_req=OAuthRequest::from_consumer_and_token($c,// Consumer$accessToken,// User Access Token"GET",// HTTP Method$apiurl,// Endpoint url$apiParams// Extra signed parameters);$api_req->sign_request($rsa_method,$c,$accessToken);$ch=curl_init();curl_setopt($ch,CURLOPT_URL,$apiurl."?".http_build_query($apiParams));curl_setopt($ch,CURLOPT_HEADER,0);curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);curl_setopt($ch,CURLOPT_HTTPHEADER,array($api_req->to_header()));// Authorization header required for api$data=curl_exec($ch);if(!$data){'Curl error: '.curl_error($ch);}echo"Returned:$data\n\n";/** * Securely get the identity of the user */$consumerSecret='';$extraSignedParams=array('title'=>'Special:OAuth/identify');$req=OAuthRequest::from_consumer_and_token($c,$accessToken,"GET",$endpoint_id,$extraSignedParams);$req->sign_request($rsa_method,$c,$accessToken);echo"Calling: '$endpoint_id'\nHeader:{$req->to_header()}\n\n";$ch=curl_init();curl_setopt($ch,CURLOPT_URL,$endpoint_id);curl_setopt($ch,CURLOPT_HEADER,0);curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);curl_setopt($ch,CURLOPT_HTTPHEADER,array($req->to_header()));$data=curl_exec($ch);if(!$data){'Curl error: '.curl_error($ch);}$identity=JWT::decode($data,$consumerSecret);// Validate the JWTif(!validateJWT($identity,$consumerKey,$req->get_parameter('oauth_nonce'))){print"The JWT did not validate";}else{print"We got a valid JWT, describing the user as:\n";print" * Username:{$identity->username}\n";print" * User's current groups: ".implode(',',$identity->groups)."\n";print" * User's current rights: ".implode(',',$identity->rights)."\n";}/** * Validate a JWT, to ensure this isn't a reply, spoof, etc. * @param $identity the decoded JWT * @param $consumerKey your App's Key * @param $nonce the nonce sent with your request, which should be returned */functionvalidateJWT($identity,$consumerKey,$nonce){$expectedConnonicalServer='http://<wiki>';// Verify the issuer is who we expect (server sends $wgCanonicalServer)if($identity->iss!==$expectedConnonicalServer){print"Invalid Issuer";returnfalse;}// Verify we are the intended audienceif($identity->aud!==$consumerKey){print"Invalid Audience";returnfalse;}// Verify we are within the time limits of the token. Issued at (iat) should be// in the past, Expiration (exp) should be in the future.$now=time();if($identity->iat>$now||$identity->exp<$now){print"Invalid Time";returnfalse;}// Verify we haven't seen this nonce before, which would indicate a replay attackif($identity->nonce!==$nonce){print"Invalid Nonce";returnfalse;}returntrue;} |
Python source code |
---|
frommwoauthimportConsumerToken,Handshakerimportrequestsfromrequests_oauthlibimportOAuth1fromsix.movesimportinput# For compatibility between python 2 and 3# Consruct a "consumer" from the key/secret provided by MediaWikiimportconfig# You'll need to provide this#Create a file called config.py somewhere where it will be found by python, e.g. in the same directory as this script,#with the following content (not including the # characters! )#consumer_key = "the consumer token you got when you registered your applicaton"#consumer_secret = "the secret token you got when you registered your application"#For example:#consumer_key = "20bc67da5081a30c736340c493f60d14"#consumer_secret = "af4313371bb2fb38e81fe7d300080085f52849f9"consumer_token=ConsumerToken(config.consumer_key,config.consumer_secret)# Construct handshaker with wiki URI and consumerhandshaker=Handshaker("https://en.wikipedia.org/w/index.php",consumer_token)# Step 1: Initialise -- ask MediaWiki for a temporary key/secret for userredirect,request_token=handshaker.initiate()# Step 2: Authorise -- send user to MediaWiki to confirm authorisationprint("Point your browser to:%s"%redirect)#response_qs=input("Response query string: ")# Step 3: Complete -- obtain authorised key/secret for "resource owner"access_token=handshaker.complete(request_token,response_qs)# Construct an auth object with the consumer and access tokensauth1=OAuth1(consumer_token.key,client_secret=consumer_token.secret,resource_owner_key=access_token.key,resource_owner_secret=access_token.secret)# Now, accessing the API on behalf of a userprint("Reading top 10 watchlist items")response=requests.get("https://en.wikipedia.org/w/api.php",params={'action':"query",'list':"watchlist",'wllimit':10,'wlprop':"title|comment",'format':"json"},auth=auth1)foriteminresponse.json()['query']['watchlist']:print("{title}\t{comment}".format(**item)) |
See:wikitech:Help:Toolforge/My first Flask OAuth tool
Before you begin:
$gogetgithub.com/mrjones/oauth
Go source code |
---|
packagemainimport("fmt""os""github.com/mrjones/oauth""io/ioutil""strconv")funcmain(){varconsumerKeystring=""varconsumerSecretstring=""iflen(consumerKey)==0||len(consumerSecret)==0{os.Exit(1)}c:=oauth.NewConsumer(consumerKey,consumerSecret,oauth.ServiceProvider{RequestTokenUrl:"http://<wiki>/wiki/index.php/Special:OAuth/initiate",AuthorizeTokenUrl:"http://<wiki>/wiki/index.php/Special:OAuth/authorize",AccessTokenUrl:"http://<wiki>/wiki/index.php/Special:OAuth/token",})c.Debug(true)c.AdditionalParams=map[string]string{"title":"Special:OAuth/initiate",}c.AdditionalAuthorizationUrlParams=map[string]string{"oauth_consumer_key":consumerKey,}requestToken,url,err:=c.GetRequestTokenAndUrl("oob")iferr!=nil{fmt.Println(err)}fmt.Println("Got token "+requestToken.Token)fmt.Println("(1) Go to: "+url)fmt.Println("(2) Grant access, you should get back a verification code.")fmt.Println("(3) Enter that verification code here: ")verificationCode:=""fmt.Scanln(&verificationCode)c.AdditionalParams=map[string]string{"title":"Special:OAuth/token",}accessToken,err:=c.AuthorizeToken(requestToken,verificationCode)iferr!=nil{fmt.Println(err)}fmt.Println("Got access token "+accessToken.Token)c.AdditionalParams=map[string]string{}response,err:=c.Get("http://<wiki>/wiki/api.php",map[string]string{"action":"query","meta":"userinfo","uiprop":"rights","format":"json",},accessToken)iferr!=nil{fmt.Println(err)}fmt.Println("\tResponse Status: '"+response.Status+"'\n")fmt.Println("\tResponse Code: "+strconv.Itoa(response.StatusCode)+"\n")bytes,_:=ioutil.ReadAll(response.Body)fmt.Println("\tResponse Body: "+string(bytes)+"\n")} |
en.wikipedia.org/w/index.php?title=Special:OAuth/initiate
.en.wikipedia.org/wiki/Special:OAuth/authorize
. A bit embarrassing, we know.