Pez
Pez

Reputation: 1299

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

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:

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.

Upvotes: 12

Views: 8595

Answers (3)

Ajay Singh
Ajay Singh

Reputation: 1

Class GenearteToken extends FOSRestController
{

     public getTokenAction(Request $request){

          $apiKey = $request->query->get('apikey');
          return $apiKey;
      }


}

Upvotes: -1

AntoineWDG
AntoineWDG

Reputation: 549

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.

Upvotes: 3

Bactisme
Bactisme

Reputation: 1683

I don't think you actually really need a /login endpoint.

In the symfony doc, the api client is required to pass it's key (via apiKey http parameter) to every request to the API.

I am not sure it's in the best practice, but you could do this.

"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"

The best is to allow your users to register via a web form (for example with the route fos_user_register). User entity could have an apikey field, pre-populated with a key generated like this sha1("secret".time()) for example, and a button in their profile to regenerate the key.

Upvotes: 2

Related Questions