oAuth2 with symfony2

This is the second post in the series over oAuth with Symfony2, iOS and Android. All posts in this serie can be found here.

Creating a simple website with Symfony2 is actually very simple. Symfony2 is a popular framework, with a lot of available libraries/bundles for all kind of things.
Most of these bundles are pretty good documented, and due to that it is pretty easy to add a new functionality.

For this website I am going to use the next libraries, besides symfony2 itself:

The FOS bundles are documented very well, and most of them are updated regularly.
The next structure/functionality will be used/included for the simple site:

  • A main site at www.example.org
  • A subdomain specific for the API at api.example.org
  • User creation/logging in is handled by the FOS user bundle.
  • There will be just one entity that is served over the REST interface, named Demo. This entity only has a title and a description.
  • The API will contain functions to create, list, edit and delete the Demo entity.
  • Database structure changes are maintained by migrations

All code from this example for the symfony2 part can be found at: https://github.com/paul999/SymfonyOauthDemo

To maintain symfony and the used bundles I am using composer, and assume you have a composer.phar available on your system.

Creating the basic symfony project can be done with the next line of code:

php composer.phar create-project symfony/framework-standard-edition path/ 2.4.4

This will create a standard symfony project, including a Acme bundle, which we aren’t going to use. This bundle can be simply removed by following the instructions from here. Instead of  removing this “demo”  bundle we could also re-use is for this example as well.

Now we can start with creating the simple site itself. First, we create our own bundle in the project:

php app/console generate:bundle --namespace=Oauth/DemoBundle --format=annotation

Use all standard answers for the questions, except for
Do you want to generate the whole directory structure
, choose yes here instead. A new directory structure is now created in the src/ directory named Oauth/DemoBundle. If you used a different name for your bundle/directory structure, please make sure to update all references to it in the next code examples as well.

For installing the FOS user bundle I refer to their documentation. Skip the part of creating the database tables itself, we are going to use migrations instead (See below).

To manage database changes you can use several options in symfony2 with doctrine. I decided to use migrations for this (And most of my other projects). Using migrations is pretty easy, and described here.
Once the bundle is installed, a new migration can be created with

php app/console doctrine:migrations:diff

This creates a new file containing the migration for the user table. The table itself is not yet created, to run (And thus create the tables) the migration:

php app/console doctrine:migrations:migrate

The tables are now created.

For this project we aren’t doing any special configuration for the user bundle, in normal cases you would (of course) modify it to your needs.
To create a new user you can use the next command line command:

php app/console fos:user:create admin --super-admin

This will create a user admin, the password will be asked at the cli.

Now we can start with adding the oauth2 server bundle from Friends of Symfony. This bundles comes with a great manual as well, which can be found here. Some parts of the manual are very similar from the user bundle. You, of course, don’t need to do the same thing twice (Except for updating composer packages ;)).
You can follow the instructions given at the documentation. We aren’t doing anything special here yet :).
Do not forgot to change Your\Own\Entity\User to Oauth\DemoBundle\Entity\User in the entities.

Now you have done the installation of the oauth server bundle we need to finish the security configuration. Because we include everything in the main firewall (Using ^/ as path), we don’t need the checks for oauth_authorize, so we can remove that section.
Within the main firewall we define that FOS user bundle is the provider, with the right login checks.
For the full security.yml that I use, see here. The full routing configuration can be found here. Just a note: I included host entries in both the router and security configurations, with a api_host and main_host parameter, this parameter should be added to the parameters.yml. It also contains already parts for the steps here below, sorry :(.

Because we added new entities, we need to create a new migration by running the diff command from above. After that we can run migrate to add the new add the new tables.

Before we continue with configuring the oAuth/API we test our changes first. While you can do this with a webserver like apache, you can also use the build in (debug/development) webserver in php. Symfony has a nice command for this:

php app/console server:run

A webserver will run now on port 8000 on localhost. Lets see if everything works:

  • http://localhost:8000/ Because there is no route defined for this will result in a 404.
  • http://localhost:8000/oauth/v2/auth if you aren’t logged in yet, you should see a simple login screen. After logging in (Which should work if you created a user above) you should return to this URL, because the URL required extra information, it will give a error “Client not found”
  • http://localhost:8000/oauth/v2/token will give a JSON message with “Invalid grant_type parameter or parameter missing”

If this doesn’t work correctly, please check the next things:

  • Did you migrate all migrations?
  • Did you run composer.phar update?
  • If you used my security.yml, you need to (temporary) removed the host field in the firewall possibly.

So, now we know that the first basics work. For the API, we are going to use the FOS Rest bundle. With this bundle we can create a REST API very easy, and thats what we want :). Like the two earlier FOS bundles, we use the standard configuration for this bundle, which can be found here.
Now, lets start configuring the API. The changes described below are based off the post from William Durand, however I don’t use form types for editing and creating new Entities.

Lets start with adding the two routes for getting the Demo entity:

demo_demo_all:
    pattern:  /demos
    host:   "%api_host%"
    defaults: { _controller: OauthDemoBundle:Demo:all, _format: ~ }
    requirements:
        _method: GET

demo_demo_get:
    host:   "%api_host%"
    pattern:  /demos/{id}
    defaults: { _controller: OauthDemoBundle:User:get, _format: ~ }
    requirements:
        _method: GET
        id: "\d+"

Before we can create the controller we first need to create the entity:

<?php

namespace Oauth\DemoBundle\Entity;


use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="demo")
 */
class Demo {
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\Column(type="text")
     */
    protected $title;

    /**
     * @ORM\Column(type="text")
     */
    protected $description;

} 

Having some getters/setters for the entity can be very handy:

php app/console doctrine:generate:entities Oauth

Because we didn’t yet generate these for the oauth entities, they will also be generated with this command. Because this class implements  some interfaces, we need to make sure this generated setters are valid, and modify them if needed.

Now, lets create the first part of the controller:

<?php namespace Oauth\DemoBundle\Controller; use FOS\RestBundle\Controller\Annotations as Rest; use Oauth\DemoBundle\Entity\Demo; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class DemoController extends Controller {     /**      * @Rest\View      */     public function allAction()     {         $demos = $this->getDoctrine()
            ->getRepository('OauthDemoBundle:Demo')
            ->findAll();

        return array('demos' => $demos);
    }

    /**
     * @Rest\View
     */
    public function getAction($id)
    {
        $demo  = $this->getDoctrine()
            ->getRepository('OauthDemoBundle:Demo')
            ->find($id);


        if (!$demo instanceof Demo) {
            throw new NotFoundHttpException('Demo not found');
        }

        return array('demo' => $demo);
    }
}

To test this, I disabled temporary the oauth provider for the api subdomain, and went to http://api.localhost:8000/demos This returned a empty XML list. (Note: If you do get a error, make sure you use my config.yml from above).

Now we have this working, we need something to insert a new Demo into the database.
Routing for adding:

demo_demo_new:
    pattern:  /demos
    defaults: { _controller: OauthDemoBundle:Demo:new, _format: ~ }
    requirements:
        _method: POST

Because we use the validator for this submission, we need to add @Assert\NotBlank() in the entity. This will make sure that the fields won’t be empty on submission.
And the controller:

    /**
     * @param Demo $demo
     * @param Request $request
     * @return View|Response
     * @ApiDoc(
     * description="Create a new demo"
     * )
     * @ParamConverter("demo", class="Oauth\DemoBundle\Entity\Demo", converter="fos_rest.request_body")
     */
    public function newAction(Demo $demo, Request $request)
    {
        return $this->processForm($demo, $request);
    }

    /**
     * @param Demo $demo
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @return View|Response
     */
    private function processForm(Demo $demo = null, Request $request)
    {
        $statusCode = 204;
        if ($demo->getId() == null)
        {
            $statusCode = 201;
        }

        if (sizeof($request->get('validationErrors')) == 0) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($demo);
            $em->flush();

            $response = new Response();
            $response->setStatusCode($statusCode);

            // set the `Location` header only when creating new resources
            if (201 === $statusCode) {
                $response->headers->set('Location',
                    $this->generateUrl(
                        'demo_demo_get', array('id' => $demo->getId()),
                        true // absolute
                    )
                );
            }

            return $response;
        }

        return View::create($request->get('validationErrors'), 400);
    }

To test adding a new Demo, you can use the curl request below:

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"title":"foo","description":"bar"}' http://api.localhost:8000/demos

Now it is time to add a edit function :).
Routing:

demo_demo_edit:
    pattern:  /demos/{id}
    defaults: { _controller: OauthDemoBundle:Demo:edit, _format: ~ }
    requirements:
        _method: PUT

Because we used the process_form method for creating, we can re-use that for editing:

    /**
     * @param Demo $demo
     * @param Request $request
     * @return View|Response
     * @ApiDoc(
     *  description="Edit a demo"
     * )
     * @ParamConverter("demo", class="Oauth\DemoBundle\Entity\Demo", converter="fos_rest.request_body")
     */
    public function editAction(Demo $demo, Request $request)
    {
        $dm  = $this->getDoctrine()
            ->getRepository('OauthDemoBundle:Demo')
            ->find((int)$request->get('id'));
        $dm->setDescription($demo->getDescription());
        $dm->setTitle($demo->getTitle());
        
        return $this->processForm($dm, $request);
    }

And that was it, lets test:

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X PUT -d '{"title":"bar","description":"foo"}' http://api.localhost:8000/demos/1

Deleting is even more straightforward:

demo_demo_delete:
    pattern:  /demos/{id}
    defaults: { _controller: OauthDemoBundle:Demo:remove, _format: ~ }
    requirements:
        _method: DELETE

Controller:

    /**
     * @Rest\View(statusCode=204)
     */
    public function removeAction(Demo $demo)
    {
        $em = $this->getDoctrine()->getManager();
        $em->remove($demo);
        $em->flush();
    }

And lets test it:

curl -v -H "Accept: application/json" -H "Content-type: application/json" -X DELETE  http://api.localhost:8000/demos/1

Now we basically have everything what we need for our (very simple) REST API. In a real world situation you would have a more complex entity, with more validation as we currently have, but for now we keep the example simple. While tests might be useful, iam not going to add them in this serie for symfony, as I don’t want to concentrate too much on it.

Before we continue on the next part of this serie, make sure to re-enable the oauth requirements for the api subdomain, you kinda need them if you want to use oauth ;). While the basics of our API work (We saw it ourselves :)) we didn’t test the oAuth functionality yet. For this we will need a oAuth client, and because we are already going to create a app with that functionality, we are going to use that for testing oAuth.
A online version of this simple site, running the code from here, can be found at www.ip-6.nl and api.ip-6.nl for demonstration purposes. I will use this online version as well in the upcoming android/iOS apps instead of localhost. As we didn’t create a nice index page, that will generate a 404.

Leave a Reply

Your email address will not be published. Required fields are marked *