This tutorial has a new version, check it out!

Lock down: Require Authentication Everywhere

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

The only endpoint that requires authentication is newAction(). But to use our API, we want to require authentication to use any endpoint related to programmers.

Using @Security

Ok, just add $this->denyAccessUnlessGranted() to every method. OR, use a cool trick from SensioFrameworkExtraBundle. Give the controller class a doc-block and a new annotation: @Security. Auto-complete that to get the use statement. Then, add "is_granted('ROLE_USER')":

... lines 1 - 12
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
... lines 14 - 19
/**
* @Security("is_granted('ROLE_USER')")
*/
class ProgrammerController extends BaseController
... lines 24 - 195

Now we're requiring a valid user on every endpoint.

Re-run all of the programmer tests by pointing to the file.

./vendor/bin/phpunit tests/AppBundle/Controller/Api/ProgrammerControllerTest.php

We should see a lot of failures. Fail, fail, fail, fail! Don't take it personally. We're not sending an Authorization header yet in most tests.

Sending the Authorization Header Everywhere

Let's fix that with as little work as possible. Copy the $token = code and delete it:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
... lines 18 - 23
$token = $this->getService('lexik_jwt_authentication.encoder')
->encode(['username' => 'weaverryan']);
... lines 26 - 40
}
... lines 42 - 260
}

Click into ApiTestCase and add a new protected function called getAuthorizedHeaders() with two arguments: a $username and an optional array of other $headers you want to send on the request:

... lines 1 - 20
class ApiTestCase extends KernelTestCase
{
... lines 23 - 281
protected function getAuthorizedHeaders($username, $headers = array())
{
... lines 284 - 289
}
... lines 291 - 345
}

Paste the $token = code here and add a new Authorization header that's equal to Bearer and then the token. Return the entire array of headers:

... lines 1 - 20
class ApiTestCase extends KernelTestCase
{
... lines 23 - 281
protected function getAuthorizedHeaders($username, $headers = array())
{
$token = $this->getService('lexik_jwt_authentication.encoder')
->encode(['username' => $username]);
$headers['Authorization'] = 'Bearer '.$token;
return $headers;
}
... lines 291 - 345
}

Now, copy the method name. Oh, and don't forget to actually use the $username argument! In ProgrammerControllerTest, add a headers key set to $this->getAuthorizedHeaders('weaverryan'):

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
... lines 18 - 23
// 1) Create a programmer resource
$response = $this->client->post('/api/programmers', [
... line 26
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 29 - 35
}
... lines 37 - 255
}

And we just need to repeat this on every single method inside of this test. I'll look for $this->client to find these... and do it as fast as I can!

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 15
public function testPOSTProgrammerWorks()
{
... lines 18 - 24
$response = $this->client->post('/api/programmers', [
... line 26
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 29 - 35
}
public function testGETProgrammer()
{
... lines 40 - 44
$response = $this->client->get('/api/programmers/UnitTester', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 48 - 60
}
public function testGETProgrammerDeep()
{
... lines 65 - 69
$response = $this->client->get('/api/programmers/UnitTester?deep=1', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 73 - 76
}
public function testGETProgrammersCollection()
{
... lines 81 - 89
$response = $this->client->get('/api/programmers', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 93 - 96
}
public function testGETProgrammersCollectionPagination()
{
... lines 101 - 113
$response = $this->client->get('/api/programmers?filter=programmer', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 117 - 129
$response = $this->client->get($nextLink, [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 133 - 141
$response = $this->client->get($lastLink, [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 145 - 153
}
public function testPUTProgrammer()
{
... lines 158 - 168
$response = $this->client->put('/api/programmers/CowboyCoder', [
... line 170
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 173 - 176
}
public function testPATCHProgrammer()
{
... lines 181 - 189
$response = $this->client->patch('/api/programmers/CowboyCoder', [
... line 191
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 194 - 196
}
public function testDELETEProgrammer()
{
... lines 201 - 205
$response = $this->client->delete('/api/programmers/UnitTester', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... line 209
}
public function testValidationErrors()
{
... lines 214 - 219
$response = $this->client->post('/api/programmers', [
... line 221
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 224 - 234
}
public function testInvalidJson()
{
... lines 239 - 246
$response = $this->client->post('/api/programmers', [
... line 248
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 251 - 253
}
public function test404Exception()
{
$response = $this->client->get('/api/programmers/fake', [
'headers' => $this->getAuthorizedHeaders('weaverryan')
]);
... lines 261 - 266
}
... lines 268 - 276
}

By hooking into Guzzle, we could add the Authorization header to every request automatically... but there might be some requests where we do not want this header.

In fact, at the bottom, we actually test what happens when we don’t send the Authorization header. Skip adding the header here:

... lines 1 - 6
class ProgrammerControllerTest extends ApiTestCase
{
... lines 9 - 268
public function testRequiresAuthentication()
{
$response = $this->client->post('/api/programmers', [
'body' => '[]'
// do not send auth!
]);
$this->assertEquals(401, $response->getStatusCode());
}
}

With any luck, we should get a bunch of beautiful passes.

./vendor/bin/phpunit tests/AppBundle/Controller/Api/ProgrammerControllerTest.php

And we do! Ooh, until we hit the last test! When we don't send an Authorization header to an endpoint that requires authentication... it's still returning a 200 status code instead of 401. When we kick out non-authenticated API requests, they are still being redirected to the login page... which is clearly not a cool way for an API to behave.

Time to fix that.

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
    }
}