User:CSteipp (WMF)/OAuth demo client

In order to use OAuth to make MediaWiki api calls on behalf of another user, you need to:

  1. Register your application, and get it approved by a wiki admin
  2. Get the user's permission when they are trying to access the resource, and receive the required set of tokens from the wiki
  3. When making api calls, include an Authorization: HTTP header, which identify the user and prove your possession of the authorized token

Register your Application (Consumer)

edit

Obtain Authorized Tokens

edit

You will need to complete this protocol for each user, in order to obtain the unique token and secret for that individual user. In a picture, this looks like [1].

NOTE: You can use the maintained version in either GitHub or Gerrit in examples/testClient.php

<?php

if ( PHP_SAPI !== 'cli' ) {
	die( "CLI-only test script\n" );
}

/**
 * A basic client for overall testing
 */

function wfDebugLog( $method, $msg) {
	echo "[$method] $msg\n";
}

require '~/code/extensions/OAuth/lib/OAuth.php';

$consumerKey = '';
$consumerSecret = '';

$baseurl = 'https://<yourwiki>/index.php?title=Special:OAuth';

$initiateUrl = $baseurl . '/initiate&format=json&oauth_callback=oob';
$tokenUrl = $baseurl . '/token&format=json';


// Get a temporary token
$c = new OAuthConsumer( $consumerKey, $consumerSecret );
$parsed = parse_url( $initiateUrl );
$params = array();
parse_str($parsed['query'], $params);
$req_req = OAuthRequest::from_consumer_and_token($c, NULL, "GET", $initiateUrl, $params);
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$sig_method = $hmac_method;
$req_req->sign_request($sig_method, $c, NULL);

$headers = array( $req_req->to_header() );

echo "Calling: $initiateUrl\n(With OAuth headers): {$headers[0]}\n\n";

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $initiateUrl );
curl_setopt( $ch, CURLOPT_PORT , 443 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, TRUE );
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt( $ch, CURLOPT_HEADER, 0 );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = curl_exec( $ch );
if( !$data ) {
	'Curl error: ' . curl_error( $ch );
}
echo "Returned: $data\n\n";
$token = json_decode( $data );


// Send the user off to Authorize our App
$authorizeUrl = $baseurl . "/authorize&oauth_token={$token->key}&oauth_consumer_key=$consumerKey";
print "Send your user to $authorizeUrl\n";

// Get the Verification code. MediaWiki will redirect the user to the callback url you registered, so
// typically this would be a separate script, but a bot may send the user to a page that just displays
// the verification code and has the user enter it like this...
print "Enter the verification code:\n";
$fh = fopen( "php://stdin", "r" );
$line = fgets( $fh );

$rc = new OAuthConsumer( $token->key, $token->secret );
$parsed = parse_url( $tokenUrl );
parse_str($parsed['query'], $params);
$params['oauth_verifier'] = trim($line);

$acc_req = OAuthRequest::from_consumer_and_token($c, $rc, "GET", $tokenUrl, $params);
$acc_req->sign_request($sig_method, $c, $rc);

echo "Calling: $acc_req\n";

unset( $ch );
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, (string) $acc_req );
curl_setopt( $ch, CURLOPT_PORT , 443 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, TRUE );
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt( $ch, CURLOPT_HEADER, 0 );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
$data = curl_exec( $ch );
if( !$data ) {
	'Curl error: ' . curl_error( $ch );
}

echo "Returned: $data\n\n";

The $data returned above contains the authorized token and secret to use when making api calls on behalf of the user. To use the authorized token/secret to make an edit, you will use some code such as:

<?php
/**
 * A basic client for overall testing - Do an API Edit
 */

function wfDebugLog( $method, $msg) {
	#echo "[$method] $msg\n";
}
require '/home/csteipp/tmp/OAuth/OAuth/lib/OAuth.php';

// You will have a php session, just like a normal user has. So track it.
$ckfile = tempnam ( "/tmp", "CURLCOOKIE" );
$endpoint_api = 'https:///<yourwiki>/api.php';

// Your App's Key and Secret, same as in the last script
$consumerKey = '';
$consumerSecret = '';

// The authorized token and secret obtained by the handshake (unique to each user)
$acctoken = '';
$accsecret = '';

$c = new OAuthConsumer( $consumerKey, $consumerSecret );
$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$sig_method = $hmac_method;

// GET Edit token
// https://localhost/wiki/api.php?action=query&prop=info&intoken=edit&titles=Main%20Page
$qparams = array(
	'action' => 'query',
	'prop' => 'info',
	'intoken' => 'edit',
	'titles' => 'Main Page',
	'format' => 'json',
);
$ac = new OAuthConsumer( $acctoken, $accsecret );
$api_req = OAuthRequest::from_consumer_and_token($c, $ac, "GET", $endpoint_api, $qparams);
$api_req->sign_request($sig_method, $c, $ac);

$qstring = http_build_query( $qparams );
$headers = array( $api_req->to_header() );
echo "Calling:  '$endpoint_api?$qstring'\nHeader: {$headers[0]}\n\n";

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $endpoint_api . "?$qstring" );
curl_setopt( $ch, CURLOPT_PORT , 443 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, TRUE );
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt( $ch, CURLOPT_HEADER, 0 );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_COOKIEJAR, $ckfile );
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = curl_exec( $ch );

if( !$data ) {
	'Curl error: ' . curl_error( $ch );
}

$dataArray = json_decode( $data, true );
$editToken = $dataArray['query']['pages'][1]['edittoken'];
echo "Edit Token: $editToken\n";

// Do the Edit
$qparams = array(
	'action' => 'edit',
	'title' => 'Main_Page',
	'section' => 'new',
	'summary' => 'Hello World',
	'text' => 'Hola',
	'token' => $editToken,
	'format' => 'json',
);

$api_req = OAuthRequest::from_consumer_and_token($c, $ac, "POST", $endpoint_api, $qparams);
$api_req->sign_request($sig_method, $c, $ac);
$headers = array( $api_req->to_header() );
echo "Calling:  '$endpoint_api'\nHeader: {$headers[0]}\n\n";

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $endpoint_api );
curl_setopt( $ch, CURLOPT_PORT , 443 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, TRUE );
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt( $ch, CURLOPT_HEADER, 0 );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
curl_setopt( $ch, CURLOPT_COOKIEJAR, $ckfile );
curl_setopt( $ch, CURLOPT_COOKIEFILE, $ckfile ); # Need to pass back the session cookie here, since that has our edit token
curl_setopt( $ch, CURLOPT_POST, 1 );
curl_setopt( $ch, CURLOPT_POSTFIELDS, http_build_query( $qparams ) ); # Pass string, not array here, so curl encode with application/x-www-form-urlencoded
$data = curl_exec( $ch );

print_r( $data );

Identify the User

edit

This is optional! If a user has authorized your application, MediaWiki can provide your application with a cryptographically signed statement about the authorizing user. This should be used to identify the user, instead of calling the userinfo api, for various security reasons.

The OAuth extension packages the statement in a Json Web Token (JWT). This token is cryptographically signed (using HMAC-sha1) using the shared secret that your application was issued during registration (your Consumer Secret).

Your application needs to both check the signature on the JWT (to ensure an attacker hasn't tampered with the contents) and validate that the JWT was issued to you (to prevent an attacker from replaying a previously issued JWT, for example).

<?php

if ( PHP_SAPI !== 'cli' ) {
	die( "CLI-only test script\n" );
}

/**
 * A basic client for overall testing
 */

function wfDebugLog( $method, $msg) {
	echo "[$method] $msg\n";
}


## Do an API request

require '~/code/extensions/OAuth/lib/OAuth.php';
require '~/code/extensions/OAuth/lib/JWT.php';

$url = 'https://<yourwiki>/index.php?title=Special:OAuth/identify&format=json';

$consumerKey = '<your key>';
$consumerSecret = '<your secret>';
$accessToken = '<user access token>';
$accessSecret = '<user access secret>';

$appToken = new OAuthToken( $consumerKey, $consumerSecret );
$userToken = new OAuthToken( $accessToken, $accessSecret );

$extraSignedParams = array(
	'title' => 'Special:OAuth/identify'
);

$req = OAuthRequest::from_consumer_and_token( $appToken, $userToken, "GET", $url, $extraSignedParams );
$req->sign_request( new OAuthSignatureMethod_HMAC_SHA1(), $appToken, $userToken );


$headers = array( $req->to_header() );

echo "Calling:  '$url'\nHeader: {$req->to_header()}\n\n";

$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $url );
curl_setopt( $ch, CURLOPT_PORT , 443 );
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0 );
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 JWT
if ( !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
 */
function validateJWT( $identity, $consumerKey, $nonce ) {

	$expectedConnonicalServer = <your wiki's connonical url>;

	// Verify the issuer is who we expect (server sends $wgCanonicalServer)
	if ( $identity->iss !== $expectedConnonicalServer ) {
		print "Invalid Issuer";
		return false;
	}

	// Verify we are the intended audience
	if ( $identity->aud !== $consumerKey ) {
		print "Invalid Audience";
		return false;
	}

	// 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";
		return false;
	}

	// Verify we haven't seen this nonce before, which would indicate a replay attack
	if ( $identity->nonce !== $nonce ) {
		print "Invalid Nonce";
		return false;
	}

	return true;
}