Creating a OAuth Service Provider in PHP

This is a guide that explains how you become a Service Provider using the PHP library from oauth.net.

Getting the Library

The first step is acquiring the OAuth.php file from http://oauth.googlecode.com/svn/code/php/. So go ahead and download it. Save it somewhere you close to your current project.
There. That’s step one. This is going to be easy!

Connecting to Your Application

Step two is going to be a bit more tricky. We will need to write some actual code now. Annoying, yes — I know. But we aren’t called developers for no reason are we?
The library binds itself to your current applications data-layer by using a DataStore-proxy class. This is basically a class where you fill in the code you need for handing the stuff that OAuth needs, over to the library.

You need to create a class that lives up to the following interface:

interface OAuthDataStore {
    function lookup_consumer($consumer_key);
    function lookup_token($consumer, $token_type, $token);
    function lookup_nonce($consumer, $token, $nonce, $timestamp);
    function new_request_token($consumer);
    function new_access_token($token, $consumer);
}

I know that in the actual library, this is a class and not a interface. But it should have been an interface, so maybe in time the library will catch up.

So what do the different methods need to do? Let’s start from the beginning.

A Sample Database Layout

In my existing application I have tables for whatever data I need. I store the users in the table called users, with the primary key id.

I will store all OAuth related material in these four tables. The Request Token table (api_request_tokens) will have a flag for saying whether it’s been been authorized or not. It the token is authorized, the userid column will indicate to what user.

The tokens stored in the nonce table can be both Request and Access Tokens, so we just store the token-string instead of the id. You could also store the id and set up foreign keys if you rather do that.

