Let’s say you built an API using Symfony and you need to access it from a mobile application using authenticated requests on behalf of your users.
Here’s how to make this work using Symfony 2.8 and Doctrine.
Install FOSOAuthServerBundle
We will use the FOSOAuthServerBundle to implement this feature. Install it using the following command:
composer require friendsofsymfony/oauth-server-bundle
Next, enable the bundle in the AppKernel.php
file:
public function registerBundles() { $bundles = array( // ... new FOS\OAuthServerBundle\FOSOAuthServerBundle(), ); }
Create OAuth model classes
To create the OAuth model classes just add the following files to your project. Here we already have FOSUserBundle installed and set up to use the ApiBundle\Entity\User
class.
src/ApiBundle/Entity/Client.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\Client as BaseClient; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class Client extends BaseClient { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; public function __construct() { parent::__construct(); // your own logic } }
src/ApiBundle/Entity/AccessToken.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class AccessToken extends BaseAccessToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
src/ApiBundle/Entity/RefreshToken.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class RefreshToken extends BaseRefreshToken { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
src/ApiBundle/Entity/AuthCode.php
<?php namespace ApiBundle\Entity; use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity */ class AuthCode extends BaseAuthCode { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToOne(targetEntity="Client") * @ORM\JoinColumn(nullable=false) */ protected $client; /** * @ORM\ManyToOne(targetEntity="User") */ protected $user; }
Configure FOSOAuthServerBundle
Import the routing configuration in your app/config/routing.yml
file:
fos_oauth_server_token: resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml" fos_oauth_server_authorize: resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"
Add FOSOAuthServerBundle settings in app/config/config.yml
:
fos_oauth_server: db_driver: orm # Drivers available: orm, mongodb, or propel client_class: ApiBundle\Entity\Client access_token_class: ApiBundle\Entity\AccessToken refresh_token_class: ApiBundle\Entity\RefreshToken auth_code_class: ApiBundle\Entity\AuthCode service: user_provider: fos_user.user_provider.username
Back to the models
Generate a migration and migrate the database:
php app/console doctrine:migrations:diff php app/console doctrine:migrations:migrate
…or, if you’re not using migrations, just update the database schema:
php app/console doctrine:schema:update --force
Configure your application’s security
Edit your app/config/security.yml
file to add FOSOAuthServerBundle specific configuration:
# ... firewalls: oauth_token: # Everyone can access the access token URL. pattern: ^/oauth/v2/token security: false api: pattern: ^/api fos_oauth: true stateless: true anonymous: true # can be omitted as its default value # ... access_control: - { path: ^/api, role: IS_AUTHENTICATED_FULLY }
Create a client
Before you can generate tokens, you need to create a Client
using the ClientManager
. For this, create a new Symfony command:
<?php namespace ApiBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class OAuthAddClientCommand extends ContainerAwareCommand { protected function configure() { $this ->setName('oauth:add-client') ->setDescription("Ads a new client for OAuth") ; } protected function execute(InputInterface $input, OutputInterface $output) { $redirectUri = $this->getContainer()->getParameter('router.request_context.scheme') . "://" . $this->getContainer()->getParameter('router.request_context.host'); $clientManager = $this->getContainer()->get('fos_oauth_server.client_manager.default'); $client = $clientManager->createClient(); $client->setRedirectUris(array($redirectUri)); $client->setAllowedGrantTypes(array('refresh_token', 'password')); $clientManager->updateClient($client); } }
Now run the above command to generate your first OAuth client:
php app/console oauth:add-client
This client will be able to generate tokens and refresh tokens using the user’s username and password. You can find it’s data in the database client
table. The token
endpoint is at /oauth/v2/token
by default.
Document using NelmioApiDocBundle
If you use the NelmioApiDocBundle to document your API, you can add these OAuth methods too. Create a new YAML file in src/ApiBundle/Resources/apidoc/oauth.yml
:
grant_type_password: requirements: [] views: [] filters: [] parameters: grant_type: dataType: string required: true name: grant_type description: Grant Type (password) readonly: false client_id: dataType: string required: true name: client_id description: Client Id readonly: false client_secret: dataType: string required: true name: client_secret description: client Secret readonly: false username: dataType: string required: true name: username description: Username readonly: false password: dataType: string required: true name: password description: Password readonly: false input: null output: null link: null description: "Get OAuth token for user using username and password" section: "OAuth" documentation: null resource: null method: "POST" host: "" uri: "/oauth/v2/token" response: token: dataType: string required: true description: OAuth token readonly: true route: path: /oauth/v2/token defaults: _controller: FOS\UserBundle\Controller\SecurityController::checkAction requirements: [] options: compiler_class: Symfony\Component\Routing\RouteCompiler host: '' schemes: [] methods: [ 'POST' ] condition: '' https: false authentication: false authenticationRoles: [] cache: null deprecated: false statusCodes: [] resourceDescription: null responseMap: [] parsedResponseMap: [] tags: [] grant_type_refresh_token: requirements: [] views: [] filters: [] parameters: grant_type: dataType: string required: true name: grant_type description: Grant Type (refresh_token) readonly: false client_id: dataType: string required: true name: client_id description: Client Id readonly: false client_secret: dataType: string required: true name: client_secret description: client Secret readonly: false refresh_token: dataType: string required: true name: refresh_token description: Refresh token readonly: false input: null output: null link: null description: "Get new OAuth token using refresh token" section: "OAuth" documentation: null resource: null method: "POST" host: "" uri: "/oauth/v2/token" response: token: dataType: string required: true description: OAuth token readonly: true route: path: /oauth/v2/token defaults: _controller: FOS\UserBundle\Controller\SecurityController::checkAction requirements: [] options: compiler_class: Symfony\Component\Routing\RouteCompiler host: '' schemes: [] methods: [ 'POST' ] condition: '' https: false authentication: false authenticationRoles: [] cache: null deprecated: false statusCodes: [] resourceDescription: null responseMap: [] parsedResponseMap: [] tags: []
Add a new NelmioApiYmlProvider.php
file in src/ApiBundle/Service
folder:
<?php namespace ApiBundle\Service; use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Extractor\AnnotationsProviderInterface; use Symfony\Component\Finder\Finder; use Symfony\Component\Routing\Route; use Symfony\Component\Yaml\Yaml; /** * Generate annotations for vendor routes to be displayed in Nelmio ApiDoc. */ class NelmioApiYmlProvider implements AnnotationsProviderInterface { private $vendorFolder; public function __construct($vendorFolder) { $this->vendorFolder = $vendorFolder; } /** * {@inheritdoc} */ public function getAnnotations() { $annotations = []; $configDirectories = array($this->vendorFolder); $finder = new Finder(); $finder->files()->in($configDirectories); if (count($finder) == 0) { return $annotations; } foreach ($finder as $file_) { $data = Yaml::parse(file_get_contents($file_)); $vendors = array_keys($data); foreach ($vendors as $vendor) { $apiDoc = new ApiDoc($data[$vendor]); $route = new Route( $data[$vendor]['route']['path'], $data[$vendor]['route']['defaults'], $data[$vendor]['route']['requirements'], $data[$vendor]['route']['options'], $data[$vendor]['route']['host'], $data[$vendor]['route']['schemes'], $data[$vendor]['route']['methods'], $data[$vendor]['route']['condition'] ); $apiDoc->setRoute($route); $apiDoc->setResponse($data[$vendor]['response']); $annotations[] = $apiDoc; } } return $annotations; } }
Add a new service in src/ApiBundle/Resources/config/services.yml
file:
services: nelmio_api_doc.yml_provider.api_yml_provider: class: ApiBundle\Service\NelmioApiYmlProvider arguments: folder: %kernel.root_dir%/../src/ApiBundle/Resources/apidoc tags: - { name: nelmio_api_doc.extractor.annotations_provider }
You’ll find now two /oauth/v2/token
methods with different parameters listed in the api/doc
section of your project.
That’s all! You can now use the generated client to authenticate your users in your mobile app using OAuth.
How to use the FOSOAuthServerBundle
First you will need to get an access token by making a POST request to the /oauth/v2/token
endpoint with the following parameters:
grant_type=password client_id=[client's id from the database followed by '_' then the corresponding random id] client_secret=[client's secret] username=[user's username] password=[users's password]
You should get back something like this:
{ "access_token": "ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw", "expires_in": 3600, "token_type": "bearer", "scope": null, "refresh_token": "MDQ3MGIwZTk5MDkwOGM5NjhkMzk5NTUyZDJjZmYwM2YzZWViZDFhZjk0NTIyZmNjNzkyMDM0YjM4ODQ2N2VhNg" }
Use the access token for authenticated requests by placing it in the request header:
Authorization: Bearer ZDgxZDlkOWI2N2IyZWU2ZjlhY2VlNWQxNzM0ZDhlOWY2ZTIwOTBkNGUzZDUyOGYxOTg1ZTRjZGExOTY2YjNmNw
When the access token expires, you can get a new one using the refresh_token
grant type at the same /oauth/v2/token
endpoint:
grant_type=refresh_token client_id=[client's id from the database followed by '_' then the corresponding random id] client_secret=[client's secret] refresh_token=[refresh token received earlier]
The response should be similar to:
{ "access_token": "MjE1NjRjNDc0ZmU4NmU3NjgzOTIyZDZlNDBiMTg5OGNhMTc0MjM5OWU3MjAxN2ZjNzAwOTk4NGQxMjE5ODVhZA", "expires_in": 3600, "token_type": "bearer", "scope": null, "refresh_token": "YzM2ZWNiMGQ5MDBmOGExNjhmNDI1YjExZTkyN2U0Mzk5ZmM4NzcwNDdhNjAzZDliMjY3YzE0ZTg5NDFlZjg3MQ" }
2 Comments
Comments are closed.
only works with symfony version 2.8 ?
I use Symfony 2.8 because that’s the current LTS version, but I guess it will work with none or minor changes on Symfony 3 too.