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).

PHP[edit]

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_RETURNTRANSFER => true,
    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);

Python[edit]

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)

Perl[edit]

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, 
);
$request->sign();
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,  

                               nonce,headerParams,dataArr,allParams,allParamsJoined,k,i,j,url2,
                               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, "=", "&")
    concatarray(headerParams,dataArr,allParams)
    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, ", "))
}

Algorithm[edit]

 Authorization: OAuth oauth_consumer_key="<consumer key>",
                      oauth_token="<access token>",
                      oauth_signature_method="HMAC-SHA1",
                      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>",
                      oauth_version="1.0"

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]