CREATE TABLE `api_consumers` (
  `id` int(1) UNSIGNED NOT NULL auto_increment,
  `name` tinytext NOT NULL,
  `key` tinytext NOT NULL,
  `secret` tinytext NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=InnoDB;

CREATE TABLE `api_request_tokens` (
  `id` int(10) UNSIGNED NOT NULL auto_increment,
  `consumerid` int(10) NOT NULL,
  `key` tinytext NOT NULL,
  `secret` tinytext  NOT NULL,
  `authorized` tinyint(1) NOT NULL,
  `userid` int(10) UNSIGNED NULL,
  PRIMARY KEY  (`id`),
  FOREIGN KEY `consumerid` REFERENCES `api_consumers` (`id`)
  FOREIGN KEY `userid` REFERENCES `users` (`id`)
) ENGINE=InnoDB;

CREATE TABLE `api_access_tokens` (
  `id` int(10) UNSIGNED NOT NULL auto_increment,
  `consumerid` int(10) UNSIGNED NOT NULL,
  `key` tinytext NOT NULL,
  `secret` tinytext NOT NULL,
  `userid` int(10) UNSIGNED NOT NULL,
  PRIMARY KEY  (`id`),
  FOREIGN KEY `consumerid` REFERENCES `api_consumers` (`id`)
  FOREIGN KEY `userid` REFERENCES `users` (`id`)
) ENGINE=InnoDB;

CREATE TABLE `api_nonces` (
  `id` int(10) UNSIGNED NOT NULL auto_increment,
  `consumerid` int(10) UNSIGNED NOT NULL,
  `token` tinytext NOT NULL,
  `nonce` tinytext NOT NULL,
  `timestamp` int(10) UNSIGNED NOT NULL,
  PRIMARY KEY  (`id`),
  FOREIGN KEY `consumerid` REFERENCES `api_consumers` (`id`)
) ENGINE=InnoDB;

lookup_consumer

lookup_consumer($consumer_key) is responsible for querying your table containing consumers. It will receive the consumer key as a string, and it’s your job to return an object that has the public properties key and secret, or just an instance of OAuthConsumer if you can’t be bothered to return an object. If no such consumer is found, you must return something that evaluates to false. A sample implementation could look like this:

function lookup_consumer( $consumer_key ) {
    $consumer_table = MyFavoriteFramework::getTable('api_consumers');
    $consumer = $consumer_table->find('key', $consumer_key);
    // MyFavoriteFramework returns a MyFavoiteFrameworkObject which
    // has overloaded __get, so $consumer->key will return the
    // value from the database-column key, thus living up the 
    // requirements.
    // If the key is not found by MyFavoriteFramework returns null,
    // which evaluates to false. So in both cases $consumer will
    // have the correct format
    return $consumer
}

Let’s say you hate frameworks (which is reasonable), here’s a much lower level implementation:

function lookup_consumer( $consumer_key ) {
    $sql  = 'SELECT * FROM api_consumers WHERE key = "';
    $sql .= mysql_real_escape_string( $consumer_key);
    $sql .= '"';
    $rs = mysql_query( $sql );
    if( mysql_num_rows($rs) == 0 ) {
        return false;
    } else {
        $data = mysql_fetch_assoc($rs);
        return new OAuthConsumer( $data['key'], $data['secret'], NULL);
    }
}

Since I can’t be bothered to write lower level implementations for the rest of this guide, I will be doing them with my cute MyFavoriteFramework framework. No, it isn’t a real existing framework - but if you have tried out any decent framework it will have functionality like what I describe.

I would highly recommend that you return your own ORM-class (like in my first example) instead of the OAuthConsumer class. The reason for this is how much time you save later on, not converting the OAuthConsumer back into your ORM-class.

lookup_token

lookup_token( $consumer, $token_type, $token ) is responsible for looking up the tokens. And by tokens I mean both Request and Access Tokens. The recieved $consumer is whatever you decided to return from lookup_consumer. So in this example I will assume that what lookup_consumer is returning, is an MyFavoriteFrameworkObject, where I can get the id of the consumer-row by doing $consumer->id. This is because I store the entire row from the consumer in the object I return from lookup_consumer. Had you only returned a OAuthConsumer, and had you used something other than the key as a foreign-key to the token-tables, you would have to lookup the token again. So remember to return everything relevant to the consumer from lookup_consumer.

You must return an object that has the public properties key and secret. This can be an object from MyFavoriteFramework, or it can be an instance of the OAuthToken class.

If the token isn’t found, or is invalid, return something that evaluates to false.

function lookup_table( $consumer, $token_type, $token ) {
    if( $token_type == 'request' ) {
        $table = MyFavoriteFramework::getTable('api_request_tokens');
    } else if( $token_type == 'access' ) {
        $table = MyFavoriteFramework::getTable('api_access_tokens');
    }

    $token = $table->find('key', $token);
    if( $token->consumerid != $consumer->id ) {
        $token = null; // The token doesn't belong to the consumer
    }

    return $token;
}

Remember that it’s important to check that the found token belongs to the consumer who is making the call.

Again, I recommend that you use your ORM-class instead of the OAuthToken class to save lookup times later on.

lookup_nonce

lookup_nonce( $consumer, $token, $nonce, $timestamp ) is responsible for avoiding replay attacks. The function should check that no identical nonce exists. If that holds, you must add the nonce to the database and return true. If, on the otherhand, the nonce is known by your application you must return false.

The received $consumer and $token is what you returned from lookup_consumer and lookup_token. Both $nonce and $timestamp are strings.

A sample implementation could be something like this:

function lookup_nonce( $consumer, $token, $nonce, timestamp ) {
    $table = MyFavoriteFramework::getTable(`api_nonces`);
    $existing = $table->find( array(
        'consumerid' => $consumer->id,
        'token' => $token->key,
        `nonce` => $nonce,
        'timestamp' => (int) $timestamp 
    ));
    if( ! $existing ) {
        // Not a replay attack. Save the nonce
        $table->insertRow( array(
            'consumerid' => $consumer->id,
            'token' => $token->key,
            `nonce` => $nonce,
            'timestamp' => (int) $timestamp 
        ));

        return true;
    } else {
        return false;
    }
}

If you are in the developing phase of your implementation you might consider hacking this method to always return true. This enables you to try your own calls again and again. But never leave this in. It’s critical for your security that you have the nonce check implemented in production.

new_request_token

new_request_token( $consumer ) is responsible for handing out new Request Tokens. So this is a fairly basic function that doesn’t involve much.

Again, the $consumer received is the same object as you returned from lookup_consumer. As with lookup_token, you need to return an object with the properties key and secret. If something goes awry you throw an OAuthException.

A sample implementation:

function new_request_token( $consumer ) {
    $table = MyFavoriteFramework::getTable('api_request_tokens');

    $key    = substr(md5(rand(0,1000)),0,8); 
    $secret = substr(md5(rand(0,1000)),0,8);
    // Note: You should do something more clever that this
    // since this only has 1000 different keys and secrets.

    $new_token = $table->insertRow( array(
        'consumerid' => $consumer->id,
        'key' => $key,
        'secret' => $secret
        'authorized' => 0
    ));

    return $new_token;
}

new_access_token

new_access_token( $token, $consumer ) is responsible for trading a authorized Request Token into a Access Token.

As you might suspect by now $token and $consumer is the same objects as you returned from lookup_token and lookup_consumer. As usual you will have to return an object with the properties key and secret. If the exchange from a Request Token to an Access Token is successful, you must invalidate the Request Token.
If something is amis (e.g. the Request Token isn’t authorized), you throw an OAuthException.

A basic implementation could be like this. Since I returned a MyFavoriteFrameworkObject from lookup_token, I have the entire row from the table in the $token-object, so I can just query the authorized property to check that the token has been authorized.

function new_access_token( $request_token, $consumer ) {
    if( $token->authorized ) {
        $table = MyFavoriteFramework::getTable('api_request_tokens');

        $key    = substr(md5(rand(0,1000)),0,8); 
        $secret = substr(md5(rand(0,1000)),0,8);
        // Note: You should do something more clever that this
        // since this only has 1000 different keys and secrets.

        $access_token = $table->insertRow( array(
            'consumerid' => $consumer->id,
            'key' => $key,
            'secret' => $secret,
            'userid' => $request_token->userid;
        ));

        $request_token->deleteFromTable();

        return $access_token
    } else {
        throw new OAuthException('Unauthorized Access Token!');
    }
}

And there you have it. We finished our DataStore. We can now move on to adding the OAuth Endpoints to our application

Adding Endpoints

The OAuth Specification requires three different endpoints. One for handing out Request Token, one for authorizing them and one for trading them into Access Tokens.

But before we start working on the endpoints, let’s make sure we are ready to work with the OAuth library. The Service Provider functionality is mainly contained in the OAuthServer class, so we need to set up an instance of it for us to work with.

Seting Up Our OAuthServer

All our OAuth Endpoints will need access to an instance of OAuthServer, instantiated with our DataStore.

So in some common directory of your application, you can create a oauthserver.php file that you include on all the endpoints. You can also do this in a million other ways. The important thing is that you need the instance at every request that involves OAuth.

$oauth_server = new OAuthServer( new OurOAuthDataStore() );
// Setup the server with our DataStore

$hmac_method = new OAuthSignatureMethod_HMAC_SHA1();
$oauth_server->add_signature_method( $hmac_method );
// We will support HMAC/SHA1 signatures

$plaintext_method = new OAuthSignatureMethod_PLAINTEXT();
$oauth_server->add_signature_method( $plaintext_method );
// And plaintext signatures

Now that we have out OAuthServer set up, we can start tackling the endpoints one by one. So let’s start with the first one: Handing out Request Tokens.

New Request Token

The endpoint for handing out Request Tokens is really really simple. It only needs to call the fetch_request_token with an instance of OAuthRequest describing the current request.

Actually, this holds for most of the methods that OAuthServer offers. You will need an instance of OAuthRequest describing the current event. Luckily for you OAuthRequest::from_request() returns just such an instance.

So the only thing this endpoint has to do is call the method, and return the token in the format specified by the spec:

try {
    $request = OAuthRequest::from_request();
    $token = $oauth_server->fetch_request_token( $request );

    printf(
        'oauth_token=%s&oauth_token_secret=%s',
        OAuthUtil::urlencodeRFC3986($this->key),
        OAuthUtil::urlencodeRFC3986($this->secret)
    );
} catch( OAuthException $e ) {
    header("HTTP/401 Unauthorized");
    echo $e->getMessage();
}

If we get an OAuthException we return the message along with a HTTP 401 header. The spec actually requires you to differentiate between different classes of errors and either return 400 or 401. But this hasn’t been built into the library yet.

Authorize Request Token

The Request Token needs to be authorized and tied to a user somehow, and that somehow is by using the Authorize endpoint.

The Authorize endpoint will receive the two parameters oauth_token and (optionally) oauth_callback. You should then present a user with a form where he can choose to grant or deny access of the Consumer to your data.

Sadly, there is no clever way to use either the OAuthServer or your DataStore to convert the received oauth_token to objects. This is because the Consumer that owns the token isn’t identified. I don’t know why that is, sorry.

So we will have to do all the dirty work again…

An example authorization page could look like this:

LoginSystem::ensure_user_is_logged_in();

$request = OAuthRequest::from_request();

$consumer_t = MyFavoriteFramework::getTable('api_consumers');
$token_t    = MyFavoriteFramework::getTable('api_request_tokens');

$token = $token_t->find('key', $request->get_parameter('oauth_token'));
$consumer = $consumer_t->find('id', $token->consumerid);

if( form_submit_verifies_access() ) {
    $token->updateTableRow(
        'authorized' => 1,
        'userid' => LoginSystem::current_user()->id
    );
} else if( form_submit_denies_access() ) {
    $token->deleteFromTable();
} else {
    show_authorization_form( $consumer );
}

Yeah, I know. I’m lazy so I couldn’t be bothered writing the actual form and checking if it’s been submitted. I have also invented the fictional class LoginSystem which is a stand in for the actual login system.
But you should get the general idea.

You need to look up the token and consumer from the received oauth_token parameter. You can get the parameter by using $request->get_parameter('oauth_token')).

If the authorization form has been submitted and is allowing access, we need to update the given token to signal that it is now authorized and tied to the current user.
If the form has been submitted but access has been denied we can just remove the Request Token.

If the form hasn’t been submitted, display some sort of form that describes who is granted what access.

It is also worth noting, that had we used the OAuthToken class instead of our own ORM-class, we would have to invest more time into updating the tokens.

Switch Request Token to Access Token

The endpoint that trades authorized Request Tokens into Access Tokens is actually really simple. Remember how we added the check that ensured only authorized tokens gets traded? That also ensures that we don’t really have to do anything clever on this endpoint.

In fact, the implementation of this endpoint only differs from the Request Token endpoint in one way. We use the fetch_access_token method instead of fetch_request_token.

try {
    $request = OAuthRequest::from_request();
    $token = $oauth_server->fetch_access_token( $request );

    printf(
        'oauth_token=%s&oauth_token_secret=%s',
        OAuthUtil::urlencodeRFC3986($this->key),
        OAuthUtil::urlencodeRFC3986($this->secret)
    );
} catch( OAuthException $e ) {
    header("HTTP/401 Unauthorized");
    echo $e->getMessage();
}

How easy was that?

Okay - we have actually fulfilled our duties towards the OAuth specs. What we need now is to actually use it to protect access to our private resources.

