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 clientFirst, 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.
Thanks for the article! I look forward to continuing ...
ReplyDeleteI would like to learn more (see more code) of the FOS User + REST API + Auth.
This tutorial has been fantastic. It's definitely help me wade into the OAuth waters.
ReplyDeleteI'm stuck in one place and it's not really covered in any detail here.
Right now my login page (oauth/v2/auth_login) works but it's not passing a client_id to my auth form (oauth/v2/auth), causing errors. This seems logical to me, as the logic in the AuthorizeController.php checks for a client_id and throws an exception. If I attack the auth form using the Authorization Code Grant Flow method, the results are as expected (not that I fully understand them yet). So, as of right now I'm stuck. My gut tells me that there's something supposed to happen post-login that gathers the needed parameters (e.g. client_id) and hands me off to the auth form without error.
Please advise.
Thank you.
-andrew
Did you get anywhere with the missing client id? I have the same thing.
DeleteDid you setup your referrer redirect? Make sure "use_referrer" is set for your form_login. See http://symfony.com/doc/current/cookbook/security/form_login.html for more information.
Deletehad the same problem, turned out that the if statement check if instanceof Client, and it was using the wrong client namespace, Acme\OAuthServerBundle\Document\Client but once I matched my own Client entity namespace it worked ok ;)
DeleteThanks Burak, Thanks for the tutorial
ReplyDeleteI am stuck with one problem, Any help will be appreciated.
Right now oauth/v2/auth takes me to the /login and once i put the password it takes me to authorize.html.php
in chrome though if i do not check the check-box element it shows me a message "please check the box to proceed" whereas in IE it doesnt. If i check on the allow access, I am getting the following error.
It seems to be trying to loading Symfony\Component\Form/Resources/config/validation.xml instead of the
src/Acme/OAuthServerBundle/Resources/config/validation.xml which i have created according to your example.
I am using symfony 2.0.13.
Warning: DOMDocument::schemaValidate(): Invalid Schema in D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Validator\Mapping\Loader\XmlFileLoader.php
at ErrorHandler ->handle ('2', 'DOMDocument::schemaValidate(): Invalid Schema', 'D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Validator\Mapping\Loader\XmlFileLoader.php', '182', array('file' => 'D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Form/Resources/config/validation.xml', 'dom' => object(DOMDocument)))
3. at DOMDocument ->schemaValidate ('D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Validator\Mapping\Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd')
in D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Validator\Mapping\Loader\XmlFileLoader.php at line 182
4. at XmlFileLoader ->parseFile ('D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Form/Resources/config/validation.xml')
in D:\Apache2.2\htdocs\milestone_1\vendor\symfony\src\Symfony\Component\Validator\Mapping\Loader\XmlFileLoader.php at line 32
Thank you very much for this tutorial!! It's very interesting!
ReplyDeleteBut I'm blocked in a problems... any help will be very nice, I've been two days looking for more info... but I can't found it.
I receive the next error:
Fatal error: Declaration of RedRudeBoy\OAuthServerBundle\Form\Type\AuthorizeFormType::buildForm() must be compatible with that of Symfony\Component\Form\FormTypeInterface::buildForm() in ...src/RedRudeBoy/OAuthServerBundle/Form/Type/AuthorizeFormType.php on line 26
Thanks!!
Arnau-Lenin: In AuthorizeFormType.php you need to change buildForm to accept FormBuilderInterface rather than FormBuilder. Also update your 'use' statement. This will get you going.
DeleteThis comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThank you very much for the article. Following your article, I have nearly managed to set up the OAuth login. I do have one problem, though.
ReplyDeleteWhen logging in through the form, I am sent to / and I get the following error:
"No route found for "GET /""
Do you have any idea why this might happen?
Simon, I am not really sure what is going on but if I had to take a guess:
Delete* Make sure the form action is correctly defined.
* Read this section carefully:
"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."
So the browser has to pass the referrer to Symfony2 otherwise the redirect will not work. This tutorial is for demonstration purposes only so you need to address this behavior/implementation in a real application.
Hi SimonBS,
Deletemaybe you have to enable the user_referer, so that after login you get redirected to the page that redirected you to login form:
http://symfony.com/doc/current/cookbook/security/form_login.html#using-the-referring-url
Hi Burak,
ReplyDeletejust a point about the Resource Owner Password Credentials Grant flow.
I'm currently using Symfony 2.0.x, so I use the 1.1.3 branch of FOSOauthServerBundle (as it is recommended in the main page).
In order to use that flow, I had to create a new User Provider (the official Symfony2 docs already tell how to do this) and inject it to the OAuthStorage (vendor/bundles/FOS/OAuthServerBundle/Storage/OAuthStorage.php) class.
The file that needs to be modified to inject the new service is:
- vendor/bundles/FOS/OAuthServerBundle/Resources/config/oauth.xml
The change needed here is to change the line that says:
null
(This line is the injection of the user provider that OAuthStorage uses to check for a specific user credenditals as it is called from the OAuth2 library (more precisely from vendor/oauth2-php/lib/OAuth2/OAuth2.php). If you try that flow without making this change, an null pointer (or whatever it is named in PHP) exception occurs)
I changed this line for the injection of my new created User Provider (identified by "entity_user_provider"):
The only thing I believe it is not correctly managed are the scopes, as I can't get to use them correctly using this flow.
Available scopes can be configured in app/config/config.yml:
fos_oauth_server:
db_driver: orm #in my case, I use Doctrine with mysql
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
service:
options:
supported_scopes: dummy_scope # now I don't remember how to define more than one scope...
# ...think this parameter accepts an array, it's a matter of syntax
Under the "options" tag, you can also define other OAuth2 server parameters (see OAuth2.php for more info), for example "auth_code_lifetime".
By the moment, I use a hard-coded scope for all the tokens granted within this flow, so I changed the file OAuthStorage.php to manually return a fixed scope (line 148).
I've been messing with OAuth2 for some (and more) hours, to figure out how to make the Resource Owner Password Credentials Grant flow work, so I hope this information can be useful to anyone that tries to use it.
Have a nice day.
hmpf... for some reason the copy&paste I made from oauth.xml file dosen't appear in the post :(
Deletethe change corresponds to changing the null injection in that file (that corresponds to a null User Provider) for an injection a a new User Provider (google it to know how DI works in Symfony).
Hi Toni, please encode your html entities before posting - try this service http://htmlentities.net/. Thank you for the explanation.
DeleteOk, thanks for the link!
DeleteSo in vendor/bundles/FOS/OAuthServerBundle/Resources/config/oauth.xml i changed this...
<argument>null</argument>
... for this:
<argument type="service" id="entity_user_provider"/>
Maybe you can update the post with this example, so that people can view the example immediately without having to go thorough all the people's comments?
DeleteFor those looking to use a custom user provider (I was using entity), use the console to find the proper service string. I spent far too much time trying to figure it out, so hopefully it saves others time!
Deletephp app/console container:debug --show-private
You'll get a lot of results so try searching for "security.user.provider"
Scope is the fourth argument of finishClientAuthorization(). So you can code in AuthorizeFormHandler::process() :
Deletereturn $this->oauth2->finishClientAuthorization(true, $user, $this->request, $this->request->get("scope"));
Thanks for documentation Burak. Such a great thing :)
ReplyDeleteI still have problems to configure it all. Could anyone take a look at this question I have posted at www.stackoverflow??
http://stackoverflow.com/questions/12572904/symfony2-security-strange-behavior-challenging-issue
Ok, I know what is going on here... but I don't know how to solve it. I would appreciate if anyone could take a look to the issue and to the next explanation.
DeleteIt seems like the method explained in this post creates for me sort of a "session" that results to be "parallel" to the real session I want it to create (which would be the good one).
So, the new login form creates its own session, but it does not create the "official" session of my web site. And that is not what I want. I want the oauth2 method to create one of those official sessions
Sort of difficult to explain and to understand :(
I hope someone can help me.
In other words, how can I do to make /auth_login form to create as well the main session of my site?
ReplyDeleteThanks for this great article. Post sample code on github in few days
ReplyDeleteThere are a few gotchas, but in general this is very good article.
ReplyDeleteThanks for sharing!
P.S The pattern for the oauth_authorize firewall has to end with a dolar sign, otherwise you will end up in a constant loop.
Current value: ^/oauth/v2/auth
Correct value: ^/oauth/v2/auth$
Why is the loginCheckAction() function empty?
ReplyDeleteI'm not sure this should ever get called as the Authentication listener should pickup the 'login check' and FOSUserBundle should handle it.
DeleteIf anyone is getting a "could not load type" it is because in services.xml the arguments for createNamed have switch places, check this discussion for details:
ReplyDeletehttps://github.com/kwattro/FOSOAuthServerBundle/commit/f437464d91c21bdfab785bb0782087e5f7755dd8
wasted a few hours and almost gave up with this.. now onto the next problem :)
Thanks a lot Roberto, your comment was very helpful :)
DeleteHi,
ReplyDeleteI have pretty much finish everything but when I try to test and I use this URL :
/oauth/v2/auth?client_id=3_1anv46pq68kk4c8kcc4kogwggo88co88g8s4woo440wsckc08o&redirect_uri=http%3A%2F%2Fwww.google.com&response_type=token
I get this error:
Client 3_1anv46pq68kk4c8kcc4kogwggo88co88g8s4woo440wsckc08o is not found.
Why isn't it finding it?
I am trying to search where the string is splitted and the DB request made, but I didn't found it yet.
I am using Doctrine ORM.
DeleteI have been searching for the past 2 days and I can't find where it comes from. I don't understand why it can't find the information because it's stored in the DB
I've the same error.
DeleteThe problem is the query generated from doctrine:
SELECT t0.random_id AS random_id1, t0.redirect_uris AS redirect_uris2, t0.secret AS secret3, t0.allowed_grant_types AS allowed_grant_types4, t0.id AS id5, t0.name AS name6 FROM api_client t0 WHERE t0.id = '1' AND t0.random_id = 66oucd7jniscwgsgk4wco4ock4cc40o8cgg0g8oo8cg4ckkocw
66oucd7jniscwgsgk4wco4ock4cc40o8cgg0g8oo8cg4ckkocw is a string and must be "66oucd7jniscwgsgk4wco4ock4cc40o8cgg0g8oo8cg4ckkocw"
But now I don't know how to solve the problem.
Any idea?
I find the problem.....
DeleteIf you use orm and not mongodb, check in AuthorizeController
namespace ....Bundle\Controller;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\Request;
use FOS\OAuthServerBundle\Controller\AuthorizeController as BaseAuthorizeController;
use TicketCloud\ApiBundle\Form\Model\Authorize;
//use TicketCloud\ApiBundle\Document\Client; <----- error
use TicketCloud\ApiBundle\Entity\Client; <----- correct
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Thank you for the help.
DeleteIt corrected my error, but I have another one right after :
" You have requested a non-existent service "mybundle_oauth_server.authorize.form". "
Would you happen to know where it comes from?
I think it's because I am using yml everywhere.
DeleteSo I decided to include the xml ressources to create the services :
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: services.yml }
- { resource: doctrine_extensions.yml }
- { resource: ../../src/MyBundle/OAuthServerBundle/Resources/config/services.xml } -> I create the services
Bug now I have an error like this : Fatal error: Declaration of MyBundle\OAuthServerBundle\Form\Type\AuthorizeFormType::buildForm() must be compatible with Symfony\Component\Form\FormTypeInterface::buildForm(Symfony\Component\Form\FormBuilderInterface $builder, array $options) in /home/myuser/mybundle/src/AppMyTaxi/OAuthServerBundle/Form/Type/AuthorizeFormType.php on line 27
Does anyone had this issue? It is the same when I convert the xml services in yml
Anyone have an idea?
Deleteuse Symfony\Component\Form\FormBuilderInterface;
Deleteuse Symfony\Component\Form\AbstractType;
class AuthorizeFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
/* some code ... */
}
/* more code ... */
}
Maybe this code will be helpful
Hi,
ReplyDeletewhen using the url below (of course with appropriate client id and host name), I'm redirected to the authorization page. If I there check the "Allow access" checkbox and submit, I get
"Catchable Fatal Error: Argument 1 passed to FOS\OAuthServerBundle\Model\Token::setUser() must implement interface Symfony\Component\Security\Core\User\UserInterface, string given"
http://acme.localhost/app_dev.php/oauth/v2/auth?client_id=4f8e5bb57f8b9a0816000000_1xwgejzp1e3o8sgosc884cgoko44wgg4gc0s84ckw0c0sk4c4s&response_type=code&redirect_uri=http%3A%2F%2Fwww.google.com
I expected to be redirected to the api login page instead. I have tried to follow up your article but I haven't found any differences between your example and my code.
I was experiencing this too. Some debugging in to vendor/friendsofsymfony/oauth-server-bundle/FOS/OAuthServerBundle/Storage/OAuthStorage.php::createAuthCode was show that the $data variable is being passed to AuthCode::setUser was the string 'anon', which hinted that maybe something expired. I relogged in via auth_login and tried again. I got a different error, but his one, at least, was solved. Onward!
DeleteHi,
ReplyDeleteI got access-token from the client side . Next How can I authenticate my client side with server credentials?
I am also very interested to know that.
DeleteIt shows different process to create an authentication, but how exactly to user it from client side?
I got a solution after a long wandering .
DeleteWe can call the secured urls by using access_token query string .
I just implemented like this http://localhost/symfonyApp/web/app_dev.php/api/users.json?access_token=MDczZjEwMDI2NzUyMzk1MmY0YTVhOGVhOGY4MThjNGNhMTE4NzExYzFhZmU0NzRkZDNiNzI1NTk1ZTRlZmI2YQ
I am using FOS Rest Bundle . This bundle Simply supporting the authentication process.
Thank you, but which authentification flow did you use?
DeleteImplicit Grant Flow ?
Hi,
DeleteI am using normal Authorization Code Grant Flow . Not Implicit grant flow.
1. Authenticating the user
2. oAuth server return an oauthCode to the call back url
3.The I request the access token to the server
4.After I getting the access_toke send a request for the resource (ie . userdetails)
Thank you, I am still having some difficulties to make it work (see the previous thread) so I can't try it now.
DeleteHi,
ReplyDeletein symfony 2.2 in the service acme_oauth_server.authorize.for must be reversed:
from
...argument type="service" id="acme_oauth_server.authorize.form_type"
...argument>acme_oauth_server_authacme_oauth_server_auth
acme_oauth_server_auth
Hi Luigi. I think i have a problem with this service... can you explain better this issue pls?
DeleteI am also very interested.
DeleteThis comment has been removed by the author.
Deletethe service acme_oauth_server.authorize.form must be:
Delete|service id="acme_oauth_server.authorize.form" factory="" ... >
|argument> acme_oauth_server_auth /argument>
|argument type="service" id="acme_oauth_server.authorize.form_type" />
|/service>
Hi! Excellent tutorial!! There is any place where i can download the source code? Thanks!!!!
ReplyDeletei am using the Authorization Code Grant Flow. Everything is ok, but i haven't a new record on my MongoDB oauthAuthCode collection. did someone has that problem?
ReplyDeleteHi,
ReplyDeleteDo you have the sample source for this on a github?
I've build an example with the Symfony 2.2 SE. https://github.com/mikepage/Symfony2-RESTAPI
DeleteI am looking forward to check this out.
DeleteThank you very much !
This is the type of example what im looking for!!!.
Delete+1
$ php app/console doctrine:generate:entities MPRestApiBundle
ReplyDelete$ php app/console doctrine:schema:update --force
Got an error
Fatal error: Declaration of MP\RestApiBundle\Entity\AccessToken::setUser() must be compatible with that of FOS\OAuthServerBundle\Model\TokenInterface::setUser()
Be sure to clean the development cache, I've seen this one before. I will add an SQL dump
DeleteSQL is added
DeleteAm having the same issue ! is there any solution ?
DeleteLook at the setUser() function in AccessToken, it will be looking for a \Acme\OAuthServerBundle\Entity\User argument, you need to change this to UserInterface, so it looks like : public function setUser(UserInterface $user = null)
DeleteHello Robert,
ReplyDeleteDid you get any solution for this problem ?
Maybe app/console cache:clear, this example is clean and simple, it should work, as it does for me
ReplyDeleteI've tried that but nothing happend.
DeleteN.B : in the AccessToken::setUser():
public function setUser(UserInterface $user)
{
$this->user = $user;
}
Am using my own user Class that extends the FOSUserBundle class. and of course it implements the UserInterface => i think it is the same thing.
I've tried that but nothing happend.
ReplyDeleteN.B : in the AccessToken::setUser():
public function setUser(UserInterface $user)
{
$this->user = $user;
}
Am using my own user Class that extends the FOSUserBundle class. and of course it implements the UserInterface => i think it is the same thing.
I have the same issue. Anyone ?
DeleteThis comment has been removed by the author.
ReplyDeleteIf you are using symfony 2.1 in AuthorizeFormType you should add
ReplyDeletepublic function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\OAuthServerBundle\Form\Model\Authorize',
));
}
https://github.com/symfony/symfony/blob/master/UPGRADE-2.1.md
Great Article! It helped me alot.
ReplyDeleteBUT I keep getting message "The client credentials are invalid" when I want to request access token via oauth/v2/token.
I have setup basic http authentication.
Client creation works and authentication works
Allow deny form seems to works. I do not get any errors and I get redirected on http://google.com but I am not redirected on http://google.com?code=........
My problem is that I can not request access token successfully. I am using Symfony 2.3.3.
Any help?
Thanks,
Caslav Sabani
I am having an issue such that when I access:
ReplyDeletehttp://acme.localhost/oauth/v2/token?client_id=3_5ph2tuyxay048ksksgko4ocs4ss8kssws8k8osgk8o08go0goc&client_secret=28ckpamz5g2s880oggs880cwg0s0css0gsg0o8w0o0ok8skk8k&grant_type=authorization_code&redirect_uri=http%3A%2F%2Fwww.google.com&code=NGEyOWJmZGJjMTdiMmIwN2VmMjNhMTI2ODJjZWU3N2YxYzI1MDRjMDY1NTAzOGRmYTNlZTliMGVmNTZiODI2MQ
it doesn't redirect me to the login page, instead it goes straight asking me permission to grant to the client id. Why is this?
Hello. I've the same issue. The problem is in your oauth_authorize firewall configuration. There is no redirect cuz anonymous is fully allowed. A redirect to the login page will only occur if you've no permissions on the requested URL. So add to following permission.
Delete- { path: ^/oauth/v2/auth, role: IS_AUTHENTICATED_FULLY }
This will work for me.
By the way, great tutorial.
Cheers.
this made my morning! thanks!
DeleteHow to request access token with refresh token?
ReplyDeleteThis comment has been removed by the author.
ReplyDeletei am getting Could not load type "acme_oauth_server_auth" error , here is the question http://stackoverflow.com/questions/19616387/could-not-load-type-in-symfony
ReplyDeleteSame for me !
DeleteHi, Thanks for this awesome post. I think I will user your post to implement restful apis. I just have question. You have firewall for /api/* endpoint. Is it possible to have both OAuth base authentication (stateless) and form login based authentication in same firewall. I have been using the same apis for my backbone based application by using cookie for authentication. If so, what would be the kind of securtiy.yml configuration ? Thanks.
ReplyDeleteError still continue with FOSOauthServerBundle 1.4.0 and FOSUserBundle 2.0
ReplyDelete{"error":"invalid_client","error_description":"The client credentials are invalid"}
==> https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/issues/199
Someone knows ?
A couple of years later I must tell you that made a typo in the title... "Syfmony"
ReplyDeletehah! thank you.
DeleteFOSOAuthServerBundle authentication working, but access_token is “rejected” online
ReplyDeleteCan anyone help us out?
http://stackoverflow.com/questions/21427335/fosoauthserverbundle-symfony2-authentication-working-but-access-token-is-rejec
Hi,
ReplyDeleteWe are facing issue for Could not load type "acme_myoauth_server_auth", Please help us to resolve this issue.
Have you found a solution to this?
DeleteIn case you haven't I have the solution.
DeleteHi,
ReplyDeleteWe are facing issue for Could not load type "acme_myoauth_server_auth", Please help us to resolve this issue.
This comment has been removed by the author.
DeleteThis comment has been removed by the author.
ReplyDelete