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.

Requiring Authentication

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

Requiring Authentication

Let’s talk about API Authentication. The web part of our site does have authentication. I can login as ryan@knplabs.com password foo. By the way, if that ever doesn’t work for you, go back and just delete the SQLite database we’re using:

rm data/code_battles.sqlite

This is because when we run our Behat tests, it’s messing with our database. That’s not something we’d stand for. In a real project, we’d want to fix that!

When we’re logged in as Ryan, any programmer we create is attached to the Ryan user in the database. The web side has authentication and we know who created which programmers.

In our API, that’s not the case at all: we have no authentication. And so we’re kind of faking the attachment of a programmer to a user. In newAction we call this function handleRequest, which lives right in this same class. Then down here on the bottom, you can see how we fake it:

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

private function handleRequest(Request $request, Programmer $programmer)
{
    // update the programmer from the request data
    // ...

    $programmer->userId = $this->findUserByUsername('weaverryan')->id;
}

We assume there’s a user called weaverryan in the database, grab it’s id and attach that to every programmer. So obviously that’s just hardcoded silliness and we need to get rid of it! We need some way for the API request to authenticate itself as a user.

Denying Access with a 401 (in Behat)

We’re going to do this with tokens. You can create your own simple token system or you can do something bigger like OAuth. The important thing to realize is that the end results are the same: the request will send a token and you’ll look that up in some database and figure out which user the token belongs to and if they have access. Using OAuth is just a different way to help you handle those tokens and get those to your users.

Before we start anything let’s write a new Behat feature file, because we haven’t done anything with authentication yet. For Behat newbies, this will feel weird, but stick with me. I’m going to describe here the business value of our authentication feature and who benefits from it, which is the API client. That part is not important technically, but we’d pity the fool who doesn’t think about why they’re building a feature:

# features/api/authentication.feature
Feature: Authentication
  In order to access protected resource
  As an API client
  I need to be able to authenticate

Below that, let’s add our first scenario, which is going to be one where we try to create a programmer without sending a token. Ultimately we want to require authentication on most endpoints.

# features/api/authentication.feature
Feature: Authentication
  # ...

  Scenario: Create a programmer without authentication
    When I request "POST /api/programmers"
    Then the response status code should be 401
    And the "detail" property should equal "Authentication Required"

If I just post to /api/programmers with no token, the response I get should be 401. We don’t know what the response is going to look like yet, but let’s imagine that it’s JSON and has some detail key on it which gives us a little bit of information about what went wrong.

So there we go! No coding yet, but we know how things should look if we don’t send a token. If we try this right now - which is always a good idea - we should see it fail. By the way we can run one feature file at a time, and that’s going to be really useful for us:

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

And there we go! You can see it’s giving us a 400 error because we’re sending invalid JSON instead of the 401 we want.

Denying Access in your Controller

So let’s go into ProgrammerController and fix this as easily as possible up in newAction. I already have a function called getLoggedinUser(), which will either give us a User object or give us null if the request isn’t authenticated. What we can do is check if !getLoggedInUser() and then return the access denied page. In Silex, you do this by throwing a special exception called AccessDeniedException. Make sure you have the right use statement for this, which is in the Security component:

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

use Symfony\Component\Security\Core\Exception\AccessDeniedException;
// ...

public function newAction(Request $request)
{
    if (!$this->isUserLoggedIn()) {
        throw new AccessDeniedException();
    }
    // ..
}

And there we go! Since that should deny access, let’s try our test:

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

Boom! And this time you can see that we are passing the 401 test. Get outta here ya tokenless request! The only issue is that the response body is coming back slightly different than we expected. There is a detail property, but instead of it being set to “authentication required”, it’s set to this not privileged to request the resource string. And yea, it’s not really obvious where that’s coming from.

What Happens when you Deny Access Anyways?

But first, how is this even working behind the scenes? If you were with us for episode 1, we did some pretty advanced error handling stuff at the bottom of this Application class. What we basically did was add a function that’s called anytime there’s an exception thrown anywhere. Then, we transform that into a consistent JSON response that follows the API problem format. So any exception gives us a nice consistent response:

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

$this->error(function(\Exception $e, $statusCode) use ($app) {
    // ...

    $data = $apiProblem->toArray();

    $response = new JsonResponse(
        $data,
        $apiProblem->getStatusCode()
    );
    $response->headers->set('Content-Type', 'application/problem+json');

    return $response;
});

And hey, when we deny access, we’re throwing an exception! But bad news, our exception is the one weird guy in the whole system: instead of being handled here, it’s handled somewhere else entirely.

ApiEntryPoint: Where Security Responses are Created

Without getting too far into things, I’ve already written most of the logic for our token authentication system, and I’ll show you the important parts but not cover how this all hooks up. If you have more detailed questions, just ask them in the comments.

