OAuth 2.0 PHP Library

Throughout the Linkey project I’ve been working on a number of PHP libraries that aim to make working with OAuth 2.0 easier.

The library is now at version 2.1, implements the entire core OAuth 2.0 specification and bearer token specification and has had over 1800 installations at the time of writing.

I’m very happy to announce that the University of Lincoln has agreed to transfer ownership of the code to the “PHP League of Extraordinary Packages” which means that it’ll continue to receive updates and be maintained by a number of developers around the world (myself included).

The server repository is now hosted at https://github.com/php-loep/oauth2-server and the client library is now hosted at https://github.com/php-loep/oauth2-client.

I have updated the wikis on both repositories with thorough documentation about how to use each library, and on the server library wiki I’ve added a lot more implementation about how to implement the library for common use cases.

My intention with all the code that has been written as an output of this project was that it would be as easy to use as possible and I’ve had some great feedback from developers:

— “Thanks for making your OAuth 2.0 server library public! It is people like you that make my job slightly more tolerable :)”

— “I wanted to thank you for your awesome library @alexbilbie :)”

— “Thank you for the great posts and lib”

— “If you drink, I will buy you a virtual beer!! What you built is awesome”

And finally in reply to a question that a developer asked me about how to do something with the library:

— “I’m really not surprised it was that simple – you’ve really built this with the end developer in mind. Once again, thanks very much for the info and the repo.”

To conclude I’m really happy with the code, I’ve learnt an awful lot as it has been developed and refined and I’m pleased that it’s new home will ensure long term sustainability.

Project dissemination at the Internet Identity Workshop

Last week I was in San Francisco for the 16th Internet Identity Workshop to disseminate the Linkey project and engage with those in the online identity communities.

The event was sponsored by many big names including Microsoft, Ping Identity, Google, Janrain, Yubico, Cisco, OneID and NetIQ. In attendance were employees from all of the sponsor companies as well as delegates ranging from freelance developers to information compliance officers at big companies to authors of many Internet specifications and protocols such as OpenID, OAuth and SAML. About half of hundred or so delegates had travelled from outside North America including about ten of us from Europe. Surprisingly I was the only delegate representing an educational institution.

The conference itself was un-conference style – i.e. each morning delegates would volunteer sessions which were then slotted into the timetable. A full list of all of the sessions can be found here. The sessions people put forward tended to either be very technical or were higher level, almost philosophical discussions.

Here is a list of sessions I attended (session notes can be found here):

  • T1G: Native Apps – SSO
  • T2B: Strong 2-Factor For All – Google and FIDO Alliance
  • T3C: The OAuth Complicit Flow
  • T4G: Identity Federation: Failed Consumer Experiences and WHat We Can Do About It
  • W1H: OAUTH Client Registration
  • W3B: OAuth 2 Bootstrapping from device to browser (technical)
  • W4B: Google’s Auth goals for the next 5 years
  • Google are strongly committed to OAuth!!!!
  • W5I: OAuth 2 Federation – RS trust external AS
  • TH2E: Practical DATA PROTECTION – Avoidance? EU and US ?
  • TH3A: RESPECT CONNECT “Facebook Connect for Personal Clouds” OR “Social Login that Doesn’t Suck”
  • TH4F: Self-Hosted Personal Clouds (FreedomBox and Raspberry PI)

The two main overarching topics throughout the event were “privacy” and “data ownership”. There were also a number of sessions about security, with one session that I attended by Google about 2-factor authentication resulting in quite heated discussion (namely because the work Google and the FIDO Alliance is not public and there is a high fee for membership).

Another interesting session was called “The OAuth Complicit Flow” (notes here) which had the premise of “what if the an OAuth authorisation server asked you to agree to “allow this application to connect to your account and murder someone” and there was no deny button. The discussion dealt with the issue of some applications asking for too many permissions, users not reading through the approve screens (similar to how users just accept EULA agreements) and applications refusing to let users access them unless they agree to allowing the application share stuff on their Facebook wall, or see the user’s friend list. Potential solutions that came out of this discussion was “reactive” permission requests as opposed to “preemptive” permissions – similar to how some iOS apps don’t ask for permission to send push notifications unless you click a button in the settings for that app.

