Sunday, April 8, 2012

Setting up a Symfony2 REST service with FOSRestBundle

Installation

First thing is to download and setup the FOSRestBundle. If you are running Symfony 2.0.x, get the FOSRestBundle version 0.6, otherwise, download the code in the master branch. The FOSRestBundle depends on the JMSSerializerBundle so the following instructions are going to include some extra information for setting up this bundle to complete our REST service.

First thing is to install our dependencies. Add the following to your deps file:

[metadata]
    git=http://github.com/schmittjoh/metadata.git
    version=1.1.0

[JMSSerializerBundle]
    git=git://github.com/schmittjoh/JMSSerializerBundle.git
    target=bundles/JMS/SerializerBundle

[FOSRest]
    git=git://github.com/FriendsOfSymfony/FOSRest.git
    target=fos/FOS/Rest

[FOSRestBundle]
    git=git://github.com/FriendsOfSymfony/FOSRestBundle.git
    target=bundles/FOS/RestBundle
    version=origin/0.6

Run the vendors script to install these bundles:

php bin/vendors install

Next, update your app/autoload.php file with the following lines:

$loader-&ght;registerNamespaces(array(

    'FOS\\Rest' => __DIR__.'/../vendor/fos',
    'FOS'       => __DIR__.'/../vendor/bundles',
    'JMS'              => __DIR__.'/../vendor/bundles',
    'Metadata'         => __DIR__.'/../vendor/metadata/src',

));

Register the new bundle in your app/AppKernel.php file:

public function registerBundles()
{
    $bundles = array(

        new JMS\SerializerBundle\JMSSerializerBundle($this),
        new FOS\RestBundle\FOSRestBundle(),

    );
}

Next, add the following configuration to your app/config.yml file:

fos_rest:
    view:
        view_response_listener: false
        failed_validation: HTTP_BAD_REQUEST
        default_engine: php
        formats:
            json: true
    format_listener:
        prefer_extension: true
    body_listener:
        decoders:
            json: fos_rest.decoder.json

A couple of words about the configuration section above. First of all, there are two formatting options called formats and templating_formats. Any format listed under the templating_formats option, requires a template for rendering. I am not going to utilize this method. Instead, I will use the JMSSerializerBundle by simply utilizing the formats option as illustrated above.

In regards to the view_response_listener option, this configuration option allows you to return a FOSRest View object directly from your controller like below:

$view = View::create();
$view->setData($data);

return $view;

This is bit of an invasive approach so I will set it to false and stick to using the handler method as illustrated below:

$view = View::create()
$view->setData($data);

return $this->get('fos_rest.view_handler')->handle($view);

However, if you decide to use the view listener approach, you will have to disable template annotations, or you will get the following error message:

RuntimeException: You need to disable the view annotations in SensioFrameworkExtraBundle when using the FOSRestBundle View Response listener.

To address this problem, you need to set the view annotations flag under sensio_framework_extra section in your app/config.yml to false.

sensio_framework_extra:
    view:
        annotations: false

If you have the AsseticBundle installed, setting prefer_extension to false will cause your css and js files to be returned from the server with application/json headers effectively breaking your UI. To prevent this you can either set prefer_extension to false and use .json or .xml URL prefixes when making REST calls or simply update the default_priorities flag with additional formats. See this link to see the solution for this issue. In this case, I will stick with the extension approach.

A full configuration example can be found here.

Finally, if you ever get a "Fatal error: Interface 'Doctrine\Common\Persistence\Proxy' not found" error, you need to upgrade your Doctrine packages from 2.1 to 2.2. Add the following to your deps file:

[doctrine-common]
    git=http://github.com/doctrine/common.git
    version=2.2.1

[doctrine-dbal]
    git=http://github.com/doctrine/dbal.git
    version=2.2.1

[doctrine]
    git=http://github.com/doctrine/doctrine2.git
    version=2.2.1

REST Service Setup

The first step is to create our REST controller. I will be exposing a User class that defines a user profile in application. Here is my REST controller with a single action that exposes a user profile:

namespace Acme\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\View\View;

