Buy

Lock down: Require Authentication Everywhere

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')":

195 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

262 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

347 lines src/AppBundle/Test/ApiTestCase.php
... 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:

347 lines src/AppBundle/Test/ApiTestCase.php
... 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'):

257 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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!

278 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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:

278 lines tests/AppBundle/Controller/Api/ProgrammerControllerTest.php
... 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!

  • 2016-06-13 Vlad

    Thank you, Ryan!

  • 2016-06-11 weaverryan

    Hey Vlad!

    Yes, another set of great questions :).

    1) Your event listener *is* called before your controller is instantiated (i.e. the __construct() is called) and execute (i.e. the action is called)

    2) You have no control over the constructor arguments of your controller. *Unless* you choose to register your controllers as a service. I don't do this, but it's a perfectly valid option: http://symfony.com/doc/current.... If you do *not* register your controller as a service, you also do *not* have access to the container - or any of the shortcuts like $this->get() - from inside __construct(). It's just too early - Symfony hasn't passed you the container yet.

    3) If you *did* register your controller as a service and you wanted the Request object as an argument, you will inject the

    request_stack

    into your service. This will give you the RequestStack object - but you shouldn't instantiate it. Symfony already has this object ready for you - and when you call getCurrentRequest(), it *will* give you the Request :).

    4) You probably already know this, but just in case: the Request object can *also* be an argument to your controller. And, continuing with my previous example, you can also get the request attributes as arguments (I mentioned this earlier, but didn't show the full code):


    // in your listener
    $request->attributes->set('someKey', 'someVal');

    // in any controller - you are now allowed to have a $someKey argument
    public function fooAction($someKey)
    {

    }

    Hope this helps! You're doing some pretty cool/advanced stuff.

    Cheers!

  • 2016-06-10 Vlad

    Hi Ryan,
    Thank you for your reply.

    A few more questions.
    How do I add additional parameters to the controller's constructor?

    I'd also like to know whether the Request object is available in the constructor of the controller. I have put together a quick test and it looks to me that an event subscriber gets called prior to the constructor of the controller, and does have the Request object, yet the controller's constructor ain't got it. I am getting the Request object via the RequestStack in the constructor:

    $requestStack = new RequestStack();
    $request = $requestStack->getCurrentRequest();

    and it appears to be NULL.

    Could you please explain the order of execution, and at which point the Request object becomes available. I understand it is not available all the time.

    Thank you!

  • 2016-06-10 weaverryan

    Great work Vlad!

    You have a few options on where to store it. You can definitely create a service and store them there. Or, there is one place on the Request object that is meant for storing "extra" stuff - the Request attributes:


    $request->attributes->set('someKey', 'someVal');

    Then, obviously, you can fetch those from the request in a controller. As an added benefit, if you put a key into the attributes, you can actually also have it as an argument to your controller. So, in this example, any controller could now have a `$someKey` argument. We talk about that here: http://knpuniversity.com/scree...

    Let me know how it goes!

  • 2016-06-10 Vlad

    Thank you, Ryan!

    I have used an event subscriber/listener, like you've suggested, with Kernel::REQUEST listener, and that certainly does the trick!
    That's exactly what I'm trying to do: to initialize some properties on a controller that would be used by its actions.
    I'm now able to extract the info I need from a request.

    My next question is, where would I store these extracted properties, so they can be accessed by controller(s)? Is there a place I could store them, or do I need to create some sort of a service for that?

    Thank you again for your great help.

  • 2016-06-09 weaverryan

    Hi Vlad!

    GREAT question. There is no preAction method in Symfony - this stuff is typically done with an event listener.

    However, what you need depends on what you're trying to accomplish. A lot of times, people want a "preAction" type of setup because they want to do some work and initialize some properties on the controller that are used by many/all different actions. For example, suppose you need to read a query parameter in all of your actions and then use it to calculate something. Instead of putting this logic in a "preAction" and setting the final result on a property, I usually create a private function that does this work and returns the value. Then, I just call this method whenever I need that work done. This also prevents that "work" from being done unnecessarily, in case there are some actions that don't need that value.

    But, you may also have a totally different situation - let me know! In the REST world, sometimes there *are* things that you just want automatically done before the controller, and so creating a listener might be the right way to go. FOSRestBundle, for example, comes with several listeners that run before the controller and prepare some things from the request.

    Cheers!

  • 2016-06-09 Vlad

    Hi Ryan,
    Is there a way to capture a request and do something with it (e.g. extract some values from it) before it gets to an action?
    Is there a method that executes prior to each action? Perhaps a constructor in a Controller, or some pre-action method?
    Thank you!