I got the opportunity to have a chat with Mike Jones from Microsoft who has been leading much of the work on the OAuth 2.0 and OpenID Connect specifications. He answered a few questions I had about edge cases in implementations and he was interested in the PHP libraries I’d developed as part of the project.

I didn’t have the opportunity to talk at length about Linkey but in all the sessions I attended I tried to take part and I had some really interesting discussions with people about some of work that we’re doing in HE around open data and open APIs (including work on OAuth) and I was able to talk about some of the issues we faced with implementing OAuth in enterprise environments (because of incompatibility and lack of understanding compared to SAML).

I feel that the conference was definitely worth attending and I would encourage JISC and other interested parties in the education sectors to try and attend the next Internet Identity Workshop as I left feeling that we’re dealing with some very similar problems that even the very large organisations present such as Oracle are dealing with.

Easily integrate other OAuth 2.0 identity providers with PHP

One of the other PHP libraries I’ve been working for Linkey is a PHP library that makes working with other OAuth 2.0 identity providers “stupidly easy”. I think I’ve done that and it’s time to announce the initial release – https://github.com/lncd/OAuth2-client.

So lets say you want to allow users to sign-in to their Facebook account:

$provider = new \OAuth2\Client\Provider\Facebook(array(
    'clientId'  =>  'XXXXXXXX',
    'clientSecret'  =>  'XXXXXXXX',
    'redirectUri'   =>  'http://your-registered-redirect-uri/'
));

if ( ! isset($_GET['code'])) {

    // If we don't have an authorization code then get one
    $provider->authorize();

} else {

    try {

        // Try to get an access token (using the authorization code grant)
        $t = $provider->getAccessToken('authorization_code', array('code' => $_GET['code']));

        try {

            // We got an access token, let's now get the user's details
            $userDetails = $provider->getUserDetails($t);

            foreach ($userDetails as $attribute => $value) {
                var_dump($attribute, $value) . PHP_EOL . PHP_EOL;
            }

        } catch (Exception $e) {

            // Failed to get user details

        }

    } catch (Exception $e) {

        // Failed to get access token

    }
}

Simple right? If you take out the try/catch statements then it essentially boils down to this:

$provider = new \OAuth2\Client\Provider\<provider name>(array(
    'clientId'  =>  'XXXXXXXX',
    'clientSecret'  =>  'XXXXXXXX',
    'redirectUri'   =>  'http://your-registered-redirect-uri/'
));

if ( ! isset($_GET['code'])) {

    $provider->authorize();

} else {

    $token = $provider->getAccessToken('authorization_code', array('code' => $_GET['code']));
    $userDetails = $provider->getUserDetails($token);
}

The library automatically manages the state parameter to help mitigate cross-site request forgery attacks (where supported by the end-IdP).

At the time of writing there is built in support for Facebook, Google and Github but adding support for other identity providers is trivial – you just need to extend the IdentityProvider class.

I will add support for more providers soon. There also aren’t any unit tests currently but they are coming.

The library is hooked up to Packagist so just add "lncd/oauth2-client": “*” to your composer.json file.

Securing your API with OAuth 2.0

In a previous post I announced my new OAuth 2.0 PHP libraries.

In this post I will show you how to use the server library to secure a simple API with OAuth 2.0.


Install the library

The recommended way of installing the library is via Composer.

If you already have a composer.json file in your root then add ”lncd/oauth2”: “*” in the require object. Then run composer update.

Otherwise create a new file in your project root called composer.json add set the contents to:

{
    "require": {
        "lncd/OAuth2": "*"
    }
}

Now, assuming you have installed Composer run composer install.

Ensure now that you’ve set up your project to autoload composer packages.

