Skip to content

Instantly share code, notes, and snippets.

@collegeman
Created November 22, 2011 06:17
Show Gist options
  • Select an option

  • Save collegeman/1385036 to your computer and use it in GitHub Desktop.

Select an option

Save collegeman/1385036 to your computer and use it in GitHub Desktop.
Baseclass for writing WordPress plugins that need to implement an oAuth workflow
<?php
require('wordpress-oauth-support.php');
class YourPluginNamespace extends YourPluginNamespace_WordPressOAuthSupport {
function __construct() {
add_action('init', array($this, 'init'));
}
function init() {
// hook up the oAuth requests:
add_action('template_redirect', array($this, 'oauth_template_redirect'));
// then hook up your oAuth handler...
// the "api.twitter.com" part is a convention, set by
// the domain name that appears in the the service's
// request token URL (see example below)
add_action('oauth_request_api.twitter.com', array($this, 'twitter_oauth'));
}
function twitter_oauth() {
// establish the consumer keys
$consumer = array(
'key' => 'your-consumer-key',
'secret' => 'your-consumer-secret'
);
// if this request has GET params, treat it as an
// authorized redirect from the remote service
if ($_GET) {
// try to get an access token
$token = $this->oAuthAccessToken(
'https://api.twitter.com/oauth/access_token',
$consumer,
$_GET['oauth_token'],
$_GET['oauth_verifier']
);
// valid access token? success!
if ($token != false) {
update_option('twitter_access_token', $token);
wp_redirect('/');
// invalid access token? oops...
} else {
wp_die("Oops! Something went wrong. We can't verify your login for Twitter. Please try again.");
}
// no request parameters? redirect the user to
// the third-party login
} else {
$token = $this->oAuthRequestToken(
// this domain (api.twitter.com) is from which the WP action hook gets its name
'https://api.twitter.com/oauth/request_token',
$consumer
//, get_bloginfo('siteurl').sprintf('/oauth/%s/whatever', __CLASS__) // don't like the domain name convention? use this.
);
wp_redirect('https://api.twitter.com/oauth/authorize?oauth_token='.$token->key);
}
}
}
new YourPluginNamespace();
<?php
class YourPluginNamespace_WordPressOAuthSupport {
function oauth_template_redirect() {
if (preg_match( sprintf('#^/oauth/%s/([^/\?]+)#', get_class($this)), $_SERVER['REQUEST_URI'], $matches )) {
do_action('oauth_request', $matches[1]);
do_action('oauth_request_'.$matches[1]);
}
}
static function _sign_hmac_sha1($signature_base, $consumer, $token) {
$key_parts = array(
$consumer->secret,
($token) ? $token->secret : ''
);
$key_parts = self::_urlencode_rfc3986($key_parts);
$key = implode('&', $key_parts);
return base64_encode(hash_hmac('sha1', $signature_base, $key, true));
}
static function _generate_timestamp() {
return time();
}
static function _generate_nonce($seed = null) {
return wp_create_nonce( ((string) $seed) . 'oauth' );
}
static function _urlencode_rfc3986($input) {
if (is_array($input)) {
return array_map(array(__CLASS__, '_urlencode_rfc3986'), $input);
} else if (is_scalar($input)) {
return str_replace(
'+',
' ',
str_replace('%7E', '~', rawurlencode($input))
);
} else {
return '';
}
}
static function _urldecode_rfc3986($string) {
return urldecode($string);
}
static function _assertNotEmpty($var, $msg = null) {
if (empty($var) && $var !== 0 && $var !== '0') {
wp_die(!is_null($msg) ? $msg : 'A required argument was empty.');
}
}
static function _get_signable_params($params) {
$P = array_merge($params, array()); // inliue of an array copy function
// Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
if (isset($P['oauth_signature'])) {
unset($P['oauth_signature']);
}
return self::_build_http_query($P);
}
static function _get_normalized_http_url($url) {
$parts = parse_url($url);
$port = @$parts['port'];
$scheme = $parts['scheme'];
$host = $parts['host'];
$path = @$parts['path'];
$port or $port = ($scheme == 'https') ? '443' : '80';
if (($scheme == 'https' && $port != '443')
|| ($scheme == 'http' && $port != '80')) {
$host = "$host:$port";
}
return "$scheme://$host$path";
}
static function _parse_parameters( $input ) {
if (!isset($input) || !$input) return array();
$pairs = explode('&', $input);
$parsed_parameters = array();
foreach ($pairs as $pair) {
$split = explode('=', $pair, 2);
$parameter = self::_urldecode_rfc3986($split[0]);
$value = isset($split[1]) ? self::_urldecode_rfc3986($split[1]) : '';
if (isset($parsed_parameters[$parameter])) {
// We have already recieved parameter(s) with this name, so add to the list
// of parameters with this name
if (is_scalar($parsed_parameters[$parameter])) {
// This is the first duplicate, so transform scalar (string) into an array
// so we can add the duplicates
$parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
}
$parsed_parameters[$parameter][] = $value;
} else {
$parsed_parameters[$parameter] = $value;
}
}
return $parsed_parameters;
}
static function _build_http_query($params = '') {
if (!$params) {
return '';
}
// Urlencode both keys and values
$keys = self::_urlencode_rfc3986(array_keys($params));
$values = self::_urlencode_rfc3986(array_values($params));
$params = array_combine($keys, $values);
// Parameters are sorted by name, using lexicographical byte value ordering.
// Ref: Spec: 9.1.1 (1)
uksort($params, 'strcmp');
$pairs = array();
foreach ($params as $parameter => $value) {
if (is_array($value)) {
// If two or more parameters share the same name, they are sorted by their value
// Ref: Spec: 9.1.1 (1)
natsort($value);
foreach ($value as $duplicate_value) {
$pairs[] = $parameter . '=' . $duplicate_value;
}
} else {
$pairs[] = $parameter . '=' . $value;
}
}
// For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
// Each name-value pair is separated by an '&' character (ASCII code 38)
return implode('&', $pairs);
}
function _get_callback_url($request_url, $callback_url, $consumer_key, $consumer_secret, $params) {
if (is_null($callback_url)) {
$callback_url = get_bloginfo('siteurl').'/oauth/'.get_class($this).'/'.parse_url($request_url, PHP_URL_HOST);
}
return apply_filters('oauth_callback_url', $callback_url, $request_url, $consumer_key, $consumer_secret, $params);
}
function oAuthGet($url, $consumer, $token = null, $params = null) {
return self::oAuthRequest($url, 'GET', $consumer, $token, $params);
}
function oAuthPost($url, $consumer, $token = null, $params = null) {
return self::oAuthRequest($url, 'POST', $consumer, $token, $params);
}
function oAuthRequestToken($request_token_url, $consumer, $callback_url = null, $params = array()) {
$consumer = (object) $consumer;
$callback_url = $this->_get_callback_url($request_token_url, $callback_url, $consumer->key, $consumer->secret, $params);
$params['oauth_callback'] = $callback_url;
$response = $this->oAuthGet(
$request_token_url,
$consumer,
null,
$params
);
if (is_wp_error($response)) {
do_action('oauth_request_token_error', $request_token_url, $consumer_key, $consumer_secret, $callback_url, $params);
return false;
} else {
$token = wp_parse_args($response['body']);
return (object) array(
'key' => $token['oauth_token'],
'secret' => $token['oauth_token_secret'],
'callback_url' => $callback_url
);
}
}
function oAuthAccessToken($access_token_url, $consumer, $token, $verifier, $params = array()) {
$consumer = (object) $consumer;
$params['oauth_token'] = $token;
$params['oauth_verifier'] = $verifier;
$response = $this->oAuthPost(
$access_token_url,
$consumer,
null,
$params
);
if (is_wp_error($response)) {
do_action('oauth_access_token_error', $access_token_url, $consumer->key, $consumer->secret, $callback_url, $params);
return false;
} else {
$token = wp_parse_args($response['body']);
return (object) array(
'key' => $token['oauth_token'],
'secret' => $token['oauth_token_secret'],
'response' => $token
);
}
$request = self::_get_oauth_request(
$access_token_url,
'GET',
$consumer,
$token,
$params
);
return $request->url.'?'.$request->query_string;
}
private static function _get_oauth_request($url, $method, $consumer, $token, $params = array()) {
$args = func_get_args();
if (count($args) < 5) {
$token = isset($params['token']) ? $params['token'] : null;
unset($params['token']);
}
$consumer = (object) $consumer;
$token = (object) $token;
$defaults = array(
'oauth_version' => '1.0',
'oauth_nonce' => self::_generate_nonce(),
'oauth_timestamp' => self::_generate_timestamp(),
'oauth_consumer_key' => $consumer->key
);
$params = array_merge( $defaults, $params );
$params = array_merge( self::_parse_parameters( parse_url($url, PHP_URL_QUERY) ), $params );
$params['oauth_signature_method'] = 'HMAC-SHA1';
$signature_base = implode('&', self::_urlencode_rfc3986(array(
strtoupper($method),
$normalized_url = self::_get_normalized_http_url($url),
$signed_params = self::_get_signable_params($params)
)));
$signature = self::_sign_hmac_sha1($signature_base, $consumer, $token);
$params['oauth_signature'] = $signature;
return (object) array(
'url' => $normalized_url,
'query_string' => self::_build_http_query($params),
'params' => $params
);
}
static function oAuthRequest($url, $method, $consumer, $token, $params = null) {
$request = self::_get_oauth_request($url, $method, $consumer, $token, $params);
$http = _wp_http_get_object();
$url = ($method == 'GET') ? $request->url.'?'.$request->query_string : $request->url;
return $http->request($url, array(
'method' => $method,
'body' => ($method == 'POST') ? $request->query_string : null
));
}
}
@davekiss
Copy link