Adding OAuth to Your Resources

This is what OAuth was invented for. Protecting access to your private resources. All the previous things we did in this guide was just warming up to this point. On all the resources we want limited access to, we add the following code snippets:

try {
    $request = OAuthRequest::from_request();
    list($consumer, $token) = $oauth_server->verify_request( $request );
} catch( OAuthException $e ) {
    // The request wasn't valid!
    header('WWW-Authenticate: OAuth realm="http://sp.example.com/"');
    echo 'I\'m afraid I can\'t let you do that Dave.';
    die;
}

LoginSystem::prime_with_oauth_userid( $token->userid );

Okay - so you might want to handle the whole “access denied”-scenario a bit better. But you get the idea.

Another important thing to note is how you would get the identity of the user who authorized this OAuth call.
Since we returned our ORM-class in the lookup_token method in our DataStore, we can just use the userid property of the token. Had we gone another way and used the OAuthToken we would have to make more lookup to find the user who authorized the token. This is another of the reasons why I recommend using your ORM-classes as consumer and token-classes.

About the Author

Morten Fangel is a contributing member to the PHP OAuth library. He has contributed with multiple patches which has merged into the trunk of the library.
He currently uses OAuth on Campus Notes, but the API has yet to be publicly announced.

This article was written in Markdown!