Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

Creating Token Resources in the API

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 $12.00

Creating Token Resources in the API

Our API clients can send a token with their request to become authenticated. But how are they supposed to get that token in the first place?

Actually, this is already possible via the web interface. First, let me delete our SQLite database, which will reset our users. Now, we can login as ryan@knplabs.com password foo. If you go to the url /tokens, you see I have a little interface here. I can add a token, put a message, click tokenify-me and there we go, I’ve got a shiny new token!

And this is something we can use right now in an API request to authenticate as the ryan@knplabs user. This is great and might actually be enough for you. But for me, I also want a way to do this through my API. I want to make a request through an endpoint that says “give me a new API token.”

As always, let’s start with creating a new Behat Scenario. In fact this is a whole new feature! Create a new behat feature file and call it token.feature Let me get my necktie on here and start describing the business value for this. In this case it’s pretty easy: you need to get tokens so you can use our API.

# features/api/token.feature
Feature: Token
  In order to access restricted information
  As an API client
  I can create tokens and use them to access information

Perfect.

Next, let’s put our first scenario here, which is going to be the working scenario for creating a token. Even though a token relates to security it’s really no different than any other resource we’re creating, like a programmer resource. So the scenario for this is going to look really similar. The only difference is that we can’t authenticate with an API token, because that’s what we are trying to get. So instead we’re going to authenticate with HTTP Basic. First, let’s make sure there is a user in the database with a certain password. And just like you saw with the web interface, every token has a note describing how it’s being used. So in our request body, we’ll send JSON with a little note.

# features/api/token.feature
Feature: Token
  # ...

  Scenario: Creating a token
    Given there is a user "weaverryan" with password "test"
    And I have the payload:
      """
      {
        "notes": "A testing token!"
      }
      """

Great! To send HTTP basic headers with my request I have a built in line for this. Then after that, we can make a request just like normal:

# features/api/token.feature
Feature: Token
  # ...

  Scenario: Creating a token
    Given there is a user "weaverryan" with password "test"
    And I have the payload:
      """
      {
        "notes": "A testing token!"
      }
      """
    And I authenticate with user "weaverryan" and password "test"
    When I request "POST /api/tokens"

I’m making up this URL but you can see I’m being consistent because we also have /api/programmers.

After this, it’s just like our programmer endpoint: we know the status code should be 201, that there should be a Location header and we’ll expect that the token resource is going to be returned to us and that it will have a key called token, which is the newly generated string:

# features/api/token.feature
Feature: Token
  # ...

  Scenario: Creating a token
    Given there is a user "weaverryan" with password "test"
    And I have the payload:
      """
      {
        "notes": "A testing token!"
      }
      """
    And I authenticate with user "weaverryan" and password "test"
    When I request "POST /api/tokens"
    Then the response status code should be 201
    And the "Location" header should exist
    And the "token" property should be a string

Awesome!

So let’s try this: we know it’s going to fail but we want to confirm that:

php vendor/bin/behat features/api/token.feature

Failure, sweet! And it does with a 404 because we don’t have an endpoint for this.

Creating the TokenController

To get this working, I’m going to create an entirely new controller class and make it look a bit like my ProgrammerController. Make it extend the BaseController class which we’ve been adding more helper methods into. Notice that I did just add a use statement for that:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
namespace KnpU\CodeBattle\Controller\Api;

use KnpU\CodeBattle\Controller\BaseController;

class TokenController extends BaseController
{
}

And it expects us to have one method called addRoutes. This is special to my implementation of Silex, but you’ll remember that we have this at the top of ProgrammerController and that’s just where we build all of our endpoints. We can do the same things here. We’ll add a new POST endpoint for /api/tokens that will execute a method called newAction when we hit it:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

class TokenController extends BaseController
{
    protected function addRoutes(ControllerCollection $controllers)
    {
        $controllers->post('/api/tokens', array($this, 'newAction'));
    }

    public function newAction()
    {
        return 'foo';
    }
}

So let’s go back and rerun the tests. Look at that, it is working. The test still fails, but instead of a 404, we see a 200 status code because we’re returning foo.