Let’s open up a class in my Security/Authentication directory called ApiEntryPoint. When we deny access, the start() method is called. Here, it’s our job to return a Response that says “hey, get outta here!”:

// src/KnpU/CodeBattle/Security/Authentication
// ...

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
// ...

class ApiEntryPoint implements AuthenticationEntryPointInterface
{
    // ...

    public function start(Request $request, AuthenticationException $authException = null)
    {
        $message = $this->getMessage($authException);

        $response = new JsonResponse(array('detail' => $message), 401);

        return $response;
    }

    /**
     * Gets the message from the specific AuthenticationException and then
     * translates it. The translation process allows us to customize the
     * messages we want - see the translations/en.yml file.
     */
    private function getMessage(AuthenticationException $authException = null)
    {
        $key = $authException ? $authException->getMessageKey() : 'authentication_required';
        $parameters = $authException ? $authException->getMessageData() : array();

        return $this->translator->trans($key, $parameters);
    }
}

So that’s where the detail key is coming from, and it’s set to some internal message that comes from the deep dark core of Silex’s security that describes what went wrong.

Fortunately, I’m also translating that message, and we setup translations in episode 1. in this translations/ directory. This is just a little key value pair. So let’s copy that unhelpful “this not privileged request” thing that Silex’s gives us and translate it to something nicer:

# translations/en.yml
authentication_required: Authentication Required
"Not privileged to request the resource.": "Authentication Required"

Ok, rerun the tests!

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

Fantastic!

Leave a comment!

6
Login or Register to join the conversation
Default user avatar

Rather basic question for a change, why do we need to have a POST request to create tokens. Can't a token be created together with the creation of a user account? So that when a user registers, a POST user request is made, creating a user and an apitoken for that user in the database, and returning the token for the user to keep browsing? Or I am just simply wrong and the idea is that a token is created for every session?

This is how I understand it so far so please correct me if wrong:

REGISTRATION IN A JS APP
1. User fills a form that sends a POST users request.
2. Server validates and creates the user (* possibly creating a token for the user too) and maybe sends a confirmation mail(?)
3. Server returns token to the client app, which will add the token in every future request.

LOGIN
1. User fills a login form that sends a ?????? request.
2. Server validates the user through Basic Auth, retrieves the token for that user and returns it to the js app.
3. The js app attaches the token to every future request.

I'd really appreciate if you could elaborate on this :) Thanks!

1 Reply

Hi Joan!

Great questions as always :). First, let me talk about the LOGIN part. Basically, the flow you have setup is correct, though I'll make 2 points:

1) If it's YOUR JS app on YOUR domain, you could just have the user login like normal and create a session (like a traditional website). Then, all requests that are made from JS are authenticated, because they carry the session with it.

2) If it's NOT your JS app, then things get interesting due to security. Ultimately, the JS app needs a token, and you can decide if this lasts for 10 minutes or 10 years. In this model, there is no session, so you can't think of it as "this token lasts for this session". In our app, the tokens last forever - you have to weigh the security implications of that. So, if you *were* to login with a JS app, it would look something like this:

1) User sends a POST request to create a token. That request somehow contains authentication information (in our app, we did this with HTTP Basic auth).
2) ... same as your #3

This whole "what's the best way for a client to retrieve a token?" question relates to OAuth, whose whole job is to basically help a client securely get an access token. Again, this only applies if you're truly opening up your API to third parties, but if you are, then you'll probably be well-served by setting up your app to be an OAuth server and replacing my (1) above with an OAuth flow that a client can use to get a token.

Oh, and last thing. You of course *can* create a token right when you create a user. Heck, the token could just be a property on your User, not a different resource. I decided to allow a User to have many tokens because I might want 10 different tokens that I give out to 10 different API clients (e.g. other sites that integrate with mine, iPhone apps, etc). Then, I could revoke any token to revoke access to just *one* of those clients, instead of needing to change my *one* key and update it in all of the places that I still want to give access to. Again, this is all in the "land of theory" unless you really do have third parties using your API.

Cheers!

1 Reply
Default user avatar

Thanks for your answer once more. For now I'll just call the createToken action when registering a user and return the new token in the response. And then create a login request that with basic auth returns the token. Should be able to go far enough with that alone... still don't know how the js app works so I can always come back to the API :p

I'll leave OAuth for my next crappy app...

http://stackoverflow.com/qu...

Reply
Default user avatar

Would it be possible sometime please do the same with Symfony2 and FOSRestBundle and other bundles maybe?

Reply

Definitely - I'll do exactly that! And we'll be able to move a little bit faster through those, because we'll have all the concepts covered here.

Cheers!

Reply
Default user avatar

well, damn it, 3 days to get the security part to work on my project, I had reformulated my question here so many times that now I feel I still need to say something! I still don't understand some parts but hey, I got that going for me!

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