Symfony2 : How to easily implement a REST API with oAuth2 (for normal guys)
composer.json
{
"hwi/oauth-bundle": "0.4.*@dev",
"guzzle/guzzle": "3.8.*@dev",
}
app/AppKernel.php
$bundles = array(
// ...
new HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
// ...
);
app/config/config.yml
hwi_oauth:
firewall_name: oauth2_secured_api
resource_owners:
test_connect:
type: oauth2
client_id: %oauth_client%
client_secret: %oauth_secret%
access_token_url: %website_back_base_url%/oauth/v2/token
authorization_url: %website_back_base_url%/oauth/v2/auth
infos_url: %website_back_base_url%/me
scope: "read"
user_response_class: HWI\Bundle\OAuthBundle\OAuth\Response\PathUserResponse
paths:
identifier: id
nickname: username
realname: username
app/config/routing.yml
hwi_oauth_redirect:
resource: "@HWIOAuthBundle/Resources/config/routing/redirect.xml"
prefix: /connect
hwi_oauth_login:
resource: "@HWIOAuthBundle/Resources/config/routing/login.xml"
prefix: /login
app/config/security.yml
Look at that ! the same context: test_connect
so the two firewalls can talk to each other !
security:
encoders:
Symfony\Component\Security\Core\User\User: plaintext
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
providers:
hwi:
id: hwi_oauth.user.provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
oauth2_secured_api:
anonymous: ~
context: test_connect
oauth:
resource_owners:
test_connect: "/login/test-connect"
login_path: /login
use_forward: false
failure_path: /login
oauth_user_provider:
service: hwi_oauth.user.provider
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/me, roles: ROLE_USER }
<?php
namespace test\ApiBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class ClientCreateCommand extends Command
{
protected function configure()
{
$this
->setName('vp:oauth-server:client-create')
->setDescription('Create 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)
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$clientManager = $this->getApplication()->getKernel()->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()));
}
}
As explained in the step 3 of the documentation, you'll have to create four entities : Client
, AccessToken
, RefreshToken
and AuthCode
.
Then to create a Client
, you might want a command line like that :
composer.json
{
"jms/serializer-bundle": "dev-master",
"friendsofsymfony/user-bundle": "2.0.*@dev",
"friendsofsymfony/rest-bundle": "1.4.*@dev",
"friendsofsymfony/oauth-server-bundle": "1.4.*@dev",
"nelmio/api-doc-bundle": "2.5.*@dev",
}
app/AppKernel.php
$bundles = array(
// ...
new JMS\SerializerBundle\JMSSerializerBundle(),
new FOS\UserBundle\FOSUserBundle(),
new FOS\RestBundle\FOSRestBundle(),
new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
new Nelmio\ApiDocBundle\NelmioApiDocBundle(),
// ...
);
app/config/config.yml
framework:
# ...
translator: { fallback: "%locale%" }
# ...
fos_user:
db_driver: orm
firewall_name: main
user_class: test\ApiBundle\Entity\User
fos_oauth_server:
db_driver: orm
client_class: test\ApiBundle\Entity\Client
access_token_class: test\ApiBundle\Entity\AccessToken
refresh_token_class: test\ApiBundle\Entity\RefreshToken
auth_code_class: test\ApiBundle\Entity\AuthCode
service:
options:
supported_scopes: read
nelmio_api_doc: ~
sensio_framework_extra:
view:
annotations: false
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener: true
view:
view_response_listener: 'force'
routing_loader:
default_format: json
access_denied_listener:
json: true
exception:
codes:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
messages:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': true
app/config/routing.yml
# FOSUserBundle
fos_user_security:
resource: "@FOSUserBundle/Resources/config/routing/security.xml"
fos_user_profile:
resource: "@FOSUserBundle/Resources/config/routing/profile.xml"
prefix: /profile
fos_user_register:
resource: "@FOSUserBundle/Resources/config/routing/registration.xml"
prefix: /register
fos_user_resetting:
resource: "@FOSUserBundle/Resources/config/routing/resetting.xml"
prefix: /resetting
fos_user_change_password:
resource: "@FOSUserBundle/Resources/config/routing/change_password.xml"
prefix: /profile
# FOSAuthServerBundle
fos_oauth_server_token:
resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"
fos_oauth_server_authorize:
resource: "@FOSOAuthServerBundle/Resources/config/routing/authorize.xml"
# testApiBundle
test_api_bundle:
type: rest
resource: "@testApiBundle/Resources/config/routing.yml"
prefix: /
app/config/security.yml
Please remember we've put a context
name at test_connect
, we'll use it soon !
security:
encoders:
vp\GlobalBundle\Entity\User:
algorithm: pbkdf2
hash_algorithm: sha512
encode_as_base64: true
iterations: 1000
role_hierarchy:
ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: ROLE_ADMIN
providers:
user_provider:
id: vp_global_user_provider
firewalls:
oauth_token:
pattern: ^/oauth/v2/token
security: false
oauth_authorize:
pattern: ^/oauth/v2/auth
form_login:
provider: user_provider
check_path: vp_global_login_check
login_path: vp_global_login
anonymous: true
context: test_connect
api:
pattern: ^/
fos_oauth: true
stateless: true
anonymous: true # Needed to allow access to oauth pages
access_control:
- { path: ^/oauth/v2/, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
We will imagine two Symfony projects :
As our users will try to connect to our front, we want a login process à la Facebook, which you will see, is the oAuth grant_type
authorization_code
process.
The front is an oauth_client
who try to connect to the back.
This oauth_client
is created with a command line on the back. You then retrieve an id
and a secret
.
Warning If you look into the database to get the id
, it's the concatenation of the oauth_client.id
and oauth_client.random_id
, separated with an underscore. Something looking like 1_kj2gjhlice8wkoxwggpok80hk0wcewkwfkk4c4wocawwgc0ko
.
You need to understand that there are different "ways" to "connect" with oAuth2 and retrieve an access_token
that you will use to hit your API. They are well explained in this Tankist blog post (read them all, they are just great).
Whatever the way you use to retrieve the access_token
, you want to get something like this :
{
access_token: "NGM3NDI2OGQ0MTRjMjhkYzY5ZGQ1YjViODhmYzNlZmRiNGI3YjIxN2IxZDcxY2ZjMDI3MmY3NjI2N2ZhODJjYQ"
expires_in: 3600
token_type: "bearer"
scope: null
refresh_token: "MjQyNTM0NjBiMmZlYjY3MGM2OGJmMDllZjE0ZjNhYTMxZmIyN2ZmMGRlOGJlOGUwYjRkZmJkMWU4NmY5NDVlYQ"
}
These ways are defined by a grant_type
that you set to an oauth_client
(multiple grant_type
is possible) (it might be specific to FOSOAuthServerBundle, but I presume you will not use something else) :
grant_type=authorization_code
The "usual" process you have with Facebook : login, authorize app, redirection. So the user want to connect to your front. Simplified, here is what's happening :
oauth_client
id
.oauth_client
, i.e. the front) to access the back.access_token
) that allow the front to request the back API.No example here, we will come back on that process later.
grant_type=password
You still want an access_token
but you get it in one request, by sending everything you have : oauth_client
id
and secret
, and user credentials.
your_back/oauth/v2/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=password&username=USERNAME&password=PASSWORD
The process is of course simpler, but your front is storing the oauth_client
secret
. It might be ok because our front is in PHP, but if it's one day in Javascript, it might not be good. Also the process is not as cool as the real "Facebook/Google/GitHub" one :)
grant_type=client_credentials
Simplest request, no user credential, you only send oauth_client
id
and secret
:
your_back/oauth/v2/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=client_credentials
This might be usefull when your back is requesting another of your API. User credential might not be needed.
grant_type=refresh_token
This one is to refresh your access_token
. As your token will expire in one hour, you can ask to refresh it :
your_back/oauth/v2/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
As you need to have the oauth_client
secret
, this is not usable between our front and back, where grant_type=authorization_code
will be used.
I found it was the clearest explanation of the authorization_code :
The authorization code is obtained by using an authorization server as an intermediary between the client and resource owner. Instead of requesting authorization directly from the resource owner, the client directs the resource owner to an authorization server, which in turn directs the resource owner back to the client with the authorization code.
Before directing the resource owner back to the client with the authorization code, the authorization server authenticates the resource owner and obtains authorization. Because the resource owner only authenticates with the authorization server, the resource owner's credentials are never shared with the client.
The authorization code provides a few important security benefits, such as the ability to authenticate the client, as well as the transmission of the access token directly to the client without passing it through the resource owner's user-agent and potentially exposing it to others, including the resource owner.
As William Durand was recently explaining in his SOS, he "didn't see any other interesting blog post about REST with Symfony recently unfortunately". After spending some long hours to implement an API strongly secured with oAuth, I thought it was time for me to purpose my simple explanation of how to do it.
You might have already seen some good explanation of how to easily create a REST API with Symfony2. There are famous really good bundles a.k.a. :