class UserRestController extends Controller
{
    /**
     * GET /users/{userId}
     * 
     * @param string $userId
     * @return Response
     */
    public function getUserAction($userId)
    {
        $user = $this->get('security.context')->getToken()->getUser();
        
        if ($userId == $user->getId()) {
            
        }
        
        $view = View::create()
          ->setStatusCode(200)
          ->setData($user);

        return $this->get('fos_rest.view_handler')->handle($view);
    }
}

Next, register your controller in your app/config/routing.yml file. Do not forget to set the type option to rest as illustrated below:

acme_user_rest:
    resource: Acme\UserBundle\Controller\UserRestController
    prefix: /api
    type: rest

Once the controller setup is done, the next step is to expose our models/documents/entities. The User class that I will be exposing is actually a child class that inherits from FOSUserBundle/Document/User. This a very important point because when you are using JMSSerializerBundle, you have to separately configure each parent class for serialization. In order to accomplish this goal, add the following section to your configuration app/config/config.yml file:

jms_serializer:
    metadata:
        auto_detection: true
        directories:
            FOSUserBundle:
                namespace_prefix: FOS\UserBundle
                path: "@AcmeUserBundle/Resources/config/serializer/fos"

The directories section signals the JMSSerializerBundle to look for configuration files for any class that has a FOS\UserBundle prefix under the path specified. This is the mechanism that you should use when you are dealing with classes from other bundles. It allow you to expose class properties that you are interested in without actually touching any other bundle code.

Simply add the JMSSerializerBundle configuration files to your project:

Acme/Userbundle/Resources/config/serializer/fos/Model.User.xml

<serializer>
    <class name="FOS\UserBundle\Model\User" exclusion-policy="all">
        <property name="username" type="string" expose="true" access-type="public_method"></property>
        <property name="usernameCanonical" type="string" expose="true" access-type="public_method"></property>
        <property name="email" type="string" expose="true" access-type="public_method"></property>
        <property name="emailCanonical" type="string" expose="true" access-type="public_method"></property>
    </class>
</serializer>

Acme/Userbundle/Resources/config/serializer/Document.User.xml

<serializer>
    <class name="Acme\UserBundle\Document\User" exclusion-policy="all">
        <property name="id" type="string" expose="true" read-only="true"></property>
        <property name="dob" type="date" expose="true" access-type="public_method"></property>
        <property name="nameFirst" type="string" expose="true" access-type="public_method"></property>
        <property name="nameMiddle" type="string" expose="true" access-type="public_method"></property>
        <property name="nameLast" type="string" expose="true" access-type="public_method"></property>
        <property name="createdAt" type="DateTime" expose="true" access-type="public_method"></property>
        <property name="modifiedAt" type="DateTime" expose="true" access-type="public_method"></property>
        <property name="blocked" type="boolean" expose="true" access-type="public_method"></property>
        <property name="deleted" type="boolean" expose="true" access-type="public_method"></property>
        <property name="avatarPath" type="string" expose="true" access-type="public_method"></property>
        <property name="lastLogin" type="DateTime" expose="true" access-type="public_method"></property>
    </class>
</serializer>

The prefixes - "Model." and "Document." in this case - that are used for naming these configuration files correspond to the original directories where each class resides. Therefore, using an incorrect prefix would break the setup.

If you look at the Document.User.xml file above, you can see that there is no access-type type attribute for the id property update and it is replaced by a read-only attribute. In fact, failure to remove the access-type attribute causes an exception. This is because you can not update OjbectIds in MongoDB and auto-generated classes do not even contain a setId() method.

Another important point is to clear the Symfony cache when you add new files or move configuration files around otherwise the changes are not picked up. This has caused me to lose many hours while trying to figure out the JMSSerializerBundle.

Finally, if your /api path is behind a firewall, first login to your application, and then access your newly created REST service by calling /api/users/{userId}.json. You should see an output like the following:

{
    "username": "burak",
    "username_canonical": "burak",
    "email": "burak@localhost.localdomain",
    "email_canonical": "burak@localhost.localdomain",
    "dob": "1934-12-31T16:00:00-0800",
    "name_first": "burak",
    "name_last": "s",
    "created_at": "2012-04-06T16:18:04-0700",
    "modified_at": "2012-04-06T18:19:10-0700",
    "id": "4f7f79ac7f8b9a000f000001"
}

In my next article, I will write about securing your REST service with OAuth using FOSOAuthServerBundle.

