Symfony OAuth Authentication for Your Mobile Application

Dragos Holban
5 min readSep 6, 2017

--

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:

<?php

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="ApiBundle\Entity\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="ApiBundle\Entity\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="ApiBundle\Entity\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:

<?phpnamespace 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"
}

Originally published at intelligentbee.com.

--

--