OAuth/Owner-only consumers

From MediaWiki.org
Jump to navigation Jump to search

Owner-only consumers are a method to use OAuth for authentication and permission control while avoiding most of the complexity of the OAuth protocol (which is in the grant authorization process). It's meant for bots and similar tools which always authenticate with the same user account. To use it, the target wiki must have version 1.27 or higher of the OAuth extension installed.

To work as an owner-only consumer, the application must take four strings as configuration settings: the consumer key, the consumer secret, the access token and the access secret. The user can obtain those via Special:OAuthConsumerRegistration/propose. The option "owner-only" has to be checked. (In case of a wikifarm, the special page is only available on the central wiki of the farm. In case of Wikimedia, it's at meta:Special:OAuthConsumerRegistration/propose.)

The application can then authenticate API requests by adding an Authorization header which is computed from those parameters as defined in the OAuth 1.0a standard; libraries exist in many languages to help with this.

Some libraries call this the two-legged OAuth 1.0 protocol. The OAuth Bible more correctly calls it one-legged.

The code snippets below assume the application uses a shared secret (HMAC-SHA1) for signing (i.e., the RSA field was left empty at registration).


Using oauthclient-php:

use MediaWiki\OAuthClient\Consumer;
use MediaWiki\OAuthClient\Token;
use MediaWiki\OAuthClient\Request;
use MediaWiki\OAuthClient\SignatureMethod\HmacSha1;

$consumer = new Consumer( $consumerKey, $consumerSecret );
$accessToken = new Token( $accessToken, $accessSecret );
$request = Request::fromConsumerAndToken( $consumer, $accessToken, 'GET', 'https://en.wikipedia.org/w/api.php', $apiParams );
$request->signRequest( new HmacSha1(), $consumer, $accessToken );
$authorizationHeader = $request->toHeader();
// Then use this header on a request, for example:
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => 'https://en.wikipedia.org/w/api.php',
    CURLOPT_USERAGENT => 'My_bot',  
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($apiParams),
    CURLOPT_HTTPHEADER => [$authenticationHeader],
$result = curl_exec($ch);

Using the PECL package:

$oauth = new OAuth( $consumerKey, $consumerSecret );
$oauth->setToken( $accessToken, $accessSecret );
// Generate a header, perhaps to be sent via a CURL object:
$authorizationHeader = 'Authorization: ' . $oauth->getRequestHeader( 'GET', 'https://en.wikipedia.org/w/api.php', $apiParams );
// Or simply fetch a page directly:
$oauth->fetch('https://en.wikipedia.org/w/api.php', $apiParams);


Using requests_oauthlib:

import requests
from requests_oauthlib import OAuth1

auth = OAuth1(consumer_key, consumer_secret, access_token, access_secret)
requests.post(url='https://en.wikipedia.org/w/api.php', data=data, auth=auth)


Using Net::OAuth:

use Net::OAuth;

my $request = Net::OAuth->request( 'protected resource' )->new(
    request_method => 'POST',
    request_url => 'https://en.wikipedia.org/w/api.php',
    consumer_key => $consumer_key,
    consumer_secret => $consumer_secret,
    token => $access_token,
    token_secret => $access_secret,
    signature_method => 'HMAC-SHA1',
    timestamp => time(),
    nonce => $nonce,

    # Only if using GET or POST as application/x-www-form-urlencoded,
    # omit extra_params if POSTing as multipart/form-data.
    extra_params => \%params, 
my $authorizationHeader = $request->to_authorization_header();

To generate the nonce, you could just do something like int( rand( 2**32 ) ), but using a random number generator such as Bytes::Random::Secure would be more secure:

use Bytes::Random::Secure ();

# This object may be kept and reused for the lifetime of the program
my $rand = Bytes::Random::Secure->new( NonBlocking => 1 );

# This generates the nonce, and replaces some characters with safer ones.
my $nonce = $rand->bytes_base64( 15 );
$nonce =~ tr!+/\n!-_!;

Awk / shell[edit]

Using GNU Awk and openssl. Function library in Wikiget.

# MWOAuthGenerateHeader() - MediaWiki Generate OAuth Header
#   Credit: translation of PHP script https://www.mediawiki.org/wiki/OAuth/Owner-only_consumers#Algorithm
function MWOAuthGenerateHeader(consumerKey, consumerSecret, accessKey, accessSecret, url, method, data,  

                               signatureBaseParts,signatureBaseString,hmac,header,save_sorted) {

  # sort associative arrays by index string ascending (lexicographic order)
    PROCINFO["sorted_in"] = "@ind_str_asc"

    nonce = strip(splitx(sys2varPipe(systime() randomnumber(1000000), "openssl md5"), "= ", 2))

    asplit(headerParams, "oauth_consumer_key=" consumerKey " oauth_token=" accessKey " oauth_signature_method=HMAC-SHA1 oauth_timestamp=" systime() " oauth_nonce=" nonce " oauth_version=1.0") 
    asplit(dataArr, data, "=", "&")
    for (k in allParams) 
        allParamsJoined[i++] = k "=" allParams[k]

    url2 = urlElement(url, "scheme") "://" tolower(urlElement(url, "netloc")) urlElement(url, "path")
    asplit(signatureBaseParts, "0=" toupper(method) " 1=" url " 2=" join(allParamsJoined, 0, length(allParamsJoined) - 1, "&"))
    signatureBaseString = urlencodeawk(signatureBaseParts[0], "rawphp") "&" urlencodeawk(signatureBaseParts[1], "rawphp") "&" urlencodeawk(signatureBaseParts[2], "rawphp")
    hmac = sys2varPipe(signatureBaseString, "openssl sha1 -hmac " shquote(urlencodeawk(consumerSecret, "rawphp") "&" urlencodeawk(accessSecret, "rawphp")) " -binary")
    headerParams["oauth_signature"] = strip(sys2varPipe(hmac, "openssl base64") )

    for (k in headerParams) 
        header[j++] = urlencodeawk(k, "rawphp") "=" urlencodeawk(headerParams[k], "rawphp")

    return sprintf("%s", "Authorization: OAuth " join(header, 0, length(header) - 1, ", "))


Using ScribeJava:

import java.io.*;
import java.net.*;
import java.nio.charset.*;
import java.util.*;

import com.github.scribejava.apis.MediaWikiApi;
import com.github.scribejava.core.builder.*;
import com.github.scribejava.core.model.*;
import com.github.scribejava.core.oauth.*;

public class MediaWikiOAuth1 {

    private URL apiUrl = new URL("https://commons.wikimedia.org/w/api.php");

    private String consumerToken = "";
    private String consumerSecret = "";
    private String accessToken = "";
    private String accessSecret = "";
    private String userAgent = ""; // https://meta.wikimedia.org/wiki/User-Agent_policy

    private OAuth1AccessToken oAuthAccessToken;
    private OAuth10aService oAuthService;
    private String csrfToken;

    public MediaWikiOAuth1() throws IOException {
        oAuthService = new ServiceBuilder(consumerToken).apiSecret(consumerSecret).build(MediaWikiApi.instance());
        oAuthAccessToken = new OAuth1AccessToken(accessToken, accessSecret);

        // Check authentication
        // Fetch CSRF token, mandatory for upload using the Mediawiki API
        csrfToken = ""; // should be extracted from above JSON response
        // Upload a file using the CSRF token
        System.out.println(upload("wikicode", "filename.jpg", new URL("http://server/filename.jpg")));

    public String queryTokens() throws IOException {
        return apiHttpGet("?action=query&meta=tokens");

    public String queryUserInfo() throws IOException {
        return apiHttpGet("?action=query&meta=userinfo&uiprop=blockinfo|groups|rights|ratelimits");

    private String apiHttpGet(String path) throws IOException {
        return httpGet(apiUrl.toExternalForm() + path + "&format=json");

    private String apiHttpPost(Map<String, String> params) throws IOException {
        return httpPost(apiUrl.toExternalForm(), params);

    private String httpGet(String url) throws IOException {
        return httpCall(Verb.GET, url, Collections.emptyMap(), Collections.emptyMap());

    private String httpPost(String url, Map<String, String> params) throws IOException {
        return httpCall(Verb.POST, url, Map.of("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"),

    private String httpCall(Verb verb, String url, Map<String, String> headers, Map<String, String> params)
            throws IOException {
        OAuthRequest request = new OAuthRequest(verb, url);
        request.addHeader("User-Agent", userAgent);
        oAuthService.signRequest(oAuthAccessToken, request);
        try {
            return oAuthService.execute(request).getBody();
        } catch (InterruptedException | ExecutionException e) {
            throw new IOException(e);

    public String upload(String wikiCode, String filename, URL url) throws IOException {
        String apiResponse = apiHttpPost(Map.of(
                "action", "upload", 
                "comment", "", 
                "format", "json", 
                "filename", filename, 
                "ignorewarnings", "1", 
                "text", wikiCode, 
                "token", csrfToken, 
                "url", url.toExternalForm()));

        if (!apiResponse.contains("success")) {
            throw new IllegalArgumentException(apiResponse);
        return apiResponse;


 Authorization: OAuth oauth_consumer_key="<consumer key>",
                      oauth_token="<access token>",
                      oauth_signature="<base64(HMAC_SHA1(key="<consumer secret>&<token secret>", text="<signature base string>"))>",
                      oauth_timestamp="<current time as unix timestamp>",
                      oauth_nonce="<a long random string>",

where <signature base string> is the urlencoded, &-concatenated list of the request method, the request endpoint (ie. the full URL to api.php), and all the parameters of the request (GET, POST, and Authorization header, except oauth_signature itself) in lexicographic order.

For example, computing the header in PHP would look like this (cutting some corners such as nested parameter handling):

function oauthHeader( $consumerKey, $consumerSecret, $accessToken, $accessSecret, $method, $url, $data ) {
    $headerParams = [
        'oauth_consumer_key' => $consumerKey,
        'oauth_token' => $accessToken,
        'oauth_signature_method' => 'HMAC-SHA1',
        'oauth_timestamp' => time(),
        'oauth_nonce' => md5( microtime() . mt_rand() ),
        'oauth_version' => '1.0',

    $allParams = array_merge( $headerParams, $data );
    uksort( $allParams, 'strcmp' );
    $allParamsJoined = array();
    foreach ( $allParams as $key => $value ) {
        $allParamsJoined[] = rawurlencode( $key ) . '=' . rawurlencode( $value );

    $urlParts = parse_url( $url );
    $url = $urlParts['scheme'] . '://' . strtolower( $urlParts['host'] ) . $urlParts['path'];

    $signatureBaseParts = [ strtoupper( $method ), $url, implode( '&', $allParamsJoined ) ];
    $signatureBaseString = implode( '&', array_map( 'rawurlencode', $signatureBaseParts ) );
    $headerParams['oauth_signature'] = base64_encode( hash_hmac( 'sha1', $signatureBaseString, rawurlencode( $consumerSecret ) . '&' . rawurlencode( $accessSecret ), true ) );

    $header = array();
    foreach ( $headerParams as $key => $value ) {
        $header[] = rawurlencode( $key ) . '=' . rawurlencode( $value );
    return 'Authorization: OAuth ' . implode( ', ', $header );

See also[edit]