Securing Symfony2 REST services with FOSOAuthServerBundle

Overview

In my previous article, I wrote about setting up a Symfony2 REST service using FOSRestBundle. However, this REST service was behind a firewall protected by a generic form_login provider. Not really ideal if you wish to open your REST API to other applications. So in this article, I will try to explain how to set up FOSOAuthServerBundle to protect your REST API methods using OAuth2. Before we start getting into the gritty details, it is a good idea to have a look at the official OAuth2 documentation. Let's begin...

FOSOAuthServerBundle Installation

You have to install v1.1.0 of FOSOAuthServerBundle if you are using Symfony 2.0.x. If not, see the docs.

First, add the following entries to your deps file:

[FOSOAuthServerBundle]
    git=git://github.com/FriendsOfSymfony/FOSOAuthServerBundle.git
    target=bundles/FOS/OAuthServerBundle
    version=origin/1.1.x
[oauth2-php]
    git=git://github.com/FriendsOfSymfony/oauth2-php.git

Run the vendors script to install these dependencies:

php bin/vendors install

Update your app/autoload.php file:

$loader->registerNamespaces(array(
    
    'FOS'    => __DIR__.'/../vendor/bundles',
    'OAuth2' => __DIR__.'/../vendor/oauth2-php/lib',

));

Register the FOSOAuthServerBundle in your app/AppKernel.php file:

public function registerBundles()
{
    $bundles = array(
        
        new FOS\OAuthServerBundle\FOSOAuthServerBundle(),

    );
}

Model Setup

Because I will be using MongoDB as the backend, we have to setup our document classes. First, create a new bundle called AcmeOAuthServerBundle by running "php app/console generate:bundle". Next, create the following model files in the Document directory.

In regards to the client clasas, I decided to add a "name" property in order to display an identity to a user on the authorization page. All other document files contain the bare minimum properties required by the FOSOAuthServerBundle as illustrated below.

src/Acme/OAuthServerBundle/Document/Client.php

namespace Acme\OAuthServerBundle\Document;

use FOS\OAuthServerBundle\Document\Client as BaseClient;

class Client extends BaseClient 
{
    protected $id;
    protected $name;
    
    public function __construct()
    {
        parent::__construct();
    }
    
    public function getName()
    {
        return $this->name;
    }

    public function setName($name)
    {
        $this->name = $name;
    }    

}

src/Acme/OAuthServerBundle/Resources/config/doctrine/Client.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\OAuthServerBundle\Document\Client" db="acme" collection="oauthClient" customId="true">
        
        <field fieldName="id" id="true" strategy="AUTO" />
        <field fieldName="name" type="string" />
        
    </document>
    
</doctrine-mongo-mapping>

src/Acme/OAuthServerBundle/Document/AccessToken.php

namespace Acme\OAuthServerBundle\Document;

use FOS\OAuthServerBundle\Document\AccessToken as BaseAccessToken;
use FOS\OAuthServerBundle\Model\ClientInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class AccessToken extends BaseAccessToken 
{
    protected $id;
    protected $client;
    protected $user;
    
    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function setUser(UserInterface $user)
    {
        $this->user = $user;
    }
    
}

src/Acme/OAuthServerBundle/Resources/config/doctrine/AccessToken.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\OAuthServerBundle\Document\AccessToken" db="acme" collection="oauthAccessToken" customId="true">
        
        <field fieldName="id" id="true" strategy="AUTO" />
        
        <reference-one target-document="Acme\OAuthServerBundle\Document\Client" field="client" />
        <reference-one target-document="Acme\UserBundle\Document\User" field="user" />
        
    </document>
    
</doctrine-mongo-mapping>

src/Acme/OAuthServerBundle/Document/RefreshToken.php

namespace Acme\OAuthServerBundle\Document;

use FOS\OAuthServerBundle\Document\RefreshToken as BaseRefreshToken;
use FOS\OAuthServerBundle\Model\ClientInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class RefreshToken extends BaseRefreshToken 
{
    protected $id;
    protected $client;
    protected $user;
    
    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function setUser(UserInterface $user)
    {
        $this->user = $user;
    }
    
}

src/Acme/OAuthServerBundle/Resources/config/doctrine/RefreshToken.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\OAuthServerBundle\Document\RefreshToken" db="acme" collection="oauthRefreshToken" customId="true">
        
        <field fieldName="id" id="true" strategy="AUTO" />
        
        <reference-one target-document="Acme\OAuthServerBundle\Document\Client" field="client" />
        <reference-one target-document="Acme\UserBundle\Document\User" field="user" />
        
    </document>
    
</doctrine-mongo-mapping>

src/Acme/OAuthServerBundle/Document/AuthCode.php

namespace Acme\OAuthServerBundle\Document;

use FOS\OAuthServerBundle\Document\AuthCode as BaseAuthCode;
use FOS\OAuthServerBundle\Model\ClientInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class AuthCode extends BaseAuthCode 
{
    protected $id;
    protected $client;
    protected $user;
    
    public function getClient()
    {
        return $this->client;
    }

    public function setClient(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function getUser()
    {
        return $this->user;
    }

    public function setUser(UserInterface $user)
    {
        $this->user = $user;
    }
    
}

src/Acme/OAuthServerBundle/Resources/config/doctrine/AuthCode.mongodb.xml

<doctrine-mongo-mapping xmlns="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping
                    http://doctrine-project.org/schemas/odm/doctrine-mongo-mapping.xsd">

    <document name="Acme\OAuthServerBundle\Document\AuthCode" db="acme" collection="oauthAuthCode" customId="true">
        
        <field fieldName="id" id="true" strategy="AUTO" />
        
        <reference-one target-document="Acme\OAuthServerBundle\Document\Client" field="client" />
        <reference-one target-document="Acme\UserBundle\Document\User" field="user" />
        
    </document>
    
</doctrine-mongo-mapping>

Update your app/config/config.yml to register your MongoDB model files:

fos_oauth_server:
    db_driver:  mongodb
    client_class:        Acme\OAuthServerBundle\Document\Client
    access_token_class:  Acme\OAuthServerBundle\Document\AccessToken
    refresh_token_class: Acme\OAuthServerBundle\Document\RefreshToken
    auth_code_class:     Acme\OAuthServerBundle\Document\AuthCode

Security Configuration

Once the model setup is complete, we are going to setup the security.yml file. We are going to enforce authentication before accessing the /oauth/v2/auth route. This will be accomplished by adding a login form before displaying the authorization page. In addition, since I am already using FOSUserBundle, I will set the security.firewalls.oauth_authorize.form_login.provider configuration parameter to fos_userbundle. This means that once the login form is filled out and posted, the user will then be redirected to the FOSOAuthServerBundle authorization end point.

First, add the following to the app/config/security.yml file:

security:
    firewalls:
        api:
            pattern: ^/api
            fos_oauth: true
            stateless: true
        oauth_authorize:
            pattern: ^/oauth/v2/auth 
            form_login:
                provider: fos_userbundle
                check_path: /oauth/v2/auth_login_check
                login_path: /oauth/v2/auth_login
            anonymous: true
        oauth_token:
            pattern: ^/oauth/v2/token
            security: false      

    access_control:
        - { path: ^/oauth/v2/auth_login$, role: IS_AUTHENTICATED_ANONYMOUSLY }

If you are running Symfony version 2.0.X add the following to your app/config/config.yml file:

imports:
    - { resource: "@FOSOAuthServerBundle/Resources/config/security.yml" }

Here is what the above configuration means. We are going to protect our authorization endpoint (/oauth/v2/auth) with the Symfony2's built in form_login authentication provider. Basically, any unauthenticated user will be forwarded to /oauth/v2/oauth_login page. Once the form is submitted and the user is successfully authenticated, he/she will be forwarded to the the /oauth/v2/auth endpoint where another form asking for an authorization grant to the respective client be displayed. Once the user grants access, he/she will be redirected to the redirect_uri as specified by the OAuth2 documentation.

The redirection to the /oauth/v2/auth will take place using the built in referrer redirect functionality of the Symfony security component. This is an important assumption as the OAuth2 flow will not work if the OAuth2 client makes a request to any URL other than the /oauth/v2/auth endpoint. The previous sentence will become much clearer when you read the "testing" section at the end of this article. You may want to have a look at the Security Configuration Reference for more information about the successful authentication redirection flow and its configuration.

First, we need to create our login and authorization forms. This part is pretty straight forward and outside the scope of this article, so I will skip the lengthy explanations.

src/Acme/OAuthServerBundle/Form/Type/AuthorizeFormType.php

namespace Acme\OAuthServerBundle\Form\Type;

use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\AbstractType;

class AuthorizeFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('allowAccess', 'checkbox', array(
            'label' => 'Allow access',
        ));
    }

    public function getDefaultOptions(array $options)
    {
        return array('data_class' => 'Acme\OAuthServerBundle\Form\Model\Authorize');
    }

    public function getName()
    {
        return 'acme_oauth_server_authorize';
    }
    
}

src/Acme/OAuthServerBundle/Form/Model/Authorize.php

namespace Acme\OAuthServerBundle\Form\Model;

class Authorize
{
    protected $allowAccess;
    
    public function getAllowAccess()
    {
        return $this->allowAccess;
    }

    public function setAllowAccess($allowAccess)
    {
        $this->allowAccess = $allowAccess;
    }
}

src/Acme/OAuthServerBundle/Form/Handler/AuthorizeFormHandler.php

namespace Acme\OAuthServerBundle\Form\Handler;

use Symfony\Component\Form\Form;
use Symfony\Component\HttpFoundation\Request;
use Acme\OAuthServerBundle\Form\Model\Authorize;
use Symfony\Component\Security\Core\SecurityContextInterface;
use OAuth2\OAuth2;
use OAuth2\OAuth2ServerException;
use OAuth2\OAuth2RedirectException;

class AuthorizeFormHandler
{
    protected $request;
    protected $form;
    protected $context;
    protected $oauth2;

    public function __construct(Form $form, Request $request, SecurityContextInterface $context, OAuth2 $oauth2)
    {
        $this->form = $form;
        $this->request = $request;
        $this->context = $context;
        $this->oauth2 = $oauth2;
    }

    public function process(Authorize $authorize)
    {
        $this->form->setData($authorize);

        if ($this->request->getMethod() == 'POST') {
            
            $this->form->bindRequest($this->request);

            if ($this->form->isValid()) {
                
                try {
                    $user = $this->context->getToken()->getUser();
                    return $this->oauth2->finishClientAuthorization(true, $user, $this->request, null);
                } catch (OAuth2ServerException $e) {
                    return $e->getHttpResponse();
                }
                
            }
            
        }

        return false;
    }

}

src/Acme/OAuthServerBundle/Resources/config/services.xml

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        
        <service id="acme_oauth_server.authorize.form_type" class="Acme\OAuthServerBundle\Form\Type\AuthorizeFormType">
        </service>
        
        <service id="acme_oauth_server.authorize.form" factory-method="createNamed" factory-service="form.factory" class="Symfony\Component\Form\Form">
            <argument type="service" id="acme_oauth_server.authorize.form_type" />
            <argument>acme_oauth_server_auth</argument>
        </service>
        
        <service id="acme_oauth_server.authorize.form_handler" class="Acme\OAuthServerBundle\Form\Handler\AuthorizeFormHandler" scope="request">
            <argument type="service" id="acme_oauth_server.authorize.form" />
            <argument type="service" id="request" />
            <argument type="service" id="security.context" />
            <argument type="service" id="fos_oauth_server.server" />
        </service>
        
     </services>
     
</container>

src/Acme/OAuthServerBundle/Resources/config/validation.xml

<constraint-mapping xmlns="http://symfony.com/schema/dic/constraint-mapping"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/constraint-mapping
        http://symfony.com/schema/dic/services/constraint-mapping-1.0.xsd">

    <class name="Acme\OAuthServerBundle\Form\Model\Authorize">

        <property name="allowAccess">
            <constraint name="True">
                <option name="message">Please check the checkbox to allow access to your profile.</option>
                <option name="groups">
                    <value>Authorize</value>
                </option>
            </constraint>
        </property>
        
    </class>
    
</constraint-mapping>

Once the form setup is complete, override the AuthorizeController class. In order to do this, we need to ensure that our bundle is a child of FOSOauthServerBundle by adding the the getParent method call to your src/Acme/OAuthServerBundle/AcmeOAuthServerBundle.php file:

namespace Acme\OAuthServerBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class AcmeOAuthServerBundle extends Bundle
{
    public function getParent()
    {
        return 'FOSOAuthServerBundle';
    }
}