davekiss commented May 4, 2012

Isn't it a bad idea to include your consumer secret in the viewable code?

@collegeman
Copy link
Author

Sure is - don't distribute code that has your personal keys. This is just an illustration. your-consumer-key would need to be stored in a database, e.g., via WordPress options, and the value would need to be provided by the user.

@davekiss
Copy link

davekiss commented May 4, 2012

So, how do you suggest the user connect to an OAuth app without having to go through the extensive process of registering their own app and entering their consumer key/secret?

@collegeman
Copy link
Author

People do it. They don't all do it, obviously, but it's a functional way to operate. The alternative is something I'm working on now - basically creating a proxy whereby the license key I issue is your consumer key, and your WP generates the consumer secret, then you authenticate on my server to access Facebook (my consumer key and secret remain hidden on my server), and I issue you an access token to access my proxy.

@davekiss
Copy link

davekiss commented May 4, 2012

Can you elaborate on your alternative method? Sounds intriguing, and something I'd implement/help with, if you're interested.

@collegeman
Copy link
Author

Sure. Can you tell me a bit about yourself though first? You don't have much in the way of github projects. What sorts of things do you enjoy programming?

@davekiss
Copy link

davekiss commented May 4, 2012

Sure thing. Can you drop me a note at hello at davekiss dot com ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment