├── README
├── example
└── index.php
├── twitterOAuth.php
└── OAuth.php
/README:
--------------------------------------------------------------------------------
1 | Abraham Williams | abraham@abrah.am | http://abrah.am | @abraham
2 |
3 | Basic example of working with Twitter's OAuth beta.
4 |
5 | Docs: https://docs.google.com/View?docID=dcf2dzzs_2339fzbfsf4
6 |
--------------------------------------------------------------------------------
/example/index.php:
--------------------------------------------------------------------------------
1 | getRequestToken();
45 |
46 | /* Save tokens for later */
47 | $_SESSION['oauth_request_token'] = $token = $tok['oauth_token'];
48 | $_SESSION['oauth_request_token_secret'] = $tok['oauth_token_secret'];
49 | $_SESSION['oauth_state'] = "start";
50 |
51 | /* Build the authorization URL */
52 | $request_link = $to->getAuthorizeURL($token);
53 |
54 | /* Build link that gets user to twitter to authorize the app */
55 | $content = 'Click on the link to go to twitter to authorize your account.';
56 | $content .= '
'.$request_link.'';
57 | break;
58 | case 'returned':
59 | /* If the access tokens are already set skip to the API call */
60 | if ($_SESSION['oauth_access_token'] === NULL && $_SESSION['oauth_access_token_secret'] === NULL) {
61 | /* Create TwitterOAuth object with app key/secret and token key/secret from default phase */
62 | $to = new TwitterOAuth($consumer_key, $consumer_secret, $_SESSION['oauth_request_token'], $_SESSION['oauth_request_token_secret']);
63 | /* Request access tokens from twitter */
64 | $tok = $to->getAccessToken();
65 |
66 | /* Save the access tokens. Normally these would be saved in a database for future use. */
67 | $_SESSION['oauth_access_token'] = $tok['oauth_token'];
68 | $_SESSION['oauth_access_token_secret'] = $tok['oauth_token_secret'];
69 | }
70 | /* Random copy */
71 | $content = 'your account should now be registered with twitter. Check here:
';
72 | $content .= 'https://twitter.com/account/connections';
73 |
74 | /* Create TwitterOAuth with app key/secret and user access key/secret */
75 | $to = new TwitterOAuth($consumer_key, $consumer_secret, $_SESSION['oauth_access_token'], $_SESSION['oauth_access_token_secret']);
76 | /* Run request on twitter API as user. */
77 | $content = $to->OAuthRequest('https://twitter.com/account/verify_credentials.xml', array(), 'GET');
78 | //$content = $to->OAuthRequest('https://twitter.com/statuses/update.xml', array('status' => 'Test OAuth update. #testoauth'), 'POST');
79 | //$content = $to->OAuthRequest('https://twitter.com/statuses/replies.xml', array(), 'POST');
80 | break;
81 | }/*}}}*/
82 | ?>
83 |
84 |
85 |
This site is a basic showcase of Twitters new OAuth authentication method. Everything is saved in sessions. If you want to start over ?test=clear'>clear sessions.
91 | 92 |
93 | Get the code powering this at http://github.com/abraham/twitteroauth
94 |
95 | Read the documentation at https://docs.google.com/View?docID=dcf2dzzs_2339fzbfsf4
96 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/twitterOAuth.php:
--------------------------------------------------------------------------------
1 | http_status; }
40 | function lastAPICall() { return $this->last_api_call; }
41 |
42 | /**
43 | * construct TwitterOAuth object
44 | */
45 | function __construct($consumer_key, $consumer_secret, $oauth_token = NULL, $oauth_token_secret = NULL) {/*{{{*/
46 | $this->sha1_method = new OAuthSignatureMethod_HMAC_SHA1();
47 | $this->consumer = new OAuthConsumer($consumer_key, $consumer_secret);
48 | if (!empty($oauth_token) && !empty($oauth_token_secret)) {
49 | $this->token = new OAuthConsumer($oauth_token, $oauth_token_secret);
50 | } else {
51 | $this->token = NULL;
52 | }
53 | }/*}}}*/
54 |
55 |
56 | /**
57 | * Get a request_token from Twitter
58 | *
59 | * @returns a key/value array containing oauth_token and oauth_token_secret
60 | */
61 | function getRequestToken() {/*{{{*/
62 | $r = $this->oAuthRequest($this->requestTokenURL());
63 | $token = $this->oAuthParseResponse($r);
64 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
65 | return $token;
66 | }/*}}}*/
67 |
68 | /**
69 | * Parse a URL-encoded OAuth response
70 | *
71 | * @return a key/value array
72 | */
73 | function oAuthParseResponse($responseString) {
74 | $r = array();
75 | foreach (explode('&', $responseString) as $param) {
76 | $pair = explode('=', $param, 2);
77 | if (count($pair) != 2) continue;
78 | $r[urldecode($pair[0])] = urldecode($pair[1]);
79 | }
80 | return $r;
81 | }
82 |
83 | /**
84 | * Get the authorize URL
85 | *
86 | * @returns a string
87 | */
88 | function getAuthorizeURL($token) {/*{{{*/
89 | if (is_array($token)) $token = $token['oauth_token'];
90 | return $this->authorizeURL() . '?oauth_token=' . $token;
91 | }/*}}}*/
92 |
93 | /**
94 | * Exchange the request token and secret for an access token and
95 | * secret, to sign API calls.
96 | *
97 | * @returns array("oauth_token" => the access token,
98 | * "oauth_token_secret" => the access secret)
99 | */
100 | function getAccessToken($token = NULL) {/*{{{*/
101 | $r = $this->oAuthRequest($this->accessTokenURL());
102 | $token = $this->oAuthParseResponse($r);
103 | $this->token = new OAuthConsumer($token['oauth_token'], $token['oauth_token_secret']);
104 | return $token;
105 | }/*}}}*/
106 |
107 | /**
108 | * Format and sign an OAuth / API request
109 | */
110 | function oAuthRequest($url, $args = array(), $method = NULL) {/*{{{*/
111 | if (empty($method)) $method = empty($args) ? "GET" : "POST";
112 | $req = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, $method, $url, $args);
113 | $req->sign_request($this->sha1_method, $this->consumer, $this->token);
114 | switch ($method) {
115 | case 'GET': return $this->http($req->to_url());
116 | case 'POST': return $this->http($req->get_normalized_http_url(), $req->to_postdata());
117 | }
118 | }/*}}}*/
119 |
120 | /**
121 | * Make an HTTP request
122 | *
123 | * @return API results
124 | */
125 | function http($url, $post_data = null) {/*{{{*/
126 | $ch = curl_init();
127 | if (defined("CURL_CA_BUNDLE_PATH")) curl_setopt($ch, CURLOPT_CAINFO, CURL_CA_BUNDLE_PATH);
128 | curl_setopt($ch, CURLOPT_URL, $url);
129 | curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
130 | curl_setopt($ch, CURLOPT_TIMEOUT, 30);
131 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
132 | //////////////////////////////////////////////////
133 | ///// Set to 1 to verify Twitter's SSL Cert //////
134 | //////////////////////////////////////////////////
135 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
136 | if (isset($post_data)) {
137 | curl_setopt($ch, CURLOPT_POST, 1);
138 | curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
139 | }
140 | $response = curl_exec($ch);
141 | $this->http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
142 | $this->last_api_call = $url;
143 | curl_close ($ch);
144 | return $response;
145 | }/*}}}*/
146 | }/*}}}*/
--------------------------------------------------------------------------------
/OAuth.php:
--------------------------------------------------------------------------------
1 | key = $key;
16 | $this->secret = $secret;
17 | $this->callback_url = $callback_url;
18 | }/*}}}*/
19 |
20 | function __toString() {/*{{{*/
21 | return "OAuthConsumer[key=$this->key,secret=$this->secret]";
22 | }/*}}}*/
23 | }/*}}}*/
24 |
25 | class OAuthToken {/*{{{*/
26 | // access tokens and request tokens
27 | public $key;
28 | public $secret;
29 |
30 | /**
31 | * key = the token
32 | * secret = the token secret
33 | */
34 | function __construct($key, $secret) {/*{{{*/
35 | $this->key = $key;
36 | $this->secret = $secret;
37 | }/*}}}*/
38 |
39 | /**
40 | * generates the basic string serialization of a token that a server
41 | * would respond to request_token and access_token calls with
42 | */
43 | function to_string() {/*{{{*/
44 | return "oauth_token=" . OAuthUtil::urlencode_rfc3986($this->key) .
45 | "&oauth_token_secret=" . OAuthUtil::urlencode_rfc3986($this->secret);
46 | }/*}}}*/
47 |
48 | function __toString() {/*{{{*/
49 | return $this->to_string();
50 | }/*}}}*/
51 | }/*}}}*/
52 |
53 | class OAuthSignatureMethod {/*{{{*/
54 | public function check_signature(&$request, $consumer, $token, $signature) {
55 | $built = $this->build_signature($request, $consumer, $token);
56 | return $built == $signature;
57 | }
58 | }/*}}}*/
59 |
60 | class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/
61 | function get_name() {/*{{{*/
62 | return "HMAC-SHA1";
63 | }/*}}}*/
64 |
65 | public function build_signature($request, $consumer, $token) {/*{{{*/
66 | $base_string = $request->get_signature_base_string();
67 | $request->base_string = $base_string;
68 |
69 | $key_parts = array(
70 | $consumer->secret,
71 | ($token) ? $token->secret : ""
72 | );
73 |
74 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
75 | $key = implode('&', $key_parts);
76 |
77 | return base64_encode( hash_hmac('sha1', $base_string, $key, true));
78 | }/*}}}*/
79 | }/*}}}*/
80 |
81 | class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/
82 | public function get_name() {/*{{{*/
83 | return "PLAINTEXT";
84 | }/*}}}*/
85 |
86 | public function build_signature($request, $consumer, $token) {/*{{{*/
87 | $sig = array(
88 | OAuthUtil::urlencode_rfc3986($consumer->secret)
89 | );
90 |
91 | if ($token) {
92 | array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret));
93 | } else {
94 | array_push($sig, '');
95 | }
96 |
97 | $raw = implode("&", $sig);
98 | // for debug purposes
99 | $request->base_string = $raw;
100 |
101 | return OAuthUtil::urlencode_rfc3986($raw);
102 | }/*}}}*/
103 | }/*}}}*/
104 |
105 | class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/
106 | public function get_name() {/*{{{*/
107 | return "RSA-SHA1";
108 | }/*}}}*/
109 |
110 | protected function fetch_public_cert(&$request) {/*{{{*/
111 | // not implemented yet, ideas are:
112 | // (1) do a lookup in a table of trusted certs keyed off of consumer
113 | // (2) fetch via http using a url provided by the requester
114 | // (3) some sort of specific discovery code based on request
115 | //
116 | // either way should return a string representation of the certificate
117 | throw Exception("fetch_public_cert not implemented");
118 | }/*}}}*/
119 |
120 | protected function fetch_private_cert(&$request) {/*{{{*/
121 | // not implemented yet, ideas are:
122 | // (1) do a lookup in a table of trusted certs keyed off of consumer
123 | //
124 | // either way should return a string representation of the certificate
125 | throw Exception("fetch_private_cert not implemented");
126 | }/*}}}*/
127 |
128 | public function build_signature(&$request, $consumer, $token) {/*{{{*/
129 | $base_string = $request->get_signature_base_string();
130 | $request->base_string = $base_string;
131 |
132 | // Fetch the private key cert based on the request
133 | $cert = $this->fetch_private_cert($request);
134 |
135 | // Pull the private key ID from the certificate
136 | $privatekeyid = openssl_get_privatekey($cert);
137 |
138 | // Sign using the key
139 | $ok = openssl_sign($base_string, $signature, $privatekeyid);
140 |
141 | // Release the key resource
142 | openssl_free_key($privatekeyid);
143 |
144 | return base64_encode($signature);
145 | } /*}}}*/
146 |
147 | public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/
148 | $decoded_sig = base64_decode($signature);
149 |
150 | $base_string = $request->get_signature_base_string();
151 |
152 | // Fetch the public key cert based on the request
153 | $cert = $this->fetch_public_cert($request);
154 |
155 | // Pull the public key ID from the certificate
156 | $publickeyid = openssl_get_publickey($cert);
157 |
158 | // Check the computed signature against the one passed in the query
159 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
160 |
161 | // Release the key resource
162 | openssl_free_key($publickeyid);
163 |
164 | return $ok == 1;
165 | } /*}}}*/
166 | }/*}}}*/
167 |
168 | class OAuthRequest {/*{{{*/
169 | private $parameters;
170 | private $http_method;
171 | private $http_url;
172 | // for debug purposes
173 | public $base_string;
174 | public static $version = '1.0';
175 |
176 | function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/
177 | @$parameters or $parameters = array();
178 | $this->parameters = $parameters;
179 | $this->http_method = $http_method;
180 | $this->http_url = $http_url;
181 | }/*}}}*/
182 |
183 |
184 | /**
185 | * attempt to build up a request from what was passed to the server
186 | */
187 | public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/
188 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
189 | @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI'];
190 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
191 |
192 | $request_headers = OAuthRequest::get_headers();
193 |
194 | // let the library user override things however they'd like, if they know
195 | // which parameters to use then go for it, for example XMLRPC might want to
196 | // do this
197 | if ($parameters) {
198 | $req = new OAuthRequest($http_method, $http_url, $parameters);
199 | } else {
200 | // collect request parameters from query string (GET) and post-data (POST) if appropriate (note: POST vars have priority)
201 | $req_parameters = $_GET;
202 | if ($http_method == "POST" && @strstr($request_headers["Content-Type"], "application/x-www-form-urlencoded") ) {
203 | $req_parameters = array_merge($req_parameters, $_POST);
204 | }
205 |
206 | // next check for the auth header, we need to do some extra stuff
207 | // if that is the case, namely suck in the parameters from GET or POST
208 | // so that we can include them in the signature
209 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
210 | $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
211 | $parameters = array_merge($req_parameters, $header_parameters);
212 | $req = new OAuthRequest($http_method, $http_url, $parameters);
213 | } else $req = new OAuthRequest($http_method, $http_url, $req_parameters);
214 | }
215 |
216 | return $req;
217 | }/*}}}*/
218 |
219 | /**
220 | * pretty much a helper function to set up the request
221 | */
222 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {/*{{{*/
223 | @$parameters or $parameters = array();
224 | $defaults = array("oauth_version" => OAuthRequest::$version,
225 | "oauth_nonce" => OAuthRequest::generate_nonce(),
226 | "oauth_timestamp" => OAuthRequest::generate_timestamp(),
227 | "oauth_consumer_key" => $consumer->key);
228 | $parameters = array_merge($defaults, $parameters);
229 |
230 | if ($token) {
231 | $parameters['oauth_token'] = $token->key;
232 | }
233 | return new OAuthRequest($http_method, $http_url, $parameters);
234 | }/*}}}*/
235 |
236 | public function set_parameter($name, $value) {/*{{{*/
237 | $this->parameters[$name] = $value;
238 | }/*}}}*/
239 |
240 | public function get_parameter($name) {/*{{{*/
241 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
242 | }/*}}}*/
243 |
244 | public function get_parameters() {/*{{{*/
245 | return $this->parameters;
246 | }/*}}}*/
247 |
248 | /**
249 | * Returns the normalized parameters of the request
250 | *
251 | * This will be all (except oauth_signature) parameters,
252 | * sorted first by key, and if duplicate keys, then by
253 | * value.
254 | *
255 | * The returned string will be all the key=value pairs
256 | * concated by &.
257 | *
258 | * @return string
259 | */
260 | public function get_signable_parameters() {/*{{{*/
261 | // Grab all parameters
262 | $params = $this->parameters;
263 |
264 | // Remove oauth_signature if present
265 | if (isset($params['oauth_signature'])) {
266 | unset($params['oauth_signature']);
267 | }
268 |
269 | // Urlencode both keys and values
270 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
271 | $values = OAuthUtil::urlencode_rfc3986(array_values($params));
272 | $params = array_combine($keys, $values);
273 |
274 | // Sort by keys (natsort)
275 | uksort($params, 'strcmp');
276 |
277 | // Generate key=value pairs
278 | $pairs = array();
279 | foreach ($params as $key=>$value ) {
280 | if (is_array($value)) {
281 | // If the value is an array, it's because there are multiple
282 | // with the same key, sort them, then add all the pairs
283 | natsort($value);
284 | foreach ($value as $v2) {
285 | $pairs[] = $key . '=' . $v2;
286 | }
287 | } else {
288 | $pairs[] = $key . '=' . $value;
289 | }
290 | }
291 |
292 | // Return the pairs, concated with &
293 | return implode('&', $pairs);
294 | }/*}}}*/
295 |
296 | /**
297 | * Returns the base string of this request
298 | *
299 | * The base string defined as the method, the url
300 | * and the parameters (normalized), each urlencoded
301 | * and the concated with &.
302 | */
303 | public function get_signature_base_string() {/*{{{*/
304 | $parts = array(
305 | $this->get_normalized_http_method(),
306 | $this->get_normalized_http_url(),
307 | $this->get_signable_parameters()
308 | );
309 |
310 | $parts = OAuthUtil::urlencode_rfc3986($parts);
311 |
312 | return implode('&', $parts);
313 | }/*}}}*/
314 |
315 | /**
316 | * just uppercases the http method
317 | */
318 | public function get_normalized_http_method() {/*{{{*/
319 | return strtoupper($this->http_method);
320 | }/*}}}*/
321 |
322 | /**
323 | * parses the url and rebuilds it to be
324 | * scheme://host/path
325 | */
326 | public function get_normalized_http_url() {/*{{{*/
327 | $parts = parse_url($this->http_url);
328 |
329 | $port = @$parts['port'];
330 | $scheme = $parts['scheme'];
331 | $host = $parts['host'];
332 | $path = @$parts['path'];
333 |
334 | $port or $port = ($scheme == 'https') ? '443' : '80';
335 |
336 | if (($scheme == 'https' && $port != '443')
337 | || ($scheme == 'http' && $port != '80')) {
338 | $host = "$host:$port";
339 | }
340 | return "$scheme://$host$path";
341 | }/*}}}*/
342 |
343 | /**
344 | * builds a url usable for a GET request
345 | */
346 | public function to_url() {/*{{{*/
347 | $out = $this->get_normalized_http_url() . "?";
348 | $out .= $this->to_postdata();
349 | return $out;
350 | }/*}}}*/
351 |
352 | /**
353 | * builds the data one would send in a POST request
354 | *
355 | * TODO(morten.fangel):
356 | * this function might be easily replaced with http_build_query()
357 | * and corrections for rfc3986 compatibility.. but not sure
358 | */
359 | public function to_postdata() {/*{{{*/
360 | $total = array();
361 | foreach ($this->parameters as $k => $v) {
362 | if (is_array($v)) {
363 | foreach ($v as $va) {
364 | $total[] = OAuthUtil::urlencode_rfc3986($k) . "[]=" . OAuthUtil::urlencode_rfc3986($va);
365 | }
366 | } else {
367 | $total[] = OAuthUtil::urlencode_rfc3986($k) . "=" . OAuthUtil::urlencode_rfc3986($v);
368 | }
369 | }
370 | $out = implode("&", $total);
371 | return $out;
372 | }/*}}}*/
373 |
374 | /**
375 | * builds the Authorization: header
376 | */
377 | public function to_header() {/*{{{*/
378 | $out ='Authorization: OAuth realm=""';
379 | $total = array();
380 | foreach ($this->parameters as $k => $v) {
381 | if (substr($k, 0, 5) != "oauth") continue;
382 | if (is_array($v)) throw new OAuthException('Arrays not supported in headers');
383 | $out .= ',' . OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"';
384 | }
385 | return $out;
386 | }/*}}}*/
387 |
388 | public function __toString() {/*{{{*/
389 | return $this->to_url();
390 | }/*}}}*/
391 |
392 |
393 | public function sign_request($signature_method, $consumer, $token) {/*{{{*/
394 | $this->set_parameter("oauth_signature_method", $signature_method->get_name());
395 | $signature = $this->build_signature($signature_method, $consumer, $token);
396 | $this->set_parameter("oauth_signature", $signature);
397 | }/*}}}*/
398 |
399 | public function build_signature($signature_method, $consumer, $token) {/*{{{*/
400 | $signature = $signature_method->build_signature($this, $consumer, $token);
401 | return $signature;
402 | }/*}}}*/
403 |
404 | /**
405 | * util function: current timestamp
406 | */
407 | private static function generate_timestamp() {/*{{{*/
408 | return time();
409 | }/*}}}*/
410 |
411 | /**
412 | * util function: current nonce
413 | */
414 | private static function generate_nonce() {/*{{{*/
415 | $mt = microtime();
416 | $rand = mt_rand();
417 |
418 | return md5($mt . $rand); // md5s look nicer than numbers
419 | }/*}}}*/
420 |
421 | /**
422 | * util function for turning the Authorization: header into
423 | * parameters, has to do some unescaping
424 | */
425 | private static function split_header($header) {/*{{{*/
426 | $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
427 | $offset = 0;
428 | $params = array();
429 | while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
430 | $match = $matches[0];
431 | $header_name = $matches[2][0];
432 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
433 | $params[$header_name] = OAuthUtil::urldecode_rfc3986( $header_content );
434 | $offset = $match[1] + strlen($match[0]);
435 | }
436 |
437 | if (isset($params['realm'])) {
438 | unset($params['realm']);
439 | }
440 |
441 | return $params;
442 | }/*}}}*/
443 |
444 | /**
445 | * helper to try to sort out headers for people who aren't running apache
446 | */
447 | private static function get_headers() {/*{{{*/
448 | if (function_exists('apache_request_headers')) {
449 | // we need this to get the actual Authorization: header
450 | // because apache tends to tell us it doesn't exist
451 | return apache_request_headers();
452 | }
453 | // otherwise we don't have apache and are just going to have to hope
454 | // that $_SERVER actually contains what we need
455 | $out = array();
456 | foreach ($_SERVER as $key => $value) {
457 | if (substr($key, 0, 5) == "HTTP_") {
458 | // this is chaos, basically it is just there to capitalize the first
459 | // letter of every word that is not an initial HTTP and strip HTTP
460 | // code from przemek
461 | $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
462 | $out[$key] = $value;
463 | }
464 | }
465 | return $out;
466 | }/*}}}*/
467 | }/*}}}*/
468 |
469 | class OAuthServer {/*{{{*/
470 | protected $timestamp_threshold = 300; // in seconds, five minutes
471 | protected $version = 1.0; // hi blaine
472 | protected $signature_methods = array();
473 |
474 | protected $data_store;
475 |
476 | function __construct($data_store) {/*{{{*/
477 | $this->data_store = $data_store;
478 | }/*}}}*/
479 |
480 | public function add_signature_method($signature_method) {/*{{{*/
481 | $this->signature_methods[$signature_method->get_name()] =
482 | $signature_method;
483 | }/*}}}*/
484 |
485 | // high level functions
486 |
487 | /**
488 | * process a request_token request
489 | * returns the request token on success
490 | */
491 | public function fetch_request_token(&$request) {/*{{{*/
492 | $this->get_version($request);
493 |
494 | $consumer = $this->get_consumer($request);
495 |
496 | // no token required for the initial token request
497 | $token = NULL;
498 |
499 | $this->check_signature($request, $consumer, $token);
500 |
501 | $new_token = $this->data_store->new_request_token($consumer);
502 |
503 | return $new_token;
504 | }/*}}}*/
505 |
506 | /**
507 | * process an access_token request
508 | * returns the access token on success
509 | */
510 | public function fetch_access_token(&$request) {/*{{{*/
511 | $this->get_version($request);
512 |
513 | $consumer = $this->get_consumer($request);
514 |
515 | // requires authorized request token
516 | $token = $this->get_token($request, $consumer, "request");
517 |
518 |
519 | $this->check_signature($request, $consumer, $token);
520 |
521 | $new_token = $this->data_store->new_access_token($token, $consumer);
522 |
523 | return $new_token;
524 | }/*}}}*/
525 |
526 | /**
527 | * verify an api call, checks all the parameters
528 | */
529 | public function verify_request(&$request) {/*{{{*/
530 | $this->get_version($request);
531 | $consumer = $this->get_consumer($request);
532 | $token = $this->get_token($request, $consumer, "access");
533 | $this->check_signature($request, $consumer, $token);
534 | return array($consumer, $token);
535 | }/*}}}*/
536 |
537 | // Internals from here
538 | /**
539 | * version 1
540 | */
541 | private function get_version(&$request) {/*{{{*/
542 | $version = $request->get_parameter("oauth_version");
543 | if (!$version) {
544 | $version = 1.0;
545 | }
546 | if ($version && $version != $this->version) {
547 | throw new OAuthException("OAuth version '$version' not supported");
548 | }
549 | return $version;
550 | }/*}}}*/
551 |
552 | /**
553 | * figure out the signature with some defaults
554 | */
555 | private function get_signature_method(&$request) {/*{{{*/
556 | $signature_method =
557 | @$request->get_parameter("oauth_signature_method");
558 | if (!$signature_method) {
559 | $signature_method = "PLAINTEXT";
560 | }
561 | if (!in_array($signature_method,
562 | array_keys($this->signature_methods))) {
563 | throw new OAuthException(
564 | "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods))
565 | );
566 | }
567 | return $this->signature_methods[$signature_method];
568 | }/*}}}*/
569 |
570 | /**
571 | * try to find the consumer for the provided request's consumer key
572 | */
573 | private function get_consumer(&$request) {/*{{{*/
574 | $consumer_key = @$request->get_parameter("oauth_consumer_key");
575 | if (!$consumer_key) {
576 | throw new OAuthException("Invalid consumer key");
577 | }
578 |
579 | $consumer = $this->data_store->lookup_consumer($consumer_key);
580 | if (!$consumer) {
581 | throw new OAuthException("Invalid consumer");
582 | }
583 |
584 | return $consumer;
585 | }/*}}}*/
586 |
587 | /**
588 | * try to find the token for the provided request's token key
589 | */
590 | private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/
591 | $token_field = @$request->get_parameter('oauth_token');
592 | $token = $this->data_store->lookup_token(
593 | $consumer, $token_type, $token_field
594 | );
595 | if (!$token) {
596 | throw new OAuthException("Invalid $token_type token: $token_field");
597 | }
598 | return $token;
599 | }/*}}}*/
600 |
601 | /**
602 | * all-in-one function to check the signature on a request
603 | * should guess the signature method appropriately
604 | */
605 | private function check_signature(&$request, $consumer, $token) {/*{{{*/
606 | // this should probably be in a different method
607 | $timestamp = @$request->get_parameter('oauth_timestamp');
608 | $nonce = @$request->get_parameter('oauth_nonce');
609 |
610 | $this->check_timestamp($timestamp);
611 | $this->check_nonce($consumer, $token, $nonce, $timestamp);
612 |
613 | $signature_method = $this->get_signature_method($request);
614 |
615 | $signature = $request->get_parameter('oauth_signature');
616 | $valid_sig = $signature_method->check_signature(
617 | $request,
618 | $consumer,
619 | $token,
620 | $signature
621 | );
622 |
623 | if (!$valid_sig) {
624 | throw new OAuthException("Invalid signature");
625 | }
626 | }/*}}}*/
627 |
628 | /**
629 | * check that the timestamp is new enough
630 | */
631 | private function check_timestamp($timestamp) {/*{{{*/
632 | // verify that timestamp is recentish
633 | $now = time();
634 | if ($now - $timestamp > $this->timestamp_threshold) {
635 | throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
636 | }
637 | }/*}}}*/
638 |
639 | /**
640 | * check that the nonce is not repeated
641 | */
642 | private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
643 | // verify that the nonce is uniqueish
644 | $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp);
645 | if ($found) {
646 | throw new OAuthException("Nonce already used: $nonce");
647 | }
648 | }/*}}}*/
649 |
650 |
651 |
652 | }/*}}}*/
653 |
654 | class OAuthDataStore {/*{{{*/
655 | function lookup_consumer($consumer_key) {/*{{{*/
656 | // implement me
657 | }/*}}}*/
658 |
659 | function lookup_token($consumer, $token_type, $token) {/*{{{*/
660 | // implement me
661 | }/*}}}*/
662 |
663 | function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
664 | // implement me
665 | }/*}}}*/
666 |
667 | function new_request_token($consumer) {/*{{{*/
668 | // return a new token attached to this consumer
669 | }/*}}}*/
670 |
671 | function new_access_token($token, $consumer) {/*{{{*/
672 | // return a new access token attached to this consumer
673 | // for the user associated with this token if the request token
674 | // is authorized
675 | // should also invalidate the request token
676 | }/*}}}*/
677 |
678 | }/*}}}*/
679 |
680 |
681 | /* A very naive dbm-based oauth storage
682 | */
683 | class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/
684 | private $dbh;
685 |
686 | function __construct($path = "oauth.gdbm") {/*{{{*/
687 | $this->dbh = dba_popen($path, 'c', 'gdbm');
688 | }/*}}}*/
689 |
690 | function __destruct() {/*{{{*/
691 | dba_close($this->dbh);
692 | }/*}}}*/
693 |
694 | function lookup_consumer($consumer_key) {/*{{{*/
695 | $rv = dba_fetch("consumer_$consumer_key", $this->dbh);
696 | if ($rv === FALSE) {
697 | return NULL;
698 | }
699 | $obj = unserialize($rv);
700 | if (!($obj instanceof OAuthConsumer)) {
701 | return NULL;
702 | }
703 | return $obj;
704 | }/*}}}*/
705 |
706 | function lookup_token($consumer, $token_type, $token) {/*{{{*/
707 | $rv = dba_fetch("${token_type}_${token}", $this->dbh);
708 | if ($rv === FALSE) {
709 | return NULL;
710 | }
711 | $obj = unserialize($rv);
712 | if (!($obj instanceof OAuthToken)) {
713 | return NULL;
714 | }
715 | return $obj;
716 | }/*}}}*/
717 |
718 | function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
719 | if (dba_exists("nonce_$nonce", $this->dbh)) {
720 | return TRUE;
721 | } else {
722 | dba_insert("nonce_$nonce", "1", $this->dbh);
723 | return FALSE;
724 | }
725 | }/*}}}*/
726 |
727 | function new_token($consumer, $type="request") {/*{{{*/
728 | $key = md5(time());
729 | $secret = time() + time();
730 | $token = new OAuthToken($key, md5(md5($secret)));
731 | if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) {
732 | throw new OAuthException("doooom!");
733 | }
734 | return $token;
735 | }/*}}}*/
736 |
737 | function new_request_token($consumer) {/*{{{*/
738 | return $this->new_token($consumer, "request");
739 | }/*}}}*/
740 |
741 | function new_access_token($token, $consumer) {/*{{{*/
742 |
743 | $token = $this->new_token($consumer, 'access');
744 | dba_delete("request_" . $token->key, $this->dbh);
745 | return $token;
746 | }/*}}}*/
747 | }/*}}}*/
748 |
749 | class OAuthUtil {/*{{{*/
750 | public static function urlencode_rfc3986($input) {/*{{{*/
751 | if (is_array($input)) {
752 | return array_map(array('OAuthUtil','urlencode_rfc3986'), $input);
753 | } else if (is_scalar($input)) {
754 | return str_replace('+', ' ',
755 | str_replace('%7E', '~', rawurlencode($input)));
756 | } else {
757 | return '';
758 | }
759 | }/*}}}*/
760 |
761 |
762 | // This decode function isn't taking into consideration the above
763 | // modifications to the encoding process. However, this method doesn't
764 | // seem to be used anywhere so leaving it as is.
765 | public static function urldecode_rfc3986($string) {/*{{{*/
766 | return rawurldecode($string);
767 | }/*}}}*/
768 | }/*}}}*/
--------------------------------------------------------------------------------