*/ // Default uris, you can change them if you want to if (!defined('OAUTH_BASE_URI')) define('OAUTH_BASE_URI', '/'); if (!defined('OAUTH_INITIATE_URI')) define('OAUTH_INITIATE_URI', OAUTH_BASE_URI . 'initiate'); if (!defined('OAUTH_AUTHORIZE_URI')) define('OAUTH_AUTHORIZE_URI', OAUTH_BASE_URI . 'authorize'); if (!defined('OAUTH_TOKEN_URI')) define('OAUTH_TOKEN_URI', OAUTH_BASE_URI . 'token'); /** * Explode an http data string (like key1=value1&key2[]=value2 etc.) * * We need this function because parse_str will delete doublon keys. * This function makes it easier to find the sent string exactly as it was sent. * * @param string $string * @returns string */ function http_explode_data($string) { $string = explode('&', $string); $out = array(); $i = 0; foreach ($string as $line) { $parts = explode('=', $line); $key = oauth_loosy_decode($parts[0]); $value = isset($parts[1]) ? oauth_loosy_decode($parts[1]) : ''; $out[] = array($key, $value); } return $out; } /** * Returns a string encoded according to RFC 3986. * In PHP 5.3+ you'll just need to do rawurlencode. * * @param string $str * @returns string */ function rfc3986_encode($str) { return str_replace('%7E', '~', rawurlencode($str)); } /** * Returns a string decoded according OAuth spec. * * "While the encoding rules specified in this specification for the * purpose of constructing the signature base string exclude the of * a "+" character to represent an encoded space character, this pratice * is widely used in application/x-www-form-urlencoded encoded values, * and MUST be properly decoded" * * @param string $str * @returns string */ function oauth_loosy_decode($str) { $str = str_replace('+', '%20', $str); return rawurldecode($str); } if (!function_exists('hash_hmac')) { /** * hash_hmac equivalent when hash PECL extension is not present */ function hash_hmac($algo, $data, $key, $raw_output = false) { $blocksize = 64; if (strlen($key) > $blocksize) { $key = pack('H*', $algo($key)); } $key = str_pad($key, $blocksize, chr(0x00)); $ipad = str_repeat(chr(0x36), $blocksize); $opad = str_repeat(chr(0x5c), $blocksize); $hmac = pack( 'H*', $algo( ($key ^ $opad) . pack( 'H*', $algo( ($key ^ $ipad) . $base ) ) ) ); if ($raw_output) return $hmac; else return bin2hex($hmac); } } /** * @package OAuth_Provider */ class OAuth_Provider_Exception extends Exception { const ERROR = 1000; const MISSING_REQUIRED_PARAMETER = 1001; const INVALID_PARAMETER = 1002; const MUST_USE_HTTPS = 1003; const INVALID_NONCE = 1004; const INVALID_TIMESTAMP = 1005; const UNKNOWN_SIGNATURE_METHOD = 1006; const INVALID_TOKEN = 1007; const INVALID_SIGNATURE = 1008; const INVALID_CONSUMER_KEY = 1009; const INVALID_VERIFIER = 1010; /** * Exception constructor */ public function __construct($code, $msg) { parent::__construct($msg, $code); switch ($code) { case self::INVALID_PARAMETER: case self::MISSING_REQUIRED_PARAMETER: case self::UNKNOWN_SIGNATURE_METHOD: case self::MUST_USE_HTTPS: $this->returnError(400, $msg); break; case self::INVALID_TIMESTAMP: case self::INVALID_NONCE: case self::INVALID_SIGNATURE: case self::INVALID_TOKEN: case self::INVALID_CONSUMER_KEY: case self::INVALID_VERIFIER: $this->returnError(401, $msg); break; default; break; } } /** * Returns an HTTP error code */ private function returnError($code, $msg) { if ($code == 400) header('HTTP/1.1 400 Bad Request', true, 400); else header('HTTP/1.1 401 Unauthorized', true, 401); header('Content-Type: text/plain'); echo $msg; exit; } } /** * @package OAuth_Provider */ class OAuth_Provider { static private $callbacks = array( 'check_nonce' => null, 'get_token_secret' => null, 'get_temp_token_secret' => null, 'get_consumer_secret' => null, 'get_rsa_public_cert' => null, 'check_verifier' => null, 'get_request_headers' => 'apache_request_headers', ); const INITIATE = 1; const AUTHORIZE = 2; const TOKEN = 3; const ALLOWED = 4; const OUT_OF_BAND = 'oob'; const TIMEOUT_THRESHOLD = 300; // 5 minutes private $action = null; private $action_datas = array(); private $request_url = null; private $request_params = array(); private $request_method = null; /** * Sets a public callback (any valid PHP callback is accepted) */ static public function setCallback($name, $value) { if (!array_key_exists($name, self::$callbacks)) { throw new Exception("Invalid callback $name"); } self::$callbacks[$name] = $value; } /** * Checks that all required callbacks are registered */ private function checkInternals() { $check = array('get_consumer_secret', 'get_token_secret', 'get_temp_token_secret', 'check_verifier'); foreach ($check as $name) { if (!self::$callbacks[$name]) { throw new OAuth_Provider_Exception(OAuth_Provider_Exception::ERROR, "You MUST specify a $name callback before."); } } } /** * Sort method for REQUEST parameters */ private function sortRequestParams() { usort($this->request_params, array($this, 'sortRequestParamsCallback')); } /** * Callback used for sorting REQUEST parameters */ private function sortRequestParamsCallback($a, $b) { $c = strcmp((string)$a[0], (string)$b[0]); if ($c == 0) return $c; return ($c > 0) ? 1 : -1; } /** * Returns current OAuth action asked by consumer * * Could either be OAuth_Provider::INITIATE, AUTHORIZE, TOKEN or ALLOWED */ public function getAction() { return $this->action; } /** * Returns a OAuth action data (parameter) sent by the consumer */ public function getActionData($key) { if (array_key_exists($key, $this->action_datas)) return $this->action_datas[$key]; else return null; } /** * Returns all OAuth action datas (parameters) sent by the consumer */ public function getActionDatas() { return $this->action_datas; } /** * Sends back a HTTP answer to the consumer */ private function httpAnswer($params) { header('HTTP/1.1 200 OK', true, 200); header('Content-Type: application/x-www-form-urlencoded'); echo http_build_query($params, 0, '&'); exit; } /** * Answers the INITIATE action with temporary token and secret */ public function answerInitiate($token, $secret) { if ($this->action != self::INITIATE) { throw new OAuth_Provider_Exception(OAuth_Provider_Exception::ERROR, "Can't answer initiate when OAuth action is not initiate."); } $this->httpAnswer(array( 'oauth_token' => (string) $token, 'oauth_token_secret' => (string) $secret, 'oauth_callback_confirmed' => 'true', )); } /** * Answers the TOKEN action with permanent token and secret */ public function answerToken($token, $secret) { if ($this->action != self::TOKEN) { throw new OAuth_Provider_Exception(OAuth_Provider_Exception::ERROR, "Can't answer token when OAuth action is not token."); } $this->httpAnswer(array( 'oauth_token' => (string) $token, 'oauth_token_secret' => (string) $secret, )); } /** * Returns the AUTHORIZE callback url to send the user to */ public function getAuthorizeCallbackUrl($callback, $verifier, $additional_args = null) { if ($this->action != self::AUTHORIZE) { throw new OAuth_Provider_Exception(OAuth_Provider_Exception::ERROR, "Can't redirect authorize when OAuth action is not authorize."); } if ($callback == self::OUT_OF_BAND) { return false; } $url = parse_url($callback); if (!empty($url['query'])) $callback .= '&'; else $callback .= '?'; $callback .= 'oauth_token=' . rawurlencode($this->getActionData('token')); $callback .= '&oauth_verifier=' . rawurlencode($verifier); if (is_array($additional_args) && !empty($additional_args)) { foreach ($additional_args as $key=>$value) { $callback .= '&' . rawurlencode($key) . '=' . rawurlencode($value); } } return $callback; } /** * OAuth_Provider constructor * * @param string $request_url Request URL of the current page, if null will be auto-magically * discovered. */ public function __construct($request_url = null) { $this->checkInternals(); if (empty($request_url)) { $request_url = 'http' . (!empty($_SERVER['HTTPS']) ? 's' : '') . '://'; $request_url.= strtolower(!empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $_SERVER['SERVER_NAME']); $request_url.= !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME']; $request_url.= !empty($_SERVER['QUERY_STRING']) ? '?' . $_SERVER['QUERY_STRING'] : ''; } // Nothing to do if (empty($request_url)) { return $this; } $action = false; $datas = array(); $url = parse_url($request_url); // No way if (empty($url['scheme']) || empty($url['host'])) { return $this; } // What action is asked by consumer? if (!empty($url['path'])) { if ($url['path'] == OAUTH_INITIATE_URI) { $action = self::INITIATE; } elseif ($url['path'] == OAUTH_AUTHORIZE_URI) { $action = self::AUTHORIZE; } elseif ($url['path'] == OAUTH_TOKEN_URI) { $action = self::TOKEN; } else { $action = self::ALLOWED; } } else { return $this; } // URL normalization $this->request_url = $url['scheme'] . '://' . $url['host']; if (!empty($url['port'])) { if ($url['scheme'] == 'http' && $url['port'] != 80) $this->request_url .= ':' . $url['port']; elseif ($url['scheme'] == 'https' && $url['port'] != 443) $this->request_url .= ':' . $url['port']; } if (!empty($url['path'])) { $this->request_url .= $url['path']; } else { $this->request_url .= '/'; } // Recording request method for later use $this->request_method = empty($_SERVER['REQUEST_METHOD']) ? 'GET' : $_SERVER['REQUEST_METHOD']; // Appending _GET datas if any // We must don't use _GET array to keep original arguments, according to spec. if (!empty($_GET) && !empty($url['query'])) { $this->request_params = array_merge($this->request_params, http_explode_data($url['query'])); } // Catching received headers to fetch Authorization stuff $headers = call_user_func(self::$callbacks['get_request_headers']); if (!empty($headers['Authorization']) && substr($headers['Authorization'], 0, 5) == 'OAuth' && preg_match_all('!([a-z0-9A-Z_-]+)="(.*)"!U', $headers['Authorization'], $match, PREG_SET_ORDER)) { foreach ($match as $m) { if ($m[1] == 'realm') continue; $this->request_params[] = array( oauth_loosy_decode($m[1]), oauth_loosy_decode($m[2]) ); } } // Appending _POST datas // We must get POST datas through php://input trick because $_POST array have some treatments // eg. would be impossible to make distinction between // param[]=1¶m[]=2 and param[0]=1¶m[1]=2 if (!empty($_POST) && !empty($headers['Content-Type']) && $headers['Content-Type'] == 'application/x-www-form-urlencoded') { $post = file_get_contents("php://input"); $this->request_params = array_merge($this->request_params, http_explode_data($post)); } // Putting oauth params in $datas foreach ($this->request_params as $key=>$param) { if (substr($param[0], 0, 6) == 'oauth_') { $datas[substr($param[0], 6)] = $param[1]; } // The oauth_signature parameter MUST be excluded from the signature base string if ($param[0] == 'oauth_signature') { unset($this->request_params[$key]); } } $this->sortRequestParams(); // No OAuth datas, then no action asked if (empty($datas) && $action == self::ALLOWED) { return $this; } // Security and spec compliance check if ($this->checkAction($action, $datas)) { $this->action = $action; $this->action_datas = $datas; } } /** * Check action validity against what spec requires */ private function checkAction($action, $datas) { // Usual checks if ($action == self::INITIATE) { // REQUIRED if (empty($datas['callback'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_callback parameter."); } // Absolute URI / "oob" to indicate out of band configuration if ($datas['callback'] != self::OUT_OF_BAND && !filter_var($datas['callback'], FILTER_VALIDATE_URL, FILTER_FLAG_SCHEME_REQUIRED | FILTER_FLAG_HOST_REQUIRED)) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_PARAMETER, "oauth_callback should be a valid URI or 'oob' (case sensitive)."); } } elseif ($action == self::TOKEN) { if (empty($datas['verifier'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_verifier parameter."); } } elseif ($action == self::AUTHORIZE || $action == self::ALLOWED) { if (empty($datas['token'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_token parameter."); } } // Security checks if ($action != self::AUTHORIZE) { // SHOULD require HTTPS if (empty($_SERVER['HTTPS'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MUST_USE_HTTPS, "Client must use HTTPS to connect."); } // Authenticated request if (isset($datas['signature']) && isset($datas['consumer_key'])) { if (!$this->checkAuthentication($action, $datas)) return false; } } // Special checks if ($action == self::TOKEN) { if (!call_user_func(self::$callbacks['check_verifier'], $datas['verifier'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_VERIFIER, "Invalid oauth_verifier parameter."); } } return true; } /** * Checks if authentication is valid (when supplied) */ private function checkAuthentication($action, $datas) { // REQUIRED if (empty($datas['signature'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_signature parameter."); } // REQUIRED if (empty($datas['consumer_key'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_consumer_key parameter."); } // REQUIRED if (empty($datas['signature_method'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_signature_method parameter."); } // MUST be set to 1.0 if present if (!empty($datas['version']) && $datas['version'] != '1.0') { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_PARAMETER, "oauth_version must be set to 1.0."); } // For signed methods, require signing datas if ($datas['signature_method'] != 'PLAINTEXT') { if (empty($datas['timestamp'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_timestamp parameter."); } if (empty($datas['nonce'])) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::MISSING_REQUIRED_PARAMETER, "Missing required oauth_nonce parameter."); } $now = time(); $diff = ($now - $datas['timestamp']); // Server MAY reject requests with stale timestamps if ($diff > self::TIMEOUT_THRESHOLD) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_TIMESTAMP, "oauth_timestamp has expired (server timestamp is ".$now . ", client is ".$datas['timestamp'].", ".$diff." seconds threshod, " . self::TIMEOUT_THRESHOLD." max threshold accepted)."); } // server ensure that the combination of nonce/timestamp/token (if present) // received from the client has not been used before in a previous request if (self::$callbacks['check_nonce']) { $return = call_user_func(self::$callbacks['check_nonce'], $datas['nonce']); if (!$return) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_NONCE, "oauth_nonce supplied (".$datas['nonce'].") in request has already been used in a previous request."); } } } $datas['consumer_secret'] = call_user_func(self::$callbacks['get_consumer_secret'], $datas['consumer_key']); if (!$datas['consumer_secret']) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_CONSUMER_KEY, "Invalid supplied oauth_consumer_key."); } if (isset($datas['token']) && $action == self::ALLOWED) { $datas['token_secret'] = call_user_func(self::$callbacks['get_token_secret'], $datas['token']); if (!$datas['token_secret']) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_TOKEN, "Invalid supplied oauth_token."); } } elseif (isset($datas['token']) && $action == self::TOKEN) { $datas['token_secret'] = call_user_func(self::$callbacks['get_temp_token_secret'], $datas['token']); if (!$datas['token_secret']) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_TOKEN, "Invalid supplied oauth_token."); } } else { $datas['token_secret'] = false; } if (!$this->checkSignature($datas)) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_SIGNATURE, "Invalid supplied oauth_signature."); } return true; } /** * Build the signature base string * * @return string */ private function getSignatureBaseString() { $params = array(); foreach ($this->request_params as $key=>$param) { $params[] = rfc3986_encode($param[0]) . '=' . rfc3986_encode($param[1]); } $params = implode('&', $params); $str = array(); $str[] = rfc3986_encode($this->request_method); // HTTP method, uppercase $str[] = rfc3986_encode($this->request_url); // Base string URI $str[] = rfc3986_encode($params); // Request parameters return implode('&', $str); } /** * Checks request signature validity */ private function checkSignature($datas) { $method = $datas['signature_method']; $key = rfc3986_encode($datas['consumer_secret']) . '&' . rfc3986_encode($datas['token_secret']); switch ($method) { case 'PLAINTEXT': { return ($key == $datas['signature']); } case 'HMAC-SHA1': { $base = $this->getSignatureBaseString(); $signature = base64_encode(hash_hmac('sha1', $base, $key, true)); if ($signature != $datas['signature']) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_SIGNATURE, "Invalid supplied oauth_signature.\n" . "Base string was:\n" . $base ); } return true; } case 'RSA-SHA1': { if (!self::$callbacks['get_rsa_public_cert']) { throw new OAuth_Provider_Exception(OAuth_Provider_Exception::ERROR, "RSA-SHA1 is not available due to lack of get_rsa_public_cert callback."); } $signature = base64_decode($datas['signature']); $base = $this->getSignatureBaseString(); // Fetch the public key cert based on the request $cert = call_user_func(self::$callbacks['get_rsa_public_cert'], $datas); if (!$cert) { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::INVALID_SIGNATURE, "Can't find RSA certificate for this request"); } // Pull the public key ID from the certificate $publickeyid = openssl_get_publickey($cert); // Check the computed signature against the one passed in the query $ok = openssl_verify($base, $signature, $publickeyid); // Release the key resource openssl_free_key($publickeyid); return ($ok == 1); } default: { throw new OAuth_Provider_Exception( OAuth_Provider_Exception::UNKNOWN_SIGNATURE_METHOD, "Unknown signature method ".$method ); } } } } ?>