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.