Let’s say you built an API using Symfony and you need to access it from a mobile application using authenticated requests on behalf of your users.

Here’s how to make this work using Symfony 2.8 and Doctrine.

Install FOSOAuthServerBundle

We will use the FOSOAuthServerBundle to implement this feature. Install it using the following command:

composer require friendsofsymfony/oauth-server-bundle

Next, enable the bundle in the AppKernel.php file:

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

Create OAuth model classes

To create the OAuth model classes just add the following files to your project. Here we already have FOSUserBundle installed and set up to use the ApiBundle\Entity\User class.

src/ApiBundle/Entity/Client.php

<?php
namespace ApiBundle\Entity;

use FOS\OAuthServerBundle\Entity\Client as BaseClient;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class Client extends BaseClient
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    public function __construct()
    {
        parent::__construct();
        // your own logic
    }
}

src/ApiBundle/Entity/AccessToken.php

<?php
namespace ApiBundle\Entity;

use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class AccessToken extends BaseAccessToken
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

src/ApiBundle/Entity/RefreshToken.php

<?php
namespace ApiBundle\Entity;

use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class RefreshToken extends BaseRefreshToken
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

src/ApiBundle/Entity/AuthCode.php

<?php
namespace ApiBundle\Entity;

use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 */
class AuthCode extends BaseAuthCode
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /**
     * @ORM\ManyToOne(targetEntity="Client")
     * @ORM\JoinColumn(nullable=false)
     */
    protected $client;

    /**
     * @ORM\ManyToOne(targetEntity="User")
     */
    protected $user;
}

Configure FOSOAuthServerBundle

Import the routing configuration in 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"

Add FOSOAuthServerBundle settings in app/config/config.yml:

fos_oauth_server:
    db_driver: orm       # Drivers available: orm, mongodb, or propel
    client_class:        ApiBundle\Entity\Client
    access_token_class:  ApiBundle\Entity\AccessToken
    refresh_token_class: ApiBundle\Entity\RefreshToken
    auth_code_class:     ApiBundle\Entity\AuthCode
    service:
        user_provider: fos_user.user_provider.username

Back to the models

Generate a migration and migrate the database:

php app/console doctrine:migrations:diff
php app/console doctrine:migrations:migrate

…or, if you’re not using migrations, just update the database schema:

php app/console doctrine:schema:update --force

Configure your application’s security

Edit your app/config/security.yml file to add FOSOAuthServerBundle specific configuration:

# ...

    firewalls:
        oauth_token: # Everyone can access the access token URL.
            pattern: ^/oauth/v2/token
            security: false
            
        api:
            pattern:    ^/api
            fos_oauth:  true
            stateless:  true
            anonymous:  true # can be omitted as its default value
    
    # ...

    access_control:
        - { path: ^/api, role: IS_AUTHENTICATED_FULLY }

Create a client

Before you can generate tokens, you need to create a Client using the ClientManager. For this, create a new Symfony command:

<?php
namespace ApiBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class OAuthAddClientCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('oauth:add-client')
            ->setDescription("Ads a new client for OAuth")
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $redirectUri = $this->getContainer()->getParameter('router.request_context.scheme') . "://" . $this->getContainer()->getParameter('router.request_context.host');
        $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default');
        $client = $clientManager->createClient();
        $client->setRedirectUris(array($redirectUri));
        $client->setAllowedGrantTypes(array('refresh_token', 'password'));
        $clientManager->updateClient($client);
    }
}

Now run the above command to generate your first OAuth client:

php app/console oauth:add-client

This client will be able to generate tokens and refresh tokens using the user’s username and password. You can find it’s data in the database client table. The token endpoint is at /oauth/v2/token by default.

Document using NelmioApiDocBundle

If you use the NelmioApiDocBundle to document your API, you can add these OAuth methods too. Create a new YAML file in src/ApiBundle/Resources/apidoc/oauth.yml:

grant_type_password:
    requirements: []
    views: []
    filters: []
    parameters:
        grant_type:
            dataType: string
            required: true
            name: grant_type
            description: Grant Type (password)
            readonly: false
        client_id:
            dataType: string
            required: true
            name: client_id
            description: Client Id
            readonly: false
        client_secret:
            dataType: string
            required: true
            name: client_secret
            description: client Secret
            readonly: false
        username:
            dataType: string
            required: true
            name: username
            description: Username
            readonly: false
        password:
            dataType: string
            required: true
            name: password
            description: Password
            readonly: false
    input: null
    output: null
    link: null
    description: "Get OAuth token for user using username and password"
    section: "OAuth"
    documentation: null
    resource: null
    method: "POST"
    host: ""
    uri: "/oauth/v2/token"
    response:
        token:
            dataType: string
            required: true
            description: OAuth token
            readonly: true
    route:
        path: /oauth/v2/token
        defaults:
            _controller: FOS\UserBundle\Controller\SecurityController::checkAction
        requirements: []
        options:
            compiler_class: Symfony\Component\Routing\RouteCompiler
        host: ''
        schemes: []
        methods: [ 'POST' ]
        condition: ''
    https: false
    authentication: false
    authenticationRoles: []
    cache: null
    deprecated: false
    statusCodes: []
    resourceDescription: null
    responseMap: []
    parsedResponseMap: []
    tags: []
    
