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