You could alternatively add the library as a git submodule or download a zip.

Set up the database

To setup the database just import sql/mysql.sql

Create the storage models

In order to retrieve data from the database you should create classes which implement the following interfaces:

  • \OAuth2\Storage\ScopeInterface
  • \OAuth2\Storage\SessionInterface

Hooking it all up

Setting up the library is simple, just create a new instance of \OAuth2\ResourceServer and pass in your storage models.

// Include the storage models
include 'model_scope.php';
include 'model_session.php';

// Initiate the Request handler
$request = new \OAuth2\Util\Request();

// Initiate the auth server with the models
$server = new \OAuth2\ResourceServer(new SessionModel, new ScopeModel);

Checking for valid access tokens

Before your API responds you need to check that an access token has been presented with the request (either in the query string ?access_token=abcdef or as an authorization header Authorization: bearer abcdef).

If you’re using a framework such as Laravel or CodeIgniter you could use a route filter to do this, or have a custom controller which other controllers extend from. In this example I’m using the Slim framework and I’m going to create a simple route middleware which is run before each endpoint function.

$checkToken = function () use ($server) {

    return function() use ($server)
    {
        // Test for token existance and validity
        try {
            $server->isValid();
        }

        // The access token is missing or invalid...
        catch (\OAuth2\Exception\InvalidAccessTokenException $e)
        {
            $app = \Slim\Slim::getInstance();
            $res = $app->response();
            $res['Content-Type'] = 'application/json';
            $res->status(403);

            $res->body(json_encode(array(
                'error' =>  $e->getMessage()
            )));
        }
    };

};

When $server->isValid() is called the library will run the following tasks:

  • Check if an access token is present in the query string
    • If not, check if a base64 encoded access token is contained in an authorization header.
      • If not, throw \OAuth2\Exception\InvalidAccessTokenException
  • Check if the access token is valid with the database
    • If not, throw \OAuth2\Exception\InvalidAccessTokenException
  • If the access token is valid:
    • Get the owner type (e.g. “user” or “client”) and their ID
    • Get a list of any scopes that are associated with the access token

Assuming an exception isn’t thrown you can then use the following functions in your API code:

  • getOwnerType() – This will return the type of the owner of the access token. For example if a user has authorized another client to use their resources the owner type would be “user”.
  • getOwnerId() – This will return the ID of the access token owner. You can use this to check if the owner has permission to do take some sort of action (such as retrieve a document or upload a file to a folder).
  • hasScope() – You can use this function to see if a specific scope (or several scopes) has been associated with the access token. You can use this to limit the contents of an API response or prevent access to an API endpoint without the correct scope.

A simple example

This example endpoint will return a user’s information if a valid access token is present. If the access token has the user.contact it will return additional information.

$app->get('/user/:id', $checkToken(), function ($id) use ($server, $app) {

    $user_model = new UserModel();

    $user = $user_model->getUser($id);

    if ( ! $user)
    {
        $res = $app->response();
        $res->status(404);
        $res['Content-Type'] = 'application/json';
        $res->body(json_encode(array(
            'error' => 'User not found'
        )));
    }

    else
    {
        // Basic response
        $response = array(
            'error' => null,
            'result'    =>  array(
                'user_id'   =>  $user['id'],
                'firstname' =>  $user['firstname'],
                'lastname'  =>  $user['lastname']
            )
        );

        // If the acess token has the "user.contact" access token include
        //  an email address and phone numner
        if ($server->hasScope('user.contact'))
        {
            $response['result']['email'] = $user['email'];
            $response['result']['phone'] = $user['phone'];
        }

        // Respond
        $res = $app->response();
        $res['Content-Type'] = 'application/json';

        $res->body(json_encode($response));
    }

});

Limiting an endpoint to a specific owner type

In this example, only a user’s access token is valid:

$app->get('/user', $checkToken(), function () use ($server, $app) {

    $user_model = new UserModel();

    // Check the access token's owner is a user
    if ($server->getOwnerType() === 'user')
    {
        // Get the access token owner's ID
        $userId = $server->getOwnerId();

        $user = $user_model->getUser($userId);

        // If the user can't be found return 404
        if ( ! $user)
        {
            $res = $app->response();
            $res->status(404);
            $res['Content-Type'] = 'application/json';
            $res->body(json_encode(array(
                'error' => 'Resource owner not found'
            )));
        }

        // A user has been found
        else
        {
            // Basic response
            $response = array(
                'error' => null,
                'result'    =>  array(
                    'user_id'   =>  $user['id'],
                    'firstname' =>  $user['firstname'],
                    'lastname'  =>  $user['lastname']
                )
            );

            // If the acess token has the "user.contact" access token include
            //  an email address and phone numner
            if ($server->hasScope('user.contact'))
            {
                $response['result']['email'] = $user['email'];
                $response['result']['phone'] = $user['phone'];
            }

            // Respond
            $res = $app->response();
            $res['Content-Type'] = 'application/json';

            $res->body(json_encode($response));
        }
    }

    // The access token isn't owned by a user
    else
    {
        $res = $app->response();
        $res->status(403);
        $res['Content-Type'] = 'application/json';
        $res->body(json_encode(array(
            'error' => 'Only access tokens representing users can use this endpoint'
        )));
    }

});

You might use an API function like this to allow a client to discover who a user is after they’ve signed into your authorization endpoint (see an example of how to do this here).

Limiting an endpoint to a specific owner type and scope

In this example, the endpoint will only respond to access tokens that are owner by client applications and that have the scope users.list.

$app->get('/users', $checkToken(), function () use ($server, $app) {

    $user_model = new UserModel();

    $users = $user_model->getUsers();

    // Check the access token owner is a client
    if ($server->getOwnerType() === 'client' && $server->hasScope('users.list'))
    {
        $response = array(
            'error' => null,
            'results'   =>  array()
        );

        $i = 0;
        foreach ($users as $k => $v)
        {
            // Basic details
            $response['results'][$i]['user_id'] = $v['id'];
            $response['results'][$i]['firstname'] = $v['firstname'];
            $response['results'][$i]['lastname'] = $v['lastname'];

            // Include additional details with the right scope
            if ($server->hasScope('user.contact'))
            {
                $response['results'][$i]['email'] = $v['email'];
                $response['results'][$i]['phone'] = $v['phone'];
            }

            $i++;
        }

        $res = $app->response();
        $res['Content-Type'] = 'application/json';

        $res->body(json_encode($response));
    }

    // Access token owner isn't a client or doesn't have the correct scope
    else
    {
        $res = $app->response();
        $res->status(403);
        $res['Content-Type'] = 'application/json';
        $res->body(json_encode(array(
            'error' => 'Only access tokens representing clients can use this endpoint'
        )));
    }

});

You might secure an endpoint in this way to only allow specific clients (such as your applications’ main website) access to private APIs.


Hopefully you will see how easy it is to secure an API with OAuth 2.0 and how you can use scopes to limit response contents or access to endpoints.

You can download a complete working example here – https://github.com/lncd/oauth2-example-resource-server.

Developing an OAuth 2.0 authentication server

This guide will show you how to setup an OAuth 2.0 authentication server which supports the authorization code grant type.

Install the library

The recommended way of installing the library is via Composer.

If you already have a composer.json file in your root then add ”lncd/oauth2”: “*” in the require object. Then run composer update.

Otherwise create a new file in your project root called composer.json add set the contents to:

{
    "require": {
        "lncd\OAuth2": "*"
    }
}

Now, assuming you have installed Composer run composer install.

Ensure now that you’ve set up your project to autoload composer packages.


You could alternatively add the library as a git submodule or download a zip.

Set up the database

To setup the database just import sql/mysql.sql

Create your first client

In OAuth terms a client is an application (it could be a website or a mobile app) that communicates with your API.

Insert a client into the oauth_clients table. It is recommended that you make the id and secret fields random alphanumeric strings – http://randomkeygen.com/ is a useful for this. The auto_approve parameter should be to 1 if you want the user to automatically approve access to the client, otherwise set it to 0.

If you want to use the authorization grant (where a user is redirected to the auth server from the client and the back in order to “sign-in” or “connect” with the client) then in the oauth_client_endpoints add a redirect URI (where the user is redirected back to after signing in). You can add multiple redirect URIs for production and development.

Create the storage models

In order to persist data to the database you should create classes which implement the following three interfaces:

  • \OAuth2\Storage\ClientInterface
  • \OAuth2\Storage\ScopeInterface
  • \OAuth2\Storage\SessionInterface

The authorization code grant

For reference here is an overview of how the authorization code grant works:

+--------+                               +---------------+
|        |--(A)- authorisation Request ->|   Resource    |
|        |                               |     Owner     |
|        |< -(B)-- authorisation Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- authorisation Grant -->| authorisation |
| Client |                               |     Server    |
|        |< -(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |< -(F)--- Protected Resource ---|               |
+--------+                               +---------------+

(A) The client requests authorisation from the resource owner. The authorisation request can be made directly to the resource owner (as shown), or preferably indirectly via the authorisation server as an intermediary.

(B) The client receives an authorisation grant, which is a credential representing the resource owner’s authorisation, expressed using one of four grant types defined in this specification or using an extension grant type. The authorisation grant type depends on the method used by the client to request authorisation and the types supported by the authorisation server.

(C) The client requests an access token by authenticating with the authorisation server and presenting the authorisation grant.

(D) The authorisation server authenticates the client and validates the authorisation grant, and if valid issues an access token.

(E) The client requests the protected resource from the resource server and authenticates by presenting the access token.

(F) The resource server validates the access token, and if valid, serves the request.

Create an oauth controller

NOTE: This is assuming you’re using a framework that follows an MVC pattern, If you’re using individual files for each page then you create a new page for each controller route listed henceforth.

In your controller constuctor you should instantiate the auth server:

public function __construct()
{
    // Initiate the request handler which deals with $_GET, $_POST, etc
    $request = new \OAuth2\Util\Request();

    // Create the auth server, the three parameters passed are references to the storage models
    $this->authserver = new \OAuth2\AuthServer(new ClientModel, new SessionModel, new ScopeModel);

    // Enable the authorization code grant type
    $this->authserver->addGrantType(new \OAuth2\Grant\AuthCode());

    // Set the TTL of an access token in seconds (default to 3600s / 1 hour)
    $this->authserver->setExpiresIn(86400);
}

Create your first route (for example “index” – which would resolve to /oauth).

public function action_index()
{
    try {

        // Tell the auth server to check the required parameters are in the query string
        $params = $this->authserver->checkAuthoriseParams();

        // Save the verified parameters to the user's session
        Session::put('client_id', $params['client_id']);
        Session::put('client_details', $params['client_details']);
        Session::put('redirect_uri', $params['redirect_uri']);
        Session::put('response_type', $params['response_type']);
        Session::put('scopes', $params['scopes']);

        // Redirect the user to the sign-in route
        return Redirect::to(‘oauth/signin');

    } catch (Oauth2\Exception\ClientException $e) {

        // Throw an error here which says what the problem is with the auth params

    } catch (Exception $e) {

        // Throw an error here which has caught a non-library specific error

    }
}

Next create a sign-in route:

public function action_signin()
{
    // Retrieve the auth params from the user's session
    $params['client_id'] = Session::get('client_id');
    $params['client_details'] = Session::get('client_details');
    $params['redirect_uri'] = Session::get('redirect_uri');
    $params['response_type'] = Session::get('response_type');
    $params['scopes'] = Session::get('scopes');

    // Check that the auth params are all present
    foreach ($params as $key=>$value) {
        if ($value === null) {
            // Throw an error because an auth param is missing - don't continue any further
        }
    }

    // Process the sign-in form submission
    if (Input::get('signin') !== null) {
        try {

            // Get username
            $u = Input::get('username');
            if ($u === null || trim($u) === '') {
                throw new Exception('please enter your username.');
            }

            // Get password
            $p = Input::get('password');
            if ($p === null || trim($p) === '') {
                throw new Exception('please enter your password.');
            }

            // Verify the user's username and password
            // Set the user's ID to a session

        } catch (Exception $e) {
            $params['error_message'] = $e->getMessage();
        }
    }

    // Get the user's ID from their session
    $params['user_id'] = Session::get('user_id');

    // User is signed in
    if ($params['user_id'] !== null) {

        // Redirect the user to /oauth/authorise route
        return Redirect::to('oauth/authorise');

    }

    // User is not signed in, show the sign-in form
    else {
        return View::make('oauth.signin', $params);
    }
}

In the sign-in form HTML page you should tell the user the name of the client that their signing into.

Once the user has signed in (if they didn’t already have an existing session) then they should be redirected the authorise route where the user explicitly gives permission for the client to act on their behalf.

public function action_authorise()
{
    // Retrieve the auth params from the user's session
    $params['client_id'] = Session::get('client_id');
    $params['client_details'] = Session::get('client_details');
    $params['redirect_uri'] = Session::get('redirect_uri');
    $params['response_type'] = Session::get('response_type');
    $params['scopes'] = Session::get('scopes');

    // Check that the auth params are all present
    foreach ($params as $key=>$value) {
        if ($value === null) {
            // Throw an error because an auth param is missing - don't continue any further
        }
    }

    // Get the user ID
    $params['user_id'] = Session::get('user_id');

    // User is not signed in so redirect them to the sign-in route (/oauth/signin)
    if ($params['user_id'] === null) {
        return Redirect::to('signin');
    }

    // Check if the client should be automatically approved
    $autoApprove = ($params['client_details']['auto_approve'] === '1') ? true : false;

    // Process the authorise request if the user's has clicked 'approve' or the client
    if (Input::get('approve') !== null || $autoApprove === true) {

        // Generate an authorization code
        $code = $this->authserver->newAuthoriseRequest('user', $params['user_id'], $params);

        // Redirect the user back to the client with an authorization code
        return Redirect::to(\OAuth2\Util\RedirectUri::make($params['redirect_uri'], array(
            'code'  =>  $code,
            'state' =>  isset($params['state']) ? $params['state'] : ''
        )));
    }

    // If the user has denied the client so redirect them back without an authorization code
    if (Input::get('deny') !== null) {
        return Redirect::to(\OAuth2\Util\RedirectUri::make($params['redirect_uri'], array(
            'error' =>  $this->authserver->exceptionCodes[2],
            'error_message' =>  $this->authserver->errors[$this->authserver->exceptionCodes[2]],
            'state' =>  isset($params['state']) ? $params['state'] : ''
        )));
    }

    // The client shouldn't automatically be approved and the user hasn't yet approved it so show them a form
    return View::make('oauth.authorise', $params);
}

In the authorize form the user should again be told the name of the client and also list all the scopes (permissions) it is requesting.

The final route to create is where the client exchanges the authorization code for an access token.

public function action_access_token()
{
    try {

        // Tell the auth server to issue an access token
        $response = $this->authserver->issueAccessToken();

    } catch (\Oauth2\Exception\ClientException $e) {

        // Throw an exception because there was a problem with a the client's request
        $response = array(
            'error' =>  $this->authserver::getExceptionType($e->getCode()),
            'error_description' => $e->getMessage()
        );

    } catch (Exception $e) {

        // Throw an error when a non-library specific exception has been thrown
        $response = array(
            'error' =>  'undefined_error',
            'error_description' => $e->getMessage()
        );
    }

    header('Content-type: application/json');
    echo json_encode($response);
}

A complete example of an authorisation server can be found at https://github.com/lncd/oauth2-example-auth-server.