grant_type_refresh_token:
    requirements: []
    views: []
    filters: []
    parameters:
        grant_type:
            dataType: string
            required: true
            name: grant_type
            description: Grant Type (refresh_token)
            readonly: false
        client_id:
            dataType: string
            required: true
            name: client_id
            description: Client Id
            readonly: false
        client_secret:
            dataType: string
            required: true
            name: client_secret
            description: client Secret
            readonly: false
        refresh_token:
            dataType: string
            required: true
            name: refresh_token
            description: Refresh token
            readonly: false
    input: null
    output: null
    link: null
    description: "Get new OAuth token using refresh token"
    section: "OAuth"
    documentation: null
    resource: null
    method: "POST"
    host: ""
    uri: "/oauth/v2/token"
    response:
        token:
            dataType: string
            required: true
            description: OAuth token
            readonly: true
    route:
        path: /oauth/v2/token
        defaults:
            _controller: FOS\UserBundle\Controller\SecurityController::checkAction
        requirements: []
        options:
            compiler_class: Symfony\Component\Routing\RouteCompiler
        host: ''
        schemes: []
        methods: [ 'POST' ]
        condition: ''
    https: false
    authentication: false
    authenticationRoles: []
    cache: null
    deprecated: false
    statusCodes: []
    resourceDescription: null
    responseMap: []
    parsedResponseMap: []
    tags: []

Add a new NelmioApiYmlProvider.php file in src/ApiBundle/Service folder:

<?php

namespace ApiBundle\Service;

use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Routing\Route;
use Symfony\Component\Yaml\Yaml;

/**
 * Generate annotations for vendor routes to be displayed in Nelmio ApiDoc.
 */
class NelmioApiYmlProvider implements AnnotationsProviderInterface
{
    private $vendorFolder;

    public function __construct($vendorFolder)
    {
        $this->vendorFolder = $vendorFolder;
    }
    /**
     * {@inheritdoc}
     */
    public function getAnnotations()
    {
        $annotations = [];
        $configDirectories = array($this->vendorFolder);

        $finder = new Finder();

        $finder->files()->in($configDirectories);

        if (count($finder) == 0) {
            return $annotations;
        }

        foreach ($finder as $file_) {
            $data = Yaml::parse(file_get_contents($file_));

            $vendors = array_keys($data);
            foreach ($vendors as $vendor) {
                $apiDoc = new ApiDoc($data[$vendor]);
                $route = new Route(
                    $data[$vendor]['route']['path'],
                    $data[$vendor]['route']['defaults'],
                    $data[$vendor]['route']['requirements'],
                    $data[$vendor]['route']['options'],
                    $data[$vendor]['route']['host'],
                    $data[$vendor]['route']['schemes'],
                    $data[$vendor]['route']['methods'],
                    $data[$vendor]['route']['condition']
                );

                $apiDoc->setRoute($route);
                $apiDoc->setResponse($data[$vendor]['response']);
                $annotations[] = $apiDoc;
            }
        }

        return $annotations;
    }
}

Add a new service in src/ApiBundle/Resources/config/services.yml file:

services:
    nelmio_api_doc.yml_provider.api_yml_provider:
        class: ApiBundle\Service\NelmioApiYmlProvider
        arguments:
            folder: %kernel.root_dir%/../src/ApiBundle/Resources/apidoc
        tags:
            - { name: nelmio_api_doc.extractor.annotations_provider }

You’ll find now two /oauth/v2/token methods with different parameters listed in the api/doc section of your project.

That’s all! You can now use the generated client to authenticate your users in your mobile app using OAuth.

How to use the FOSOAuthServerBundle

First you will need to get an access token by making a POST request to the /oauth/v2/token endpoint with the following parameters:

grant_type=password
client_id=[client's id from the database followed by '_' then the corresponding random id]
client_secret=[client's secret]
username=[user's username]
password=[users's password]

You should get back something like this:

{
  "access_token": "ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw",
  "expires_in": 3600,
  "token_type": "bearer",
  "scope": null,
  "refresh_token": "MDQ3MGIwZTk5MDkwOGM5NjhkMzk5NTUyZDJjZmYwM2YzZWViZDFhZjk0NTIyZmNjNzkyMDM0YjM4ODQ2N2VhNg"
}

Use the access token for authenticated requests by placing it in the request header:

Authorization: Bearer ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw

When the access token expires, you can get a new one using the refresh_token grant type at the same /oauth/v2/token endpoint:

grant_type=refresh_token
client_id=[client's id from the database followed by '_' then the corresponding random id]
client_secret=[client's secret]
refresh_token=[refresh token received earlier]

The response should be similar to:

{
  "access_token": "MjE1NjRjNDc0ZmU4NmU3NjgzOTIyZDZlNDBiMTg5OGNhMTc0MjM5OWU3MjAxN2ZjNzAwOTk4NGQxMjE5ODVhZA",
  "expires_in": 3600,
  "token_type": "bearer",
  "scope": null,
  "refresh_token": "YzM2ZWNiMGQ5MDBmOGExNjhmNDI1YjExZTkyN2U0Mzk5ZmM4NzcwNDdhNjAzZDliMjY3YzE0ZTg5NDFlZjg3MQ"
}