Create the new AuthorizeController at src/Acme/OAuthServerBundle/Controller/AuthorizeController.php

namespace Acme\OAuthServerBundle\Controller;

use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\Request;
use FOS\OAuthServerBundle\Controller\AuthorizeController as BaseAuthorizeController;
use Acme\OAuthServerBundle\Form\Model\Authorize;
use Acme\OAuthServerBundle\Document\Client;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class AuthorizeController extends BaseAuthorizeController
{
    public function authorizeAction(Request $request)
    {
        if (!$request->get('client_id')) {
            throw new NotFoundHttpException("Client id parameter {$request->get('client_id')} is missing.");
        }
        
        $clientManager = $this->container->get('fos_oauth_server.client_manager.default');
        $client = $clientManager->findClientByPublicId($request->get('client_id'));
        
        if (!($client instanceof Client)) {
            throw new NotFoundHttpException("Client {$request->get('client_id')} is not found.");
        }
        
        $user = $this->container->get('security.context')->getToken()->getUser();
        
        $form = $this->container->get('acme_oauth_server.authorize.form');
        $formHandler = $this->container->get('acme_oauth_server.authorize.form_handler');
        
        $authorize = new Authorize();
        
        if (($response = $formHandler->process($authorize)) !== false) {
            return $response;
        }
                
        return $this->container->get('templating')->renderResponse('AcmeOAuthServerBundle:Authorize:authorize.html.php', array(
            'form' => $form->createView(),
            'client' => $client,
        ));
    }
}

Next, add a new controller named SecurityController located at src/Acme/OAuthServerBundle/Controller/SecurityController.php. This controller will handle the form_login authorization for us.

namespace Acme\OAuthServerBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
    public function loginAction(Request $request)
    {
        $session = $request->getSession();
        
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
        } elseif (null !== $session && $session->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        } else {
            $error = '';
        }

        if ($error) {
            $error = $error->getMessage(); // WARNING! Symfony source code identifies this line as a potential security threat.
        }
        
        $lastUsername = (null === $session) ? '' : $session->get(SecurityContext::LAST_USERNAME);

        return $this->render('AcmeOAuthServerBundle:Security:login.html.php', array(
            'last_username' => $lastUsername,
            'error'         => $error,
        ));
    }
    
    public function loginCheckAction(Request $request)
    {
        
    }
}

Define your routes for these actions in your src/Acme/OAuthServerBundle/Resources/config/routing/security.xml

<routes xmlns="http://symfony.com/schema/routing"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">

    <route id="acme_oauth_server_auth_login" pattern="/oauth/v2/auth_login">
        <default key="_controller">AcmeOAuthServerBundle:Security:login</default>
    </route>
    
    <route id="acme_oauth_server_auth_login_check" pattern="/oauth/v2/auth_login_check">
        <default key="_controller">AcmeOAuthServerBundle:Security:loginCheck</default>
    </route>
    
</routes>

Next, update your app/config/routing.yml file:

fos_oauth_server_token:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

fos_oauth_server_authorize:
    resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"
    
acme_oauth_server_security:
    resource: "@AcmeOAuthServerBundle/Resources/config/routing/security.xml"
    prefix: /

Next, we need to setup our views. We need one for the login form with _username and _password fields that are processed by the security component's authorization listener.

src/Acme/OAuthServerBundle/Resources/views/Security/login.html.php

<?php $view['slots']->start('body') ?>
    <div class="form">
        <form id="login" class="vertical" action="<?php echo $view['router']->generate('acme_oauth_server_auth_login_check') ?>" method="post">
            <div class="form_title">
                OAuth Authorization
            </div>
            <?php if ($error): ?>
                <div class='form_error'><?php echo $view->escape($error); ?></div>
            <?php endif; ?>
            <div class="form_item">
                <div class="form_label"><label for="username">Username</label>:</div>
                <div class="form_widget"><input type="text" id="username" name="_username" /></div>
            </div>
            <div class="form_item">
                <div class="form_label"><label for="password">Password</label>:</div>
                <div class="form_widget"><input type="password" id="password" name="_password" /></div>
            </div>
            <div class="form_button">
                <input type="submit" id="_submit" name="_submit" value="Log In" />
            </div>
        </form>
    </div>
