If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe only endpoint that requires authentication is newAction()
. But to use our API, we want to require authentication to use any endpoint related to programmers.
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.
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.
Hey Robert!
Good questions :). There are two approaches to handling authentication with a JS frontend:
1) You could just use cookies. This is not as trendy as tokens, but it's dead simple. Basically, it's how you've been doing AJAX for years. If you create a login system (with username, password), then your users browser will have a traditional session cookie (I believe you'll need to have the page refresh to set the session cookie, or set it manually if everything is done via AJAX). But once your session cookie is set... you're good! Your session cookie acts a lot like a token, it's automatically send on every request and authenticates your AJAX calls. This is a dead-simple approach. It's drawbacks are that other entities (e.g. iPhone app) can't use this way of authenticating to use your API and your JavaScript doesn't know anything about the user (i.e. if you want to know the users name or email in JavaScript, you need to store that somewhere manually - it's kind of "separate" from authentication).
2) You could use tokens (I recommend JWT)! You have the flow for this basically correct. When you originally authenticate, the response back to your user will include the token (similar to how we do it in this tutorial - so yes, the token would be created in an authenticator and then returned on the successful JSON response). You'll then store this and attach it (e.g. as a header) on each subsequent request made by your API. Store the token in an HTTPS-only cookie to avoid XSS attacks. How to attach it on every request? It depends on what you're using for AJAX - most have a way to globally attach a header value to *all* requests.
Like with many things in life, the token approach is a bit more professional... but a bit more complex. For expiration, you can either set an expiration time... but then you need to require your user to log back in (or do something with refresh tokens, which can be complex). If you make your token last forever... then you'll need to add some storage on the backend with a list of tokens that you have created (e.g. you could give each JWT a unique id, then have a table of these) so that when the user logs out (or you think they should be logged out, for any reason), you can set that token as expired. You'll check this whenever any request is made to your backend.
And yep, *always* use HTTPS (and store the token in an HTTPs-only cookie). In any authentication system, there is ultimately a key/token... and if this is stolen... well... all bets are off :). But if you're transferring over HTTPs and storing the cookie in the HTTPS only cookie, you've done your duty to protect the user from this.
I hope this helps! GREAT questions! Btw, you might like this article: https://stormpath.com/blog/...
Cheers!
Hi Ryan,
First of all thank you for such detailed answer.
I think I will go with JWT for two reasons: I want to learn how to use them and later on I will expose my api outside application so I will have to use some kind of token authentication anyway.
Just one thing to clarify: So I should attach header on JS side not on Symfony Side?
Thank you again. I will go through more material and provided article of course.
You rock!
Cheers!
Hey Robert!
> Just one thing to clarify: So I should attach header on JS side not on Symfony Side?
Yes, on the JS side. When you originally make the request to login (with hard credentials), your Symfony API will return the token (probably in JSON, like a normal endpoint). Then, on each request your JS makes to your API after, your JS will attach the token on a header. You will then *read* that header in Symfony (i.e. in your authenticator).
Good luck and cheers!
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!
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!
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.
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/screencast/symfony-journey/magically-add-controller-args
Let me know how it goes!
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!
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/cookbook/controller/service.html. 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!
// 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
}
}
Hi,
First of all sorry if I posted this question in wrong part of the tutorial.
I just wanted to know if I understand it all correctly. So my questions are:
1. How do I secure SPA (Single Page Application).
2. Do I see the flow correctly?
3. Security?
1. Say I have API backend ready for my app. For example I can go to /api/posts and add new post. Now I want to build new VueJS component which will send data over post to this end point. Once I have my end point how do I secure it? I know how to attach token in tests(I even have my own testing tools!) but how do I attach it on every request? Should I attach it on every request?
Should I create this new token for example in guard login form auth?
2. The way I see the whole thing for now:
- I log into the system with hard credentials (login, password)
- After that I create web token right away and attach it to every request
- if it expires and I'm still logged in I will need to refresh it
- I store token somewhere?
3. If I see this correctly as long as I don't provide very sensitive informations, make sure that it uses HTTPS and token is not stolen I can assume I'm safe?
Thank you in advance.
Hope I didn't miss anything obvious.
Best Regards,
Robert