Creating the Token Resource

So let’s do as little work as possible to get this going. The first thing to know is that we do have a token table. And just like with our other tables like the programmer table where we have a Programmer class, I’ve also created an ApiToken class:

// src/KnpU/CodeBattle/Security/Token/ApiToken.php
namespace KnpU\CodeBattle\Security\Token;

use Symfony\Component\Validator\Constraints as Assert;

class ApiToken
{
    public $id;

    public $token;

    /**
     * @Assert\NotBlank(message="Please add some notes about this token")
     */
    public $notes;

    public $userId;

    public $createdAt;

    public function __construct($userId)
    {
        $this->userId = $userId;
        $this->createdAt = new \DateTime();
        $this->token = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
    }
}

If we can create this new ApiToken object, then we can use some ORM-magic I setup to save a new row to that table.

So let’s start doing that: $token = new ApiToken();. I’ll add the use statement for that:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

public function newAction()
{
    $token = new ApiToken();
}

You can see immediately it’s angry with me because I need to pass the userId to the constructor. Now, what is the id of the current user? Remember, in our scenario, we’re passing HTTP Basic authentication. So here we need to grab the HTTP Basic username and look that up in the database. I’m not going to worry about checking the password yet, we’ll do that in a second.

In Silex, whenever you need request information you can just type hint a $request variable in your controller and it will be passed in. Am I sounding like a broken record yet? Don’t forget your use statement!

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

use Symfony\Component\HttpFoundation\Request;
// ...

public function newAction(Request $request)
{
    $token = new ApiToken();
}

You may or may not remember this - I had to look it up - but if you want to get the HTTP Basic username that’s sent with the request, you can say $request->headers->get('PHP_AUTH_USER'). Oops don’t forget your equals sign. Next I’ll look this up in our user table. For now we’ll just assume it exists: I’m living on the edge by not doing any error handling. And then, we’re going to say $user->id:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

use Symfony\Component\HttpFoundation\Request;
// ...

public function newAction(Request $request)
{
    $username = $request->headers->get('PHP_AUTH_USER');
    $user = $this->getUserRepository()->findUserByUsername($username);

    $token = new ApiToken($user->id);
}

Perfect!

Decoding the Request Body

Next, we need to set the notes. In our scenario we’re sending a JSON body with a notes field. So here, what we can do is just grab that from the request. We did this before in Episode 1: $request->getContent() gets us the raw JSON and json_decode will return an array. So, we’ll get the notes key off of that. And that’s really it! All we need to do now is save the token object, which I’ll do with my simple ORM system:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

public function newAction(Request $request)
{
    $username = $request->headers->get('PHP_AUTH_USER');

    $user = $this->getUserRepository()->findUserByUsername($username);

    $data = json_decode($request->getContent(), true);

    $token = new ApiToken($user->id);
    $token->notes = $data['notes'];

    $this->getApiTokenRepository()->save($token);
}

Now, we need to return our normal API response. Remember we’re using the Serializer at this point and in the last couple of chapters we created a nice new function in our BaseController called createApiResponse. All we need to do is pass it the object we want to serialize and the status code - 201 here - and that’s going to build and return the response for us:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

public function newAction(Request $request)
{
    // ...

    $this->getApiTokenRepository()->save($token);

    return $this->createApiResponse($token, 201);
}

That’s as simple as Jean-Luc Picard sending the Enterprise into warp! Engage.

Head over to the terminal:

php vendor/bin/behat features/api/token.feature

Awesome...ish! So it’s failing because we don’t have a Location header set, but if you look at what’s being returned from the endpoint, you can tell it’s actually working and inserting this in the database. We’re missing the Location header and we should have it, but for now I’m just going to comment that line out. I don’t want to take the time to build the endpoint to view a single token. I’ll let you handle that:

# features/api/token.feature
# ...

Scenario: Creating a token
  # ...
  When I request "POST /api/tokens"
  Then the response status code should be 201
  # And the "Location" header should exist
  And the "token" property should be a string

Let’s run the test. Perfect it passes!

Testing for a Bad HTTP Basic Password

