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 SubscribeAlmost every API authentication system - whether you're using JWT, OAuth or something different - works basically the same. Somehow, your API client gets an access token. And once it does that, it attaches it to all future requests to prove who it is and that it has access to perform some action.
So, there are two parts to the process:
And actually, the first part is a lot more interesting because there are a bunch of strategies for how a client should obtain a token. For example, you could create an endpoint where the client submits their username and password in exchange for a token. Or, you can do something more complex: like use the OAuth flow. This is a good idea when you have third-party clients - like an iPhone app - that need to gain access to your server on behalf of some user. Or, you could use both strategies - GitHub lets you do that.
But the end result is always the same: the client gets a token. We're going to build the first idea: a simple endpoint where the client can submit a username and password to get back a token. That's something that will work for most APIs.
Everything we've built so far has been centered around the Programmer resource. Now, we'll be sending back tokens: and you can think of a Token as our second API resource: the client will be able to create new tokens, and potentially, we could allow them to delete tokens.
As always, we'll start with the test. Create a new class called TokenControllerTest
. Make it extend the handy ApiTestCase
that we've been working on. Add public function testPOSTCreateToken()
:
... lines 1 - 2 | |
namespace Tests\AppBundle\Controller\Api; | |
use AppBundle\Test\ApiTestCase; | |
class TokenControllerTest extends ApiTestCase | |
{ | |
public function testPOSTCreateToken() | |
{ | |
... lines 11 - 20 | |
} | |
} |
Ok, let's think about this. First, we're going to need a user in the database before we start. To create one, add $this->createUser()
with weaverryan
and the super-secure and realistic password I<3Pizza
:
... lines 1 - 6 | |
class TokenControllerTest extends ApiTestCase | |
{ | |
public function testPOSTCreateToken() | |
{ | |
$this->createUser('weaverryan', 'I<3Pizza'); | |
... lines 12 - 20 | |
} | |
} |
Next, make the POST request: $response = $this->client->post()
to /api/tokens
:
... lines 1 - 10 | |
$this->createUser('weaverryan', 'I<3Pizza'); | |
$response = $this->client->post('/api/tokens', [ | |
... line 14 | |
]); | |
... lines 16 - 23 |
That URL could be anything, but the most important thing is that it's consistent with the /api/programmers
we already have.
The last thing we need to do is send the username and password. And really, you can do this however you want. But, why not take advantage of the classic HTTP Basic Authentication. To send an HTTP Basic username and password with Guzzle, add an auth
option and set it to an array containing the username and password:
... lines 1 - 10 | |
$this->createUser('weaverryan', 'I<3Pizza'); | |
$response = $this->client->post('/api/tokens', [ | |
'auth' => ['weaverryan', 'I<3Pizza'] | |
]); | |
... lines 16 - 23 |
And hey, reminder time! On production, you will make your API work over HTTPS. The last thing we want is plain-text password flying all over the interwebs.
Below, assert that we get back a 200 status code, or you could use 201 - since technically a resource is being created:
... lines 1 - 10 | |
$this->createUser('weaverryan', 'I<3Pizza'); | |
$response = $this->client->post('/api/tokens', [ | |
'auth' => ['weaverryan', 'I<3Pizza'] | |
]); | |
$this->assertEquals(200, $response->getStatusCode()); | |
... lines 17 - 23 |
Now, what should the response look like? Well, it should be a token resource... which is really just a string. Use the asserter to assert that the JSON at least contains a token
property - we don't know exactly what its value will be:
... lines 1 - 10 | |
$this->createUser('weaverryan', 'I<3Pizza'); | |
$response = $this->client->post('/api/tokens', [ | |
'auth' => ['weaverryan', 'I<3Pizza'] | |
]); | |
$this->assertEquals(200, $response->getStatusCode()); | |
$this->asserter()->assertResponsePropertyExists( | |
$response, | |
'token' | |
); | |
... lines 21 - 23 |
Looks cool! Copy the method name and run only this test:
./vendor/bin/phpunit --filter testPOSTCreateToken
This should fail... and it does! A 404 not found. Time to bring this to life!
Hey Andrei V.!
Sorry for my super late reply - I lost track of your message (totally my fault!). These are great (hard) questions!
> Assuming that we use knpu_oauth2_client and created an appropriate authenticator
There are kinda 2 options here. First, if you use the flow you've described (where the user's browser is actually redirected), then the only way to "stay" logged in after finishing redirecting would be to use stateful authentication (i.e. cookies). If you did this, then there is really no point to using JWT - as your JavaScript could make XHR requests without doing anything else - the session cookie would authenticate them. Or, if you really want a JWT for some reason, you could, in your JWT creation endpoint, check to see if the user is authenticated (they will be, due to the session cookie) and return a JWT for the authenticated user.
If you want a JavaScript approach that does *not* involve redirecting the user's browser, then you'll need to use the JavaScript OAuth flow (i.e. not using the KnpUOAuth2ClientBundle) - this is the flow where, usually, the user clicks a button, a popup comes up (e.g. to Facebook), then after they auth, it closes, and the access token is sent back to your JavaScript. If you had this setup, you'd then make an AJAX request from your JavaScript to your app where you send the access_token. Your server would then use that access_token to fetch user data from the API of that service (e.g. API), authenticate the user (and maybe create a new user record in the database), and send back a JWT. So, it would be similar to our user/pass auth, except that you're sending up an access_token instead.
> The other question is a mechanism of authenticating users from mobile apps
So, this is a bit interesting. If YOU are the person creating the mobile app (meaning, you own the API and the mobile app), you don't need OAuth: you can simply have the user enter the email/pass into your mobile app, you forward that to your API and it sends back a JWT. An OAuth server is more necessary if some *other* entity is creating a mobile app and wants to be able to take actions in their app to *your* API on behalf of some user. For example, suppose "Ryan" uses the iPhone app "DoCoolStuff" that's created by someone else. One of the features of the DoCoolStuff app is that it will automate some tasks for my account on YOUR site. In order to do this, the DoCoolStuff app needs to become authenticated as "Ryan" for your API. Making Ryan type his password directly into DoCoolStuff so that it an send the email/pass to your API is a problem, because it forces Ryan to "give" his password to DoCoolStuff (sure, they may say that they're not saving it, but who knows - it's just not a professional flow). Instead, to enable this, your API has an OAuth server so that apps like DoCoolStuff can redirect Ryan through the OAuth flow so that he never needs to enter his password directly into DoCoolStuff.
Phew! About all of this, full disclosure, this stuff is complex and I'm not an app developer - so there may in fact be some best practices I'm missing or misrepresenting. But hopefully this helps you along the way :).
Cheers!
There was 1 error:
1) Tests\AppBundle\Controller\Api\TokenControllerTest::testPOSTCreateToken
count(): Parameter must be an array or an object that implements Countable
Any idea?
Hey Howard,
Hm, probably... but probably you just use a new PHPUnit version which has some BC breaks? What version of PHPUnit do you have? Let us know if you still have this issue.
Cheers!
Hey Howard,
Hm, try to dump the variable which is passed to count() before this line. What type is it of?
Cheers!
How to handle Add User API with Symfony form where our builder has a password field:
->add('plainPassword', RepeatedType::class, array(
'type' => PasswordType::class,....
Hey Zuhayer Tahir!
Ah, good question! I would actually probably split your form into *two* separate forms: one that uses the RepeatedType (for your HTML frontend), and one that does not use the RepeatedType, but simply uses PasswordType (use this one for your API). Basically, you simply don't need the RepeatedType for your API, as it doesn't make sense for your API client to need to send the password to you in two different fields :)
Cheers!
I've added the the new test class but it's not detected by phpunit, also it detect and run the cases in ProgrammerControllerTest.php but when I add the filter testPOSTCreateToken it shows this message "No tests executed!".
Is there is a way to debug this issue finding what's wrong with loading test class.
Hey Gehad Mohamed
Could you show me your test class code ? I believe your method name is in someway wrong or maybe the location where you are storing your test files.
Have you make a change to `phpunit.xml.dist` file ? That file stores the configuration for PHPUnit
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
}
}
User/pass auth works perfectly, when you have normal login form, but how would you suggest to support social login, via facebook, for example from the react app?
Assuming that we use knpu_oauth2_client and created an appropriate authenticator. It works great, but it redirects user in browser and the question is - how to return jwt token to client side properly?
The other question is a mechanism of authenticating users from mobile apps. You mentioned oauth flow, but what exactly do you mean by that? Creating our own oauth provider?
Thanks in advance!