PHP and the Twitter API v1.1

I’d been using the relatively-awesome TwitterOAuth library by Abraham Williams for quite some time to handle interactions between my sites and Twitter’s REST API v1.  With Twitter having eliminated v1 of the API, I started looking into other options.

It’s true that TwitterOAuth can be updated easily, changing a hardcoded 1 to 1.1 but Twitter introduced a new wrinkle with the move to v1.1 that v1 didn’t have: All requests must be authenticated.

This makes sense for actions such as posting a new Tweet, as you can’t very well do so without having a user to Tweet on the behalf of.  For that reason, you had to be authenticated to make that request in v1, so it’s nothing new for v1.1.  But what about if you just want to get the timeline of a specific user, or data on a specific Tweet?  Those are actions you might want to do through an automated process, in which case there would be no logged-in user to act on the behalf of.

Well, that’s what bearer tokens are for.  And TwitterOAuth doesn’t handle them.  So rather than use TwitterOAuth for one set of requests and something else for others, I wrote a new class that can handle Basic, OAuth, and Bearer auth types.

<?php
  class twitter_api {
    private $application_key;
    private $application_secret;
    private $user_token;
    private $user_secret;
    private $bearer_token;
    private $auth_type;
    private $oauth;
    private $args;

    public function __construct ($application_key, $application_secret) {
      $this->application_key = $application_key;
      $this->application_secret = $application_secret;
      $this->auth_type = AUTH_TYPE_BASIC;
    }

    public function auth ($type, $token, $secret = false) {
      if ($type == AUTH_TYPE_OAUTH) {
        $this->user_token = $token;
        $this->user_secret = $secret;
        $this->oauth = array('oauth_consumer_key' => $this->application_key, 'oauth_nonce' => (string)mt_rand(), 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_token' => $this->user_token, 'oauth_timestamp' => time(), 'oauth_version' => '1.0');
      } elseif ($type == AUTH_TYPE_BEARER) {
        $this->bearer_token = $token;
      }

      $this->set_auth_type($type);
    }

    public function set_auth_type ($type) {
      $this->auth_type = $type;
    }

    public function request ($type, $request, $args = false, $body = false) {
      $this->args = (is_array($args)) ? $args : array($args);
      $full_url = $base_url = 'https://api.twitter.com/' . $request;

      if (($type == 'GET') AND (count($this->args))) $full_url .= '?' . http_build_query($this->args);

      if ($this->auth_type == AUTH_TYPE_OAUTH) {
        $this->oauth['oauth_signature'] = base64_encode(hash_hmac('sha1', $this->build_oauth_base_string($type, $base_url), (rawurlencode($this->application_secret) . '&' . rawurlencode($this->user_secret)), true));
        $header = array($this->build_oauth_header(), 'Expect:');
      } elseif ($this->auth_type == AUTH_TYPE_BEARER) {
        $header = array('Authorization: Bearer ' . $this->bearer_token);
      } else {
        $header = array('Authorization: Basic ' . base64_encode($this->application_key . ':' . $this->application_secret));
      }

      $c = curl_init();
      curl_setopt($c, CURLOPT_HTTPHEADER, $header);
      curl_setopt($c, CURLOPT_HEADER, 0);
      curl_setopt($c, CURLOPT_VERBOSE, 0);
      curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
      curl_setopt($c, CURLOPT_SSL_VERIFYPEER, 1);
      curl_setopt($c, CURLOPT_URL, $full_url);

      switch ($type) {
        case 'POST':
          curl_setopt($c, CURLOPT_POST, 1);
          break;
        case 'GET':
          curl_setopt($c, CURLOPT_HTTPGET, 1);
          break;
        default:
          curl_setopt($c, CURLOPT_CUSTOMREQUEST, $type);
      }

      if ($body) {
        curl_setopt($c, CURLOPT_POSTFIELDS, $body);
      } elseif (($type != 'GET') AND (count($this->args))) {
        curl_setopt($c, CURLOPT_POSTFIELDS, http_build_query($this->args));
      }

      $data = curl_exec($c);
      curl_close($c);

      return json_decode($data);
    }

    private function build_oauth_base_string ($type, $url) {
      $incoming = array_merge($this->oauth, $this->args);
      ksort($incoming);

      $data = array();
      foreach ($incoming AS $key => $val) {
        $data[] = $key . '=' . rawurlencode($val);
      }

      return $type . '&' . rawurlencode($url) . '&' . rawurlencode(implode('&', $data));
    }

    private function build_oauth_header () {
      ksort($this->oauth);

      $data = array();
      foreach ($this->oauth as $key => $val) {
        $data[] = $key . '="' . rawurlencode($val) . '"';
      }

      return 'Authorization: OAuth ' . implode(', ', $data);
    }
  }
?>

You’ll never hear me say that this is some kind of end-all, be-all solution.  I’m not even sure it’s all that good.  It just appears to solve the problem I was trying to handle and since I didn’t see a lot of code that did, I figured it might be useful to post.

Some more details on how this works…

This is built to handle the kind of Basic auth requests you would need to make in order to get an OAuth or Bearer token to continue making requests.  After you get your bearer token, you can switch to using that.  Then you could switch to OAuth or back to Basic if you needed to.

Here’s an example:

$twitter = new twitter_api($consumer_key, $consumer_secret);

$data = $twitter->request('POST', 'oauth2/token', false, 'grant_type=client_credentials');
$bearer_token = $data->access_token;

$twitter->auth(AUTH_TYPE_BEARER, $bearer_token);
$data = $twitter->request('GET', '1.1/statuses/show.json', array('id'=> '345219659211108353', 'include_entities' => true));

print_r($data);

$twitter->set_auth_type(AUTH_TYPE_BASIC);
$data = $twitter->request('POST', 'oauth2/invalidate_token', false, ('access_token=' . $bearer_token));

unset($twitter);

We start by initiating the object using our application’s consumer key and consumer secret (no getting around that).  Because that’s all we have at that point, we use them with Basic auth to make the request for a bearer token.  That request is a POST to oauth2/token with a body of grant_type=client_credentials.  The third parameter of my request function is for any arguments for the API call and we have none for this one, so it’s set to false.

That request spits back an object that includes a bearer token, so we save that as $bearer_token for future use.

Our next request is for data on a specific Tweet.  We need OAuth or Bearer auth for that so we use the auth function to feed in the bearer token we just got.  That function will also switch us over to using Bearer auth for all of our subsequent requests.  With that out of the way, we use request again, this time hitting 1.1/statuses/show.json with a GET request.  Unlike in our previous call, we have optional parameters to use (but no body, so it can be ignored).  Our parameters will be passed in as an array, with the Tweet ID defined and include_entities set to true.

That request will return the data for the Tweet we specified.  Since we’re not doing anything with it in this example, we just spit it back out on the screen.

Since this example is done, we close it out by invalidating the bearer token we just created.  You probably would actually want to save that token to reuse it within your application but we destroy it for example’s sake.  To do that, we use set_auth_type to switch back to Basic auth, then we POST to oauth2/invalidate_token with a body of access_token=XXXXXXX (where XXXXXXX is the bearer token we got earlier).

For the record, had we wanted to make a request that required a user’s OAuth authorization, it would have looked like this:

$twitter->auth(AUTH_TYPE_OAUTH, $token, $secret);
$response = $twitter->request('POST', '1.1/statuses/update.json', array('status' => $text));

Where $text is the text to be Tweeted, of course.

As I said, this isn’t any kind of end-all, be-all.  It doesn’t have any kind of error handling, I’ve only tested it on the things I was already using the Twitter API for, and I’ve only tested it on my own machines.  It works for me, though, so I figured I’d throw it out to the world in case it might work for someone else.

S/T: There’s a great answer on StackOverflow about manually building the OAuth headers that really helped me out in this.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.