Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

The "Fetch a Token" Endpoint Test

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.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

How does the Client get a Token?

So, there are two parts to the process:

  1. How the client gets a token
  2. How a client uses that token.

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.

The new Token Resource

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!

Leave a comment!

12
Login or Register to join the conversation
Andrei V. Avatar
Andrei V. Avatar Andrei V. | posted 4 years ago

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!

Reply

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!

1 Reply
Andrei V. Avatar

This is an absolutely amazing answer)
Thank you!

Reply
Default user avatar
Default user avatar Howard Ng | posted 5 years ago

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?

Reply
Default user avatar

May be something wrong with php 7.2 ... ... let me test in other platform

Reply

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!

Reply

Hey Howard,

Hm, try to dump the variable which is passed to count() before this line. What type is it of?

Cheers!

Reply
Default user avatar
Default user avatar Zuhayer Tahir | posted 5 years ago

How to handle Add User API with Symfony form where our builder has a password field:
->add('plainPassword', RepeatedType::class, array(
'type' => PasswordType::class,....

Reply

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!

1 Reply
Default user avatar
Default user avatar Zuhayer Tahir | weaverryan | posted 5 years ago

Thanks (y)

Reply
Default user avatar
Default user avatar Gehad Mohamed | posted 5 years ago

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.

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of API tokens & JWT are still valid, but integration in newer Symfony versions may be different.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice