This example shows us how to create an OAuth2 API with symfony and uses client_credentials grant type. I'll go step by step so that it becomes easy to follow. If the API doesn't use role_hierarchy to control controller access with @Security("has_role('xxxxx')") in annotations, client_credentials grant type is the best/simplest option to use. See the reference links at the bottom of the post. We're going to use FOSOAuthServerBundle to secure our API.


Important


In example below, I expose client_id and client_secret in request URI to obtain access token for demonstration purposes. You should not do that in real world applications. Instead, you should encode both of them with base64_encode function and add it to request header as Authorization.


$clientId = 'i-am-client-id';
$clientSecret = 'i-am-client-secret';

$base64 = base64_encode($clientId.':'.$clientSecret);

$header = 'Basic '.$base64;

So you should use it like Authorization: Basic aS1hbS1jbGllbnQtaWQ6aS1hbS1jbGllbnQtc2VjcmV0 in your request. On top of that, you should remove grant_type from URI to send them as parameters encoded with application/x-www-form-urlencoded. Your final request should look like the one below.


curl -X POST
-H 'Authorization: Basic aS1hbS1jbGllbnQtaWQ6aS1hbS1jbGllbnQtc2VjcmV0'
-H 'content-type: application/x-www-form-urlencoded'
-d 'grant_type=client_credentials'
http://oauth-server.dev/app_dev.php/oauth/v2/token

Facts



Entity association



Setup unsecured API


Virtual Host


# /etc/apache2/sites-available/oauth-server.dev.conf

<VirtualHost *:80>
ServerName oauth-server.dev
DocumentRoot "/var/www/html/local/oauth-server/web"

<Directory "/var/www/html/local/oauth-server/web">
Options Indexes FollowSymlinks
AllowOverride All
Order allow,deny
Allow from all
</Directory>

ErrorLog ${APACHE_LOG_DIR}/oauth-server.dev.error.log
CustomLog ${APACHE_LOG_DIR}/oauth-server.dev.access.log combined
</VirtualHost>

# etc/hosts

127.0.0.1 oauth-server.dev

$ sudo service apache2 restart

API domain


Setup http://oauth-server.dev/ virtual host.


Install symfony application and bundle


composer create-project symfony/framework-standard-edition oauth-server "2.6.9"
app/console generate:bundle --namespace=Application/ServerBundle

Configuration


Prefix /1/ represents API version in URL.


# oauth-server/app/config/routing.yml
application_server:
resource: "@ApplicationServerBundle/Controller"
prefix: /1/
type: annotation

ServerController


namespace Application\ServerBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("server", service="application_server.controller.server")
*/
class ServerController extends Controller
{
/**
* @param Request $request
*
* @Method({"GET"})
* @Route("/team")
*
* @return Response
*/
public function getTeamAction(Request $request)
{
return new Response('GET your team from Server');
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("/team")
*
* @return Response
*/
public function createTeamAction(Request $request)
{
return new Response('POST your team to Server');
}
}

services:
application_server.controller.server:
class: Application\ServerBundle\Controller\ServerController

Tests


# Request
GET http://oauth-server.dev/app_dev.php/1/server/team

# Response
HTTP/1.1 200 OK - GET your team from Server

# Request
POST http://oauth-server.dev/app_dev.php/1/server/team
{
"hello": "world"
}

# Response
HTTP/1.1 200 OK - POST your team to Server

Secure unsecured API


Composer


Install "friendsofsymfony/oauth-server-bundle": "1.4.2" with composer and enable it with new FOS\OAuthServerBundle\FOSOAuthServerBundle(), in AppKernel.php file.


Configuration


# oauth-server/app/config/config.yml
fos_oauth_server:
db_driver: orm
client_class: Application\ServerBundle\Entity\Client
access_token_class: Application\ServerBundle\Entity\AccessToken
refresh_token_class: FOS\OAuthServerBundle\Entity\RefreshToken # Not implemented in this example
auth_code_class: FOS\OAuthServerBundle\Entity\AuthCode # Not implemented in this example
service:
options:
access_token_lifetime: 3600

# oauth-server/app/config/routing.yml
fos_oauth_server_token:
resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

# oauth-server/app/config/security.yml
security:
providers:
in_memory:
memory: ~

firewalls:
oauth_token:
pattern: ^/oauth/v2/token
security: false

secured_area:
pattern: ^/
fos_oauth: true
stateless: true

access_control:
- { path: ^/, roles: [ IS_AUTHENTICATED_FULLY ] }

Client entity


# oauth-server/src/Application/ServerBundle/Entity/Client.php
namespace Application\ServerBundle\Entity;

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

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

AccessToken entity


# oauth-server/src/Application/ServerBundle/Entity/AccessToken.php
namespace Application\ServerBundle\Entity;

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

/**
* @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;
}

Create database and entities


app/console doctrine:database:create
app/console doctrine:schema:update --force

Tests


As you can see below, API is secured.


# Request
GET http://oauth-server.dev/app_dev.php/1/server/team

# Response
HTTP/1.1 401 Unauthorized
{
"error": "access_denied",
"error_description": "OAuth2 authentication required"
}

# Request
POST http://oauth-server.dev/app_dev.php/1/server/team
{
"hello": "world"
}

# Response
HTTP/1.1 401 Unauthorized
{
"error": "access_denied",
"error_description": "OAuth2 authentication required"
}

Create client credentials


Every client needs client_id and client_secret to generate access_token that is used in requests to consume API. Command below is used to generate client_id and client_secret.


# oauth-server/src/Application/ServerBundle/Command/CreateOauthClientCommand.php
namespace Application\ServerBundle\Command;

use FOS\OAuthServerBundle\Entity\Client;
use FOS\OAuthServerBundle\Entity\ClientManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class CreateOauthClientCommand extends Command
{
const REDIRECT_URI = 'redirect-uri';
const GRANT_TYPE = 'grant-type';

private $clientManager;

public function __construct(ClientManager $clientManager)
{
parent::__construct();

$this->clientManager = $clientManager;
}

protected function configure()
{
$this
->setName('create:oauth:client')
->setDescription('Creates a new OAuth client.')
->addOption(
self::REDIRECT_URI,
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Sets redirect uri for client. Can be used multiple times.'
)
->addOption(
self::GRANT_TYPE,
null,
InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
'Sets allowed grant type for client. Can be used multiple times.'
);
}

protected function execute(InputInterface $input, OutputInterface $output)
{
/** @var Client $client */
$client = $this->clientManager->createClient();
$client->setRedirectUris($input->getOption(self::REDIRECT_URI));
$client->setAllowedGrantTypes($input->getOption(self::GRANT_TYPE));

$this->clientManager->updateClient($client);

$this->echoCredentials($output, $client);
}

private function echoCredentials(OutputInterface $output, Client $client)
{
$output->writeln('OAuth client has been created...');
$output->writeln(sprintf('Public ID: %s', $client->getPublicId()));
$output->writeln(sprintf('Secret ID: %s', $client->getSecret()));
}
}

services:
application_server.command.create_oauth_client:
class: Application\ServerBundle\Command\CreateOauthClientCommand
arguments:
- @fos_oauth_server.client_manager.default
tags:
- { name: console.command }

Run command below and do not distribute credentials! This will add a new record in client table in database.


Mac:oauth-server inanzzz$ app/console create:oauth:client --grant-type="client_credentials"
OAuth client has been created...
Public ID: 8_4mcij3t948isk880w0gskkksc88w0wo0wowogkgcowk4coocwk
Secret ID: 4gxi02iydlwkwokko4cs8w4skw8goks0s00cw00okosskcg8sg

Getting Access Token


Chrome Postman.


# Request
GET http://oauth-server.dev/app_dev.php/oauth/v2/token?client_id=8_4mcij3t948isk880w0gskkksc88w0wo0wowogkgcowk4coocwk&client_secret=4gxi02iydlwkwokko4cs8w4skw8goks0s00cw00okosskcg8sg&grant_type=client_credentials

# Response
HTTP/1.1 200 OK
{
"access_token": "ODk5ZTU2MWQ2ZDBiODYxZTU2NjJmMzYyNDk0NTc3YzMyY2Q0NzNmMTQ2NDAzYmFhZTAxNTNmZmEwYmIxNWRlZA",
"expires_in": 3600,
"token_type": "bearer",
"scope": null
}

Curl.


# Request
curl 'http://oauth-server.dev/app_dev.php/oauth/v2/token?client_id=8_4mcij3t948isk880w0gskkksc88w0wo0wowogkgcowk4coocwk&client_secret=4gxi02iydlwkwokko4cs8w4skw8goks0s00cw00okosskcg8sg&grant_type=client_credentials'

# Response
HTTP/1.1 200 OK
{
"access_token": "MjA5ZDQ0MTFhZGM5OTJiN2VmNGViNWNiMDFiZTM4YzMzMTcxMDU3NjI3M2Q0NjU2NWU1MGY0MDUxOTk5ZTQ3YQ",
"expires_in": 3600,
"token_type": "bearer",
"scope": null
}

Symfony.


