Simple API Key Authentication in Symfony2 using FOSUserBundle (and HWIOauthBundle), filling in the gaps

Pez picture Pez · Mar 28, 2014 · Viewed 8.1k times · Source

Edit: See below for my own solution, which is, at the time of writing, functioning but imperfect. Would love some criticism and feedback, if I get something put together that I feel is really solid then I'll make a howto blog post for other people facing the same challenge.

I've been struggling with this for days, and I'm hoping someone can let me know if I'm on the right path.

I have a system with a FOSRestBundle webservice in which I'm currently using FOSUserBundle and HWIOAuthBundle to authenticate users.

I would like to set up stateless api key authentication for the webservice.

I've read through http://symfony.com/doc/current/cookbook/security/api_key_authentication.html and this seems simple enough to implement, I've also installed UecodeApiKeyBundle which seems to be mostly just an implementation of this book page.

My question is a n00b one...what now? The book page and bundle both cover authenticating a user by API key, but don't touch on the flow of logging users in, generating API keys, allowing users to register, etc. What I would really like is simple API endpoints for login, register, and logout that my app developers can use. Something like /api/v1/login, etc.

I think I can handle registration....login is confusing me though. Based upon some additional reading, it seems to me like what I need to do for login is this:

  • Create a controller at api/v1/login that accepts POST requests. The request will either look like { _username: foo, _password: bar } or something like { facebook_access_token: foo. Alternately, the facebook login could require a different action, like /user/login/facebook, and just redirect to the HWIOAuthBundle path }.

  • If the request contains _username and _password parameters, then I need to forward the request to login-check (I'm not sure about this one. Can I just process this form myself? Or, should I manually check the username and password against the database?)

  • Add a login event listener, if user authenticated successfully, generate an api key for the user (This is only necessary if I'm not checking it myself, of course)

  • Return the API Key in the response of the POST request (This breaks the post-redirect-get strategy, but otherwise I don't see any issues with it) I think this eliminates the redirect to login-check option I listed above.

As you can probably see I'm confused. This is my first Symfony2 project, and the book pages on Security sound simple...but seem to gloss over some of the details and it's left me quite unsure of what way to proceed.

Thanks in advance!

=============================================================

Edit:

I've installed a API Key Authentication pretty much identically to the relevant cookbook article: http://symfony.com/doc/current/cookbook/security/api_key_authentication.html

To handle user's logging in, I've created a custom controller method. I doubt that this is perfect, I would love to hear some feedback on how it can be improved, but I do believe that I'm on the right path as my flow is now working. Here's the code (Please note, still early in development...I haven't looked at Facebook login yet, only simple username/password login):

class SecurityController extends FOSRestController
{

    /**
     * Create a security token for the user
     */

    public function tokenCreateAction()
    {
        $request = $this->getRequest();

        $username = $request->get('username',NULL);
        $password = $request->get('password',NULL);

        if (!isset($username) || !isset($password)){
            throw new BadRequestHttpException("You must pass username and password fields");
        }

        $um = $this->get('fos_user.user_manager');
        $user = $um->findUserByUsernameOrEmail($username);

        if (!$user instanceof \Acme\UserBundle\Entity\User) {
            throw new AccessDeniedHttpException("No matching user account found");
        }

        $encoder_service = $this->get('security.encoder_factory');
        $encoder = $encoder_service->getEncoder($user);
        $encoded_pass = $encoder->encodePassword($password, $user->getSalt());

        if ($encoded_pass != $user->getPassword()) {
            throw new AccessDeniedHttpException("Password does not match password on record");
        }


        //User checks out, generate an api key
        $user->generateApiKey();
        $em = $this->getDoctrine()->getEntityManager();
        $em->persist($user);
        $em->flush();

        return array("apiKey" => $user->getApiKey());
    }

}

This seems to work pretty well, and user registration will be handled similarly.

Interestingly to me, the api key authentication method I implemented from the cookbook appears to ignore the access_control settings in my security.yml file, in the cookbook they outline how to only generate the token for a specific path, but I didn't like that solution, so I've implemented my own (also somewhat poor) solution to not check the path I'm using to authenticate users

api_login:
    pattern: ^/api/v1/user/authenticate$
    security: false

api:
    pattern: ^/api/*
    stateless: true
    anonymous: true
    simple_preauth:
        authenticator: apikey_authenticator

I'm sure there's a better way to do this too, but again...not sure what it is.

Answer

AntoineWDG picture AntoineWDG · Jul 16, 2015

You are trying to implement stateless authentication with username and login. This is pretty much what the Oauth2 authentication passsword grant does. This is pretty standard, so instead of trying to implement it yourself i'd recommend you use a Bundle for that, for example the FOSOauthServerBundle. It can use FOSUserBundle as its user provider and would be cleaner, more secured and easier to use than a home-made solution.

To register user, your can create a register action in your API (e.g., in a REST API I'd use POST - api/v1/users), and in the controller method copy and past the code from the FOSUserBundle:RegistrationController (of course adapt it for your needs).

I did that in a REST API, it worked like a charm.