Since we’re not checking to see if the password is valid, let’s add another scenario for that. We can copy most of the working scenario but we’ll change a couple of things. Instead of the right password we’ll send something different. And instead of 201 this time it’s going to be a 401:

# features/api/token.feature
# ...

Scenario: Creating a token with a bad password
  Given there is a user "weaverryan" with password "test"
  And I have the payload:
    """
    {
      "notes": "A testing token!"
    }
    """
  And I authenticate with user "weaverryan" and password "WRONG"
  When I request "POST /api/tokens"
  Then the response status code should be 401

Remember whenever we have an error response, we are always returning that API Problem format. Great! So let’s run just this one scenario which starts on line 21. And again, we’re expecting it to fail, but I like to see my failures before I actually do the code:

php vendor/bin/behat features/api/token.feature:21

Yes, failing!

Activating Silex’s HTTP Basic Authentication

In our controller we need to check to see if the password is correct for the user. But hey, let’s not do that, Silex can help us with some of this straightforward logic. In my main Application class, where I configure my security, I’ve already setup things to allow http basic to happen. By adding this little key here, when the http basic username and password come into the request, the Silex security system will automatically look up the user object and deny access if they have the wrong password:

// src/KnpU/CodeBattle/Application.php
// ...

private function configureSecurity()
{
    $app = $this;

    $this->register(new SecurityServiceProvider(), array(
        'security.firewalls' => array(
            'api' => array(
                // ...

                // add this line to the bottom of the array
                'http' => true,
            ),
            // ...
        )
    ));
    // ...
}

It’s kind of like our API token system: but instead of sending a token it’s going to be reading it off of the HTTP Basic username and password headers. That’s pretty handy.

That means that in the controller, if we need the actual user object we don’t need to query for it - the security system already did that for us. We can just say $this->getLoggedInUser():

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

public function newAction(Request $request)
{
    // ...

    $token = new ApiToken($this->getLoggedInUser()->id);
    // ...
}

We don’t really know if the user logged in via HTTP basic or passed a token, and frankly we don’t care. And since we need our user to be logged in for this endpoint, we can use our nice $this->enforceUserSecurity() function:

// src/KnpU/CodeBattle/Controller/Api/TokenController.php
// ...

public function newAction(Request $request)
{
    $this->enforceUserSecurity();

    // ...
}

Perfect, let’s try that out. And it passes with almost no effort!

Leave a comment!

11
Login or Register to join the conversation
Default user avatar
Default user avatar Jacob Kiers | posted 5 years ago

Ryan,

First off, thank you for these informative courses. I really enjoy learning about these techniques.