<?php $view['slots']->stop() ?>

Here is the other form that allows the user to grant access to a OAuth2 client:

src/Acme/OAuthServerBundle/Resources/views/Authorize/authorize.html.php

<?php $view['slots']->start('body') ?>
    <div class="form">
        <form class="vertical" action="<?php echo $view['router']->generate('fos_oauth_server_authorize', array(
            'client_id' => $view['request']->getParameter('client_id'),
            'response_type' => $view['request']->getParameter('response_type'),
            'redirect_uri' => $view['request']->getParameter('redirect_uri'),
            'state' => $view['request']->getParameter('state'),
            'scope' => $view['request']->getParameter('scope'),
        )) ?>" method="POST"  <?php echo $view['form']->enctype($form); ?>>
            <div class="form_title">
                Grant access to <?php echo $view->escape($client->getName()); ?>?
            </div>
            <?php echo $view['form']->widget($form) ?>
            <div class="form_button">
                <input type="submit" value="Authorize" />
            </div>
        </form>
    </div>
<?php $view['slots']->stop() ?>

Testing

Create a new client

First, lets add a command line utility to our bundle to create a new OAuth2 client in MongoDB:

src/Acme/OAuthServerBundle/Command/ClientCreateCommand.php

namespace Acme\OAuthServerBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Acme\OAuthServerBundle\Document\Client;

class ClientCreateCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('acme:oauth-server:client:create')
            ->setDescription('Creates a new client')
            ->addArgument('name', InputArgument::REQUIRED, 'Sets the client name', null)
            ->addOption('redirect-uri', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Sets redirect uri for client. Use this option multiple times to set multiple redirect URIs.', null)
            ->addOption('grant-type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Sets allowed grant type for client. Use this option multiple times to set multiple grant types..', null)
            ->setHelp(<<<EOT
The <info>%command.name%</info>command creates a new client.

  <info>php %command.full_name% [--redirect-uri=...] [--grant-type=...] name</info>
   
EOT
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
        $client = $clientManager->createClient();
        $client->setName($input->getArgument('name'));
        $client->setRedirectUris($input->getOption('redirect-uri'));
        $client->setAllowedGrantTypes($input->getOption('grant-type'));
        $clientManager->updateClient($client);
        $output->writeln(sprintf('Added a new client with name <info>%s</info> and public id <info>%s</info>.', $client->getName(), $client->getPublicId()));        
    }
}

Now, run the following command to create a new client.

[burak@localhost platform]$ php app/console acme:oauth-server:client:create --redirect-uri=http://www.google.com --grant-type=token --grant-type=authorization_code ClientName
Added a new client with name ClientName and public id 4f8e5bb57f8b9a0816000000_1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s.

As you can see below, we have created a new client entry in our oauthClient MongoDB collection:

{
   "_id": ObjectId("4f8e5bb57f8b9a0816000000"),
   "randomId": "1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s",
   "redirectUris": {
     "0": "http:\/\/www.google.com"
  },
   "secret": "147v1qcgxvuscg4owg4480ww484kc0ow0cwgkw0c4g4g8oowkc",
   "allowedGrantTypes": {
     "0": "token",
     "1": "authorization_code"
  },
   "name": "ClientName"
}

Now we can use this client's public id to make some test requests. OAuth2 supports four types of authorization grant flows:

The Authorization Code Grant flow generates an authorization code when the user grants access and the OAuth2 client needs to make a subsequent request to get the access and refresh tokens.

In the Implicit Grant flow, an access token is immediately generated and sent back to the OAuth2 client. Refresh tokens are not supported in this flow.

In the Resource Owner Password Credentials Grant flow, the OAuth2 client sends the user's login name and password to the OAuth2 authorization server to get the access and refresh tokens. This implies a trust relationship between the user and the OAuth2 client.

Finally, in the Client Credentials Grant flow, the OAuth2 client authenticates with the server and requests an access token directly. According to the documentation, a refresh token should not be included in this flow.

In our case, we will be testing the Authorization Code Grant and Implicit Grant flows.

Authorization Code Grant Flow

Open your browser and enter the following into your address bar:

http://acme.localhost/app_dev.php/oauth/v2/auth?client_id=4f8e5bb57f8b9a0816000000_1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s&response_type=code&redirect_uri=http%3A%2F%2Fwww.google.com

