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 SubscribeYou 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.
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.
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!
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.
Hey @alex
Mainly everything will be the same, but of course there are some changes in configuration, class and variable names Lixik JWT has good UPGRADE documents which show differences between versions, And you can follow them to build everything in right way!
Cheers!
I have downloaded the course code and done the setup on PHP 7.1 (It doesn't work on 7.2) . I'm running into a weird issue. When I try to run the tests with the filter, it says so tests executed. If I run tests without the filter, it says everything passed, but I'm not confident that it's actually running the tests in ProgrammerTestController because I've put in die statements or actual parsing errors, and the tests pass without a filter. I've made no modifications to the code other than to put in the very first test, which should fail.
I'm stumped on this one. Any thoughts?
Yay! I figured it out. The tests moved since the last tutorial. The are in /tests now, not the ones in /src/AppBundle/Tests . I strongly suspect the people below had the same issue. Maybe you all should remove the copy from the old location?
Hey Amy anuszewski
Sorry for the confusion and thanks for sharing your findings :)
We will discuss about your suggestion because we really want to avoid this kind of problems to others
Cheers!
Architecture question!
In a traditional Symfony web app a user would click on a link, a request is sent to the server, and then the server sends back the html for the browser to display.
With a REST application it seems there needs to be (at least) 2 end points for any page that has data from a database. Here is my flow.
1. User sends request i.e. /app
2. Server sends back HTLML for page
3. Data is required so a javascript file calls an end point to retrieve this i.e. /app/api/posts
4. Javasctipt receives JSON and adds the data to the page.
Have i got this right or am i missing something?
Loving this course BTW!
Hey Shaun T.!
Architecture! Woo! :)
The answer to your question is.... sort of? But kinda no.... ;). Let me explain. There are 3 types of applications:
1) 100% traditional HTML web apps: the user's browser sends a request to /app & gets back HTML. There is NO AJAX, every page is a full refresh This is 0% API
2) 100% single-page application. In this model, there is literally 1 HTML file that the user gets when they access "/", and this is probably mostly an empty page with a div on it and some JavaScript. Then, 100% of all other requests are AJAX/API requests, which are used to build everything for your application.
3) A mixture :). And this is still the most common (though single-page apps, SPAs, are becoming more and more common). This site is a good example of a "mixed" app. When your browser makes a request to "/", you get a traditional HTML page, and there is no AJAX. But, if you go to, for example, a challenge page (https://knpuniversity.com/screencast/php-namespaces-in-120-seconds/namespaces/activity/307), this returns HTML... but the original HTML is actually mostly empty. We then make several AJAX requests in order to build the page. Whenever you change a file in the editor, more AJAX/API requests are made.
So yes, there is ALWAYS at least one HTML response. But, once that HTML responses is received, you could build a JavaScript app that makes many API requests. Or, (and I think this really gets to your question) if you really only need to print some data on your page, you can just use the data to print all of the HMTL you need and return it. In that case, there is no need to make any API request: your page is fully loaded and ready to go. Heck, even if you DO want to get some JSON data in order to use with JavaScript, you can (if you want) avoid an AJAX request by making that data available inside your HTML - e.g. https://knpuniversity.com/screencast/javascript/data-attribute, or simply by setting your data to a global variable, like:
<script>
// this will print as a JavaScript object, which you can then read from JavaScript
window.SOME_GLOBAL_DATA = {{ someDataArray|json|raw }};
</script>
<script src="{{ asset('path/to/some.js') }}"></script>
I hope that helps!
Thanks for such a detailed explanation weaverryan , this has made the subject loads clearer for me, I really appreciate it!
// 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
}
}
Hey! Is the lexik jwt implementation done in a different way between Symfony 3 and 4? I'm building my api with Symfony 4 and i'm not sure if some things should be done differently.