OAuth/For Developers

From MediaWiki.org
Jump to: navigation, search

OAuth Security Provisions[edit | edit source]

  • MediaWiki users can allow other websites to edit and perform other actions using the MediaWiki api on their behalf.
  • The attached website does not share the user's password, instead they are issued a unique token and secret to make calls on behalf of the user.
  • The access is limited to explicit sets of permissions (“grants”) for the application.[1]
  • Users can revoke their authorization of an attached application at any time.
  • Administrators can reject entire applications at any time.

MediaWiki Specific Provisions[edit | edit source]

  • Extension:OAuth implements an /identify function to allow the attached application to identify the authorizing user[2]

Signatures and TLS[edit | edit source]

  • For OAuth 1.0a, all interactions between MediaWiki and the attached application are signed with either a shared secret (using HMAC-SHA1), or RSA signature.
  • If shared secrets are used, the attached application must use TLS when negotiating the shared secret.
  • If RSA is used, TLS is not required (except for the initial registration)

Intended Users[edit | edit source]

  • Websites that want to take actions on MediaWiki on behalf of their users
  • Bots
  • But not...
    • Desktop applications (the Consumer Secret needs to be secret!)
    • Websites wanting single sign-on with MediaWiki

Application Approval[edit | edit source]

  • Wiki administrators will verify that OAuth applications are written by reputable developers, and the developers are intending to use OAuth correctly.
  • Application developers must apply to have their application (Consumer) approved at Special:OAuthConsumerRegistration/propose
  • A user with the oauth-admin right must approve the application (currently Stewards)
  • Your MediaWiki user can authorize your app while waiting for approval, so as a developer, you can start integrating your app immediately, without waiting for approval (you'll just have to get approval before other users can authorize your app)


The Protocol Flow[edit | edit source]

OAuth-basicSVG.svg

Attached Application Responsibility[edit | edit source]

  • Establish user's session
  • Special:OAuth/initiate - get a temporary (request) token
  • Redirect the user's browser to Special:Oauth/authorize?oauth_token=<temp token>&oauth_consumer_key=<your app's key>
  • The user will be redirected back the the url you registered
  • Special:OAuth/token – get the authorized (access) token for this user
  • Set an Authorization: header when calling api.php with oauth_version, oauth_nonce, oauth_timestamp, oauth_consumer_key, oauth_token, oauth_signature_method, oauth_signature

Developing[edit | edit source]

OAuth is now available in your vagrant development environment. Add the oauth role, and your wiki will be able to authorize OAuth apps.

$ vagrant enable-role oauth

Demo Time![edit | edit source]

PHP demo cli client with RSA keys[edit | edit source]

Before Starting:

$ openssl genrsa -out appkey.pem 4096
$ openssl rsa -in appkey.pem -pubout > appkey.pub
<?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 '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 = new OAuthConsumer( $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 app
	NULL,              // 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 = new MWOAuthSignatureMethod_RSA_SHA1( new OAuthDataStore(), $privateKey );
$init_req->sign_request(
	$rsa_method, // OAuthSignatureMethod
	$c,          // OAuthConsumer for your app
	NULL         // 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 params
curl_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 TOKEN
print "Enter the verification code:\n";
$fh = fopen( "php://stdin", "r" );
$line = fgets( $fh );
 
$rc = new OAuthToken( $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 = new OAuthToken( $acc->key, $acc->secret );
 
/**
 * Insecurely call the api for infomation 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 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 = 'http://<wiki>';
 
	// 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;
}

Golang demo cli client with HMAC[edit | edit source]

Before you begin:

$ go get github.com/mrjones/oauth
package main
 
import (
	"fmt"
	"os"
	"github.com/mrjones/oauth"
	"io/ioutil"
	"strconv"
)
 
func main() {
 
	var consumerKey string = ""
	var consumerSecret string = ""
 
	if len(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")
	if err != 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)
	if err != 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)
 
	if err != 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" )
 
}
  1. http://www.gossamer-threads.com/lists/wiki/wikitech/
  2. https://gerrit.wikimedia.org/r/#/c/93859/