Now, in the part about checking the authentication and enabling the Silex HTTP Authentication (https://knpuniversity.com/s... ff.), when the 'http' argument is enabled, the API does not in fact return a API Problem response. Instead, it just sends an empty HTTP 401 response with content-type text/html. So technically, the feature should not pass in that case.

That can indeed be solved by using '$this->enforceUserSecurity()', however, in that case the 'http' argument on the firewall needs to be disabled (that is not said explicitly in this course).

Now as for my question: is it possible to use the Silex HTTP Authentication feature, and still return an API Problem response when the request is for one of the /api routes?

Reply
Default user avatar

Actually, disabling the HTTP authentication makes the $this->enforceUserSecurity() call fail. So to keep the HTTP Basic authentication working, it does need to be enabled. However, in that case, not API Problem response is returned as said before.

Further investigation shows that the BasicAuthenticationListener of the Firewall catches the AuthenticationException and sets a response. Therefore, the error handler is not returned.

To solve this, it is necessary to register our own BasicAuthenticationListener for the API:

$this['security.authentication_listener.api.http'] = $app->share(function () use ($app) {
return new ApiBasicAuthenticationListener(
$app['security'],
$app['security.authentication_manager'],
'api',
$app['security.entry_point.api.http'],
$app['logger']
);
});

I just copied the default BasicAuthenticationListener, and removed the try/catch block around the authenticate call in its ::handle() method.

That makes sure that the AuthenticationException is thrown. Now our custom error listener is called. We have one more problem here: the error listener sets the statusCode to 500. Therefore, we have to manually register the ErrorListener, instead of using the Silex $app->error() method. Furthermore, the callback needs to handle the AuthenticationException. And finally, the priority of this event listener needs to be higher.

The full implementation of the new error listener callback is as follows:

private function configureListeners()
{
$app = $this;

$callback = function(GetResponseForExceptionEvent $event, $statusCode) use ($app) {
$exception = $event->getException();
// only act on /api URLs
if (strpos($app['request']->getPathInfo(), '/api') !== 0) {
return;
}

// allow 500 errors in debug to be thrown
if ($app['debug'] && $statusCode == 500) {
return;
}

if ($exception instanceof ApiProblemException) {
$apiProblem = $exception->getApiProblem();
} elseif ($exception instanceof AuthenticationException) {
$apiProblem = new ApiProblem(401, ApiProblem::TYPE_AUTHENTICATION_ERROR);
} else {
$apiProblem = new ApiProblem(
$statusCode
);

/*
* If it's an HttpException message (e.g. for 404, 403),
* we'll say as a rule that the exception message is safe
* for the client. Otherwise, it could be some sensitive
* low-level exception, which should *not* be exposed
*/
if ($exception instanceof HttpException) {
$apiProblem->set('detail', $exception->getMessage());
}
}

$response = $app['api.response_factory']->createResponse($apiProblem);

$event->setResponse($response);
$event->stopPropagation();
};

$this->on(KernelEvents::EXCEPTION, $callback, 100);
}
You might be able to use this if you decide to update this course.

In order to make sure that it is possible: to the extent possible under law,

Jacob Kiers
has waived all copyright and related or neighboring rights to
this post ( https://creativecommons.org... ).

Again, thanks for this great course!

-- Jacob

Reply

Hey Jacob!

Haha, wow - nice work. And you're right - the HTTP Basic error is a 401, but not an Api Problem response - I had overlooked that. The response *is* blank, so we could really probably be "ok" with this - the headers and status code tell the whole story.

But, in the interest of consistency, let's make this *also* return an API Problem response. The Symfony security stuff here is complex, but fortunately you don't need to override the BasicAuthenticationListener (but nice digging!). There is something called an "entry point", which is what's called when the system needs to tell the user "hey, you should login!". That's used inside the BasicAuthenticationListener, and we can use that. In fact, we already have an entry point setup for the "api_token" authentication system, and we can re-use it here.

The end result - summarized - looks like this: https://gist.github.com/wea....

Let me know if that works or doesn't make any sense at all :).

Cheers!

Reply
Default user avatar

Hi Ryan. I tested that, and it works indeed. Furthermore, it seems that Silex overrides the entry point each time a new authentication provider is registered. So moving the "http => true" line above the "api_token => true" line works too. That is a bit hacky, though. Your solution is definitively cleaner.

Reply

Yep, you're right about the ordering. Silex is missing a setting where we can set the "entry point" at the firewall level explicitlyThe Symfony framework has this - it's probably missing from Silex just on accident.

Cheers!

Reply
Default user avatar

I basically copypasted your TokenController and when sending a request to /tokens the addRoutes gets executed (adding some echoes there show in the response). However the newAction function is not called, adding an echo at the beginning (before enforcing security) doesn't show up. The server returns a 401 unauthorized. I am not sure of where is this error thrown. In the ApiTokenListener I can make sure that the basic auth headers are received properly, Authorization equals something of the form "Basic c35CWETCWERC2c52" (so now I understand why the "token ABCD123" ;D). However I am not sure of if this "c35..." has to equal the encoded password in the database (it does not). Also, if Basic auth were incorrect it would fail after enforceUserSecurity and that function never gets executed so the Unauthorized must come from somewhere else. Do you have any idea of what may I be doing wrong or how could I debug here?

I did change the config at the time to register the Security Provider but it works well with all the other Controllers... i have both 'http' and 'api_token' set to true...

Thanks!

Reply

Hey Joan!

I hope it's this easy: are you making a request to /api/tokens or just /tokens? If you're requesting to /tokens, that doesn't exist, but it *is* a secured path, so this *could* be the issue :). And since the "api" firewall is only active for URLs under /api, sending your token will not activate the ApiTokenListener.

Let me know!

1 Reply
Default user avatar

Thanks for the prompt answer Ryan. As I don't intend to do a web version I haven't used the /api/ distinction. The requests look like this https://github.com/mezod/mu...

Therefore, my Application.php looks like follows:

$this->register(new SecurityServiceProvider(), array(
'security.firewalls' => array(
'main' => array(
'pattern' => '^/',
'form' => true,
'users' => $this->share(function () use ($app) {
return $app['repository.user'];
}),
'anonymous' => true,
'logout' => true,
'stateless' => true,
'http' => true,
'api_token' => true,
),
)
));

// require login for application management
$this['security.access_rules'] = array(
// allow anonymous API - if auth is needed, it's handled in the controller
array('^/', 'IS_AUTHENTICATED_ANONYMOUSLY'),
);

I'm not sure if you mean that /tokens is some kind of reserved path by Silex or similar. I have changed that by /apitokens or /api/tokens with no luck (same 401). I am not sure if it is that the route is not matched or if there's something in the config of the security that throws it off. It is a 401 Unauthorized but it is not returning any ApiProblem at all. Must be an exception thrown by Silex. But my access rules are basically accepting everything and anonymously. The ApiTokenListener is being activated, but as the Authorization is of the type "Basic XXXX" the listener just returns because he's got nothing to do there. I'll try to find it out tomorrow, but if you come up with any other idea I'll be glad to try it out!

Reply

Hey Joan!

If you send an API token to this endpoint (i.e. authenticate like you do with any other endpoint), does it work? If so, then we know the problem is quite simply that the HTTP Basic authentication isn't working - and that there's nothing special about this URL/controller that is causing issues.

If you're debugging this, I wouldn't look directly at the Authorization header, as I believe there's some encoding that happens, so it might be confusing (http://en.wikipedia.org/wik.... Instead, add some var_dump and die statements to the BasicAuthenticationListener class (deep in the core of Symfony) - it is the equivalent of ApiTokenListener, but for the HTTP Basic authentication. My guess is that the username/password is being sent incorrectly, or isn't matching up with how you're encoding/salting passwords in the db. Also, tail logs/development.log as some of what's going on in the security system *is* logged.

Good luck!

Reply
Default user avatar

Sorry that I start a new thread, apparently disqus is not loading our previous conversation. I just came to share my stupidity with everyone x)

Ryan, as you suggested, making the request with a token worked. I narrowed it down to the fact that the passwords weren't matching, yesterday I spent the whole day trying to find out how did the encode actually work and also, I discarded the Salt as I wasn't using it, also got lost into the Security Component. But today, after a couple of tests in the same direction I thought, what if I am not saving the password properly? I dumped the encoded password and realized that it had 88 chars. My row in the database was limited to 50. Shame on me.

All of this made me realize that the validation via annotations doesn't work for the user password, because of all of this encoding process. Is there an obvious workaround?

In other words:

if ($errors = $this->validate($programmer)) {
$this->throwApiProblemValidationException($errors);
}

doesn't throw an Exception for a password with 1 character with this Annotations:

/**
* @Assert\Length(
* min = 8,
* max = 26,
* minMessage = "The password must be at least {{ limit }} characters long",
* maxMessage = "The password cannot be longer than {{ limit }} characters long"
* )
*/
public $password;

Reply

Hey Joan!

Yes, the Disqus issue is actually my fault - but I noticed it too and will fix it soon! Thanks for re-posting.

To answer your question about validation, what you typically have is 2 properties in this case: plainPassword (which is *not* persisted, but which *is* validated) and password (which is persisted, but not validated, and contains the encoded password). You can see that this solves you problem. It also solves the potential problem of setting a plain password on your property, somehow forgetting to encode it, and ending up with plain passwords in your database.

Cheers!

Reply
Cat in space

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

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ?valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4", // v2.7.3
        "jms/serializer": "~0.16", // 0.16.0
        "willdurand/hateoas": "~2.3" // v2.3.0
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice