OneSheep

Testing OAuth 2.0 protected API endpoints in Laravel

As part of our current project, we are building a web service with a RESTful API. We are using the delightful Laravel PHP framework to drive the service. The requirement is that the API should be consumable by a variety of clients including an HTML5 web app, iPhone app and Android app. So, we are architecting for token based authentication, and protecting our privileged api endpoints with OAuth 2.0

In the process I learned quite a few things that I would like to share here. Let’s talk about setup first and then look at testing.

Setup

One can either go and study the 500 page OAuth 2.0 spec and write your own standards compliant authorization server, or you can stand on the shoulders of giants like Alex Bilbie and Luca Degasperi who did it for you.

Very easy instructions for installation via composer on the package’s github page

For our service we will use the password grant type, so I have this configuration:

// in app/config/packages/lucadegasperi/oauth2-server-laravel/oauth2.php
'grant_types' => array(

    'password' => array(
        'class' => 'LeagueOAuth2ServerGrantPassword',
        'access_token_ttl' => 604800,
        'callback' => function ($username, $password) {
            $credentials = array(
                'email' => $username,
                'password' => $password,
            );

            $valid = Auth::validate($credentials);

            if (!$valid) {
                return false;
            }

            return Auth::getProvider()->retrieveByCredentials($credentials)->id;
        }
    )
  ), ... 

In my app/routes.php I have this:

// issue an access token to a post from a login form
Route::post('oauth/access_token', function() {
    return AuthorizationServer::performAccessTokenFlow();
});

// protect priviledged endpoints
Route::group(array('prefix' => 'api', 'before' => 'oauth'), function() {

    Route::get('users/me', array('uses' => 'UserController@doMe'));
    Route::post('circles/new', array('uses' => 'CircleController@newCircle'));
    ...
});

Testing

Using phpunit, I test my protected endpoints like this:

<?php

class CreateCircleTest extends TestCase
{

    public function setUp()
    {
        parent::setUp();
        $this->prepareTheDatabase();
        Route::enableFilters();
    }

    public function testTheEndpointIsProtected()
    {
        $this->areWeTalkingToStrangersOn('POST', '/api/circles/new');
    }

    public function testMissingParameters()
    {
        $this->checkForMissingParametersOn('POST', '/api/circles/new');
    }

    public function testWellFormedButNotValid()
    {
        $parameters = array('name' => '  '); // empty name
        $this->prepAothServer('POST', $parameters);
        $response = $this->call('POST', '/api/circles/new', $parameters);
        $data = json_decode($response->getContent());

        $this->assertFalse($response->isOk());
        $this->assertResponseStatus(400);  // Bad Request
        $this->assertCount(1, $data->messages); // the client is told why

        /* test that duplicate cicle names for the same user is rejected */
        $duplicateName = "Friends";
        $circle = Circle::newFrom($duplicateName, 1);

        $parameters = array('name' => $duplicateName);
        $this->prepAothServer('POST', $parameters);
        $response = $this->call('POST', '/api/circles/new', $parameters);
        $data = json_decode($response->getContent());

        $this->assertFalse($response->isOk());
        $this->assertResponseStatus(400);  // Bad Request
        $this->assertCount(1, $data->messages); // the client is told why
    }

    public function testAddValidNewCircle()
    {
        $newCircleName = 'Buddies';
        $parameters = array('name' => $newCircleName);
        $this->prepAothServer('POST', $parameters);
        $response = $this->call('POST', '/api/circles/new', $parameters);
        $data = json_decode($response->getContent());
        
        /* test the response */
        $this->assertTrue($response->isOk());
        $this->assertResponseStatus(200);
        $this->assertObjectHasAttribute('id', $data);
        $this->assertEquals($newCircleName, $data->name);

        /* test if the circle was actually added */
        $owner = Circle::getCircleOwner($data->id);
        $this->assertEquals(1, $owner);
    }

}

/* end of file app/tests/CreateCircleTest.php */

So nothing very different from a normal unit test except for the highlighted four methods that we inherit from our parent TestCase class. Let’s look at them now.

Preparing a test fixture

To set up and seed the test database (I use an in-memory sqlite database for testing) before every test, I call prepareTheDatabase() on the parent class in the setUp() function. It looks like this:

protected function prepareTheDatabase()
{
    Artisan::call("migrate");

    /* vendor migrations */

    $packages = array(
        "lucadegasperi/oauth2-server-laravel",
    );

    foreach ($packages as $packageName)
    {
        Artisan::call("migrate",
                array("--package" => $packageName, "--env" => "testing"));
    }

    /* do seeding */

    $seeders = array(
        "OAuthTestSeeder",
    );

    foreach ($seeders as $seedClass)
    {
        Artisan::call("db:seed", array("--class" => $seedClass));
    }
}

Preparing the OAuth server

Before each call to a protected endpoint, we must prepare the OAuth server with a call to prepAothServer() The function looks like this:

protected function prepAothServer($verb, &$parameters)
{
    /* sign the request */
    $parameters['access_token'] = OAuthTestSeeder::ACCESS_TOKEN;

    $request = MockRequest::newRequest($verb, $parameters);
    ResourceServer::setRequest($request);
}

So, with these tests in place we can start building our protected endpoint till we go green.

For full details showing my implementation of my OAuthTestSeeder, MockRequest etc. you can check out this Gist


Posted on Oct 02, 2014 by Jannie Theunissen

Back to all posts