public function indexAction()
{
$oauthHeaders = [
"client_id" => "8_4mcij3t948isk880w0gskkksc88w0wo0wowogkgcowk4coocwk",
"client_secret" => "4gxi02iydlwkwokko4cs8w4skw8goks0s00cw00okosskcg8sg",
"grant_type" => "client_credentials"
];

$endpoint = "http://oauth-server.dev/app_dev.php/oauth/v2/token";

$curl = curl_init($endpoint);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HEADER, 'Content-Type: application/x-www-form-urlencoded');
curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($oauthHeaders));
$response = curl_exec($curl);
curl_close($curl);

return new Response($response);
}

# Response
HTTP/1.1 200 OK
{
"access_token": "NmRmNjBjMTQyNzdiOGI5ZDIwZmQyN2IwZjcxMDUxMDRiZjBhMDUyMmMxNmYzMGYxYTNhOTIwNjhlZDVmM2M5Mg",
"expires_in": 3600,
"token_type": "bearer",
"scope": null
}

Verify if Access Token works


Curl.


# Request
curl -H "Authorization: Bearer NTU2ODcyYmIzNDM5N2M2YWRiYzAyNTk5NDI0OGI0ZjM2ZGFmZTgyYjZiZmJhOGEwN2RjNWEzMzIxNzQyNzQ3ZA" http://oauth-server.dev/app_dev.php/1/server/team

# Response
HTTP/1.1 200 OK
GET your team from Server

# Request
curl -v -X POST -H 'Authorization: Bearer NTU2ODcyYmIzNDM5N2M2YWRiYzAyNTk5NDI0OGI0ZjM2ZGFmZTgyYjZiZmJhOGEwN2RjNWEzMzIxNzQyNzQ3ZA' 'Content-Type:application/json' -d '{"hello":"world"}' 'http://oauth-server.dev/app_dev.php/1/server/team'

# Response
HTTP/1.1 200 OK
POST your team to Server

Symfony.


public function indexAction()
{
$oauthHeaders = [
'Authorization: Bearer NTU2ODcyYmIzNDM5N2M2YWRiYzAyNTk5NDI0OGI0ZjM2ZGFmZTgyYjZiZmJhOGEwN2RjNWEzMzIxNzQyNzQ3ZA'
];

$endpoint = "http://oauth-server.dev/app_dev.php/1/server/team";

$curl = curl_init($endpoint);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $oauthHeaders);
$response = curl_exec($curl);
curl_close($curl);

return new Response($response);
}

# Response
HTTP/1.1 200 OK
GET your team from Server

# Request
public function indexAction(Request $request)
{
$oauthHeaders = [
'Authorization: Bearer NTU2ODcyYmIzNDM5N2M2YWRiYzAyNTk5NDI0OGI0ZjM2ZGFmZTgyYjZiZmJhOGEwN2RjNWEzMzIxNzQyNzQ3ZA'
];

$endpoint = "http://oauth-server.dev/app_dev.php/1/server/team";

$curl = curl_init($endpoint);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $oauthHeaders);
curl_setopt($curl, CURLOPT_POSTFIELDS, $request->getContent());
$response = curl_exec($curl);
curl_close($curl);

return new Response($response);
}

# Response
HTTP/1.1 200 OK
POST your team to Server

Request with expired token.


# Request
curl -v -H "Authorization: Bearer ZDc3ZDgyYzRmYjQ2MzdiMDJkYjc3N2UzYTMyNDg1NjkyYzFiNjUyMGQ2NjYwMTNlMjEwYzZlMzBmODZjN2EwYQ" http://oauth-server.dev/app_dev.php/1/server/team

# Response
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to oauth-server.local (127.0.0.1) port 80 (#0)
> GET /app_dev.php/1/server/team HTTP/1.1
> User-Agent: curl/7.37.1
> Host: oauth-server.local
> Accept: */*
> Authorization: Bearer ZDc3ZDgyYzRmYjQ2MzdiMDJkYjc3N2UzYTMyNDg1NjkyYzFiNjUyMGQ2NjYwMTNlMjEwYzZlMzBmODZjN2EwYQ
>
< HTTP/1.1 401 Unauthorized
< Date: Tue, 29 Dec 2015 14:52:17 GMT
* Server Apache/2.4.10 (Unix) PHP/5.6.9 is not blacklisted
< Server: Apache/2.4.10 (Unix) PHP/5.6.9
< Vary: Authorization
< X-Powered-By: PHP/5.6.9
< WWW-Authenticate: Bearer realm="Service", error="invalid_grant", error_description="The access token provided has expired."
< Cache-Control: no-store, private
< Pragma: no-cache
< X-Debug-Token: 532cf9
< X-Debug-Token-Link: /app_dev.php/_profiler/532cf9
< Content-Length: 86
< Content-Type: application/json
<
* Connection #0 to host oauth-server.local left intact
{"error":"invalid_grant","error_description":"The access token provided has expired."}

References


For more information, you can check links below.