This tutorial has a new version, check it out!

Start Securing the App!

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

You again? Get outta here.... punk... is what we will be saying soon to API clients in this tutorial that don't have valid credentials! Yep, welcome back guys, this time to a tutorial that's making security exciting again! Seriously, I'm pumped to talk about authentication in an API... and in particular, a really powerful tool called JSON web tokens.

To make sure your JSON web tokens are the envy of all your friends, code along with me by downloading the code from any of the tutorial pages. Then, just unzip it and move into the start/ directory. I already have that start code in symfony-rest.

I also upgraded our project to Symfony 3! Woohoo! Almost everything we'll do will work for Symfony 2 or 3, but there are a few differences in the directory structure. We have a tutorial on upgrading to Symfony 3 if you want to see those.

Let's start the built-in web server with:

bin/console server:run

And if you just downloaded the code, open the README and follow a few other steps there.

The (sad) State of our App's Security

Ok, our app is Code Battles! It has a cool web interface and you can login with weaverryan and password foo: super secure! Here, we can create programmers and start battles. And our API already supports a lot of this stuff.

Open up ProgrammerController inside the Controller/Api directory:

... lines 1 - 18
class ProgrammerController extends BaseController
{
... lines 21 - 24
public function newAction(Request $request)
... lines 26 - 54
public function showAction($nickname)
... lines 56 - 76
public function listAction(Request $request)
... lines 78 - 95
public function updateAction($nickname, Request $request)
... lines 97 - 128
public function deleteAction($nickname)
... lines 130 - 189
}

Awesome! We can already create, fetch and update programmers. AND, we've got a pretty sweet test:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOST()
... lines 17 - 36
public function testGETProgrammer()
... lines 38 - 59
public function testGETProgrammerDeep()
... lines 61 - 73
public function testGETProgrammersCollection()
... lines 75 - 91
public function testGETProgrammersCollectionPagination()
... lines 93 - 142
public function testPUTProgrammer()
... lines 144 - 164
public function testPATCHProgrammer()
... lines 166 - 183
public function testDELETEProgrammer()
... lines 185 - 246
}

um, suite... that checks these endpoints.

Ready for the problem? Our API has no security! The horror! Anonymous users are able to create programmers and then change the avatar on other programmers. It's chaos!

On the web interface, you need to be logged in to do any of these things. Let's make the API work the same way.

Testing for Security

As always: we need to start by writing a test. In ProgrammerControllerTest, add a new public function testRequiresAuthentication():

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 247
public function testRequiresAuthentication()
{
... lines 250 - 253
}
}

Let's make an API request to an endpoint that should be secured and then assert some things. Start with $response = $this->client->post('/api/programmers'). Send this a valid JSON body:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 247
public function testRequiresAuthentication()
{
$response = $this->client->post('/api/programmers', [
'body' => '[]'
]);
... line 253
}
}

Ok, if our API client tries to anonymously access a secured endpoint, what should be returned? Well, at the very least, assert that the response status code is 401, meaning "Unauthorized":

... lines 1 - 249
$response = $this->client->post('/api/programmers', [
'body' => '[]'
]);
$this->assertEquals(401, $response->getStatusCode());
... lines 254 - 256

Ok! Let's go make sure this fails! Copy the method name and find the terminal. Run:

./vendor/bin/phpunit --filter testRequiresAuthentication

It fails with a validation error: it is getting beyond the security layer and executing our controller. Time to lock that down!

Securing a Controller

Open ProgrammerController. How can we require the API client to be authenticated? The exact same way you do in a web application. Add $this->denyAccessUnlessGranted('ROLE_USER'):

... lines 1 - 18
class ProgrammerController extends BaseController
{
... lines 21 - 24
public function newAction(Request $request)
{
$this->denyAccessUnlessGranted('ROLE_USER');
... lines 28 - 50
}
... lines 52 - 191
}

That's it. I'm using ROLE_USER because all of my users have this role - you could also use IS_AUTHENTICATED_FULLY.

Ok, back to the test! Run it!

./vendor/bin/phpunit --filter testRequiresAuthentication

Oh, interesting - it's a 200 status code instead of 401. Look closely: it redirected us to the login page. So, it's kind of working... you can't add programmers anonymously anymore. But clearly, we've got some work to do.

Leave a comment!

This tutorial uses an older version of Symfony. The concepts of API tokens & JWT are still valid, but integration in newer Symfony versions may be different.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.0.*", // v3.0.3
        "doctrine/orm": "^2.5", // v2.5.4
        "doctrine/doctrine-bundle": "^1.6", // 1.6.2
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // v2.10.0
        "sensio/distribution-bundle": "^5.0", // v5.0.4
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.2
        "jms/serializer-bundle": "^1.1.0", // 1.1.0
        "white-october/pagerfanta-bundle": "^1.0", // v1.0.5
        "lexik/jwt-authentication-bundle": "^1.4" // v1.4.3
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.6
        "symfony/phpunit-bridge": "^3.0", // v3.0.3
        "behat/behat": "~3.1@dev", // dev-master
        "behat/mink-extension": "~2.2.0", // v2.2
        "behat/mink-goutte-driver": "~1.2.0", // v1.2.1
        "behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
        "phpunit/phpunit": "~4.6.0", // 4.6.10
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}