After you login and grant access, you should be redirected to:

http://www.google.com/?code=6c7136745d8556650cb5e0d5cd53029c925aae72

In fact, your MongoDB oauthAuthCode collection should also have a new record:

{
   "_id": ObjectId("4f8e64b97f8b9a8d05000000"),
   "token": "6c7136745d8556650cb5e0d5cd53029c925aae72",
   "redirectUri": "http: \/\/www.google.com",
   "expiresAt": 1334731991,
   "scope": null,
   "client": {
     "$ref": "oauthClient",
     "$id": ObjectId("4f8e5bb57f8b9a0816000000"),
     "$db": "acme"
   },
   "user": {
     "$ref": "user",
     "$id": ObjectId("4f7f79ac7f8b9a000f000001"),
     "$db": "acme"
   } 
}

Now, call the /oauth/v2/token endpoint to get your access and refresht tokens. Enter the following URL in your browser's address bar:

http://acme.localhost/app_dev.php/oauth/v2/token?client_id=4f8e5bb57f8b9a0816000000_1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s&client_secret=147v1qcgxvuscg4owg4480ww484kc0ow0cwgkw0c4g4g8oowkc&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.google.com&code=6c7136745d8556650cb5e0d5cd53029c925aae72
{
    "access_token": "8315796acc79f6a1bfb4e4935aea01362d59ecce",
    "expires_in": 3600,
    "token_type": "bearer",
    "scope": null,
    "refresh_token": "da359ceafe501fd2445df0a6c406953264e54c47"
}

Access token stored in MongoDB

{
   "_id": ObjectId("4f8e67d87f8b9a8e05000001"),
   "token": "8315796acc79f6a1bfb4e4935aea01362d59ecce",
   "expiresAt": 1334736360,
   "scope": null,
   "client": {
     "$ref": "oauthClient",
     "$id": ObjectId("4f8e5bb57f8b9a0816000000"),
     "$db": "acme"
   },
   "user": {
     "$ref": "user",
     "$id": ObjectId("4f7f79ac7f8b9a000f000001"),
     "$db": "acme"
   }
}

Refresh token stored in MongoDB

{
   "_id": ObjectId("4f8e67d87f8b9a8e05000002"),
   "token": "da359ceafe501fd2445df0a6c406953264e54c47",
   "expiresAt": 1335942360,
   "scope": null,
   "client": {
     "$ref": "oauthClient",
     "$id": ObjectId("4f8e5bb57f8b9a0816000000"),
     "$db": "acme"
   },
   "user": {
     "$ref": "user",
     "$id": ObjectId("4f7f79ac7f8b9a000f000001"),
     "$db": "acme"
   }
}

Implicit Grant Flow

I was able to test this flow only after manually editing the FOS/OAuthServerBundle/Storage/OAuthStorage.php file and implementing the IOAuth2GrantImplicit interface.

use OAuth2\IOAuth2GrantImplicit;

class OAuthStorage implements IOAuth2RefreshTokens, IOAuth2GrantUser, IOAuth2GrantCode, IOAuth2GrantImplicit
{
...
}

As of the date of this article, the implicit grant flow does not work without these change. Nevertheless, I have decided to include section just for reference.

UPDATE 2012/05/28: This issue is fixed in the master branch.

Anyway, to test the implicit grant flow, open your browser and enter the following into your address bar:

http://acme.localhost/app_dev.php/oauth/v2/auth?client_id=4f8e5bb57f8b9a0816000000_1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s&redirect_uri=http%3A%2F%2Fwww.google.com&response_type=token

After you login and authorize the request, you should be redirected with your access and refresh tokens right away:

http://www.google.com/#access_token=1a4b82f02bdb7425d14948d686f76e777e5d0a65&expires_in=3600&token_type=bearer&refresh_token=43937901d873da5e80a8f59c632b6eab59988c54

Obviously, this is not really proper testing. It would be much better if an actual application makes these requests. So in one of my future articles, I will try to write a tutorial about an Android client making requests to a Symfony2 REST API protected by FOSOAuthServerBundle. For now, that's it. Phew...

UPDATE 2012/06/04: Modified the article to reflect some changes introduced in branch 1.1.x.