In the previous post we set up a basic symfony project aiming to develop a RESTFul solution.
Now we are on the last post of this series, “Security“. You may question:
Why security as the last post, should not be security the first thing we should ensure?
Sure, security can be, and from my point of view; the most controversial and complex topic on our business. There a lot of ways to enforce security on our apps, ones may say some of those solutions are not bullet proof, others may say some of them are too complicated for their business. The truth is that you should study a lot about this topic and decide by your self which is the best solution for your business at the right moment.
Because all of that is why i leave this post for the last of this series so you can decide to read it or not, or even if it should be part of this series or not; i hope what ever your call is, this post remains useful.
As always all the code on this series can be found on this github repository under the symfony2_restful_example branch.
Securing a RESTFul App:
In almost all the cases you will need to perform some kind of user authentication and authorization, unless your app is completely open to anonymous, but for the sake of this post i will assume that you will need it.
From the many security approaches out there i will talk about only two of them, both i have used it on the past.
API Key Authentication and WSSE:
Both approaches share the same concept where the final authentication is performed with the API Token. In the first case is much basic since the clients need to know the token from the start and that can be used from 3th sources to attack our system. The WSSE specification describe a more secure process where the user authenticate with their credentials and then the API Token is delivered to the clients for farther use.
Both on this section are well documented on the symfony official documentation, so i will not extend on they and how to implement it.
OAuth:
As wikipedia describes:
OAuth is an open standard for authorization. OAuth provides client applications a ‘secure delegated access’ to server resources on behalf of a resource owner.
The standard evolve from version 1.0 to version 2.0
OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications, desktop applications, mobile phones, and living room devices.
On this post we will talking about version 2.0 of OAuth since its more complete. This approach is used for authentication on almost all the social networks ans services that we use now a day (Google, Facebook, Twitter, LinkedIn, Github, etc)
FOSOAuthServerBundle:
Yes another bundle from FOS, this one will allow us to use OAuth2 on our projects. Sadly the documentation of this bundle is no so great as the others, and there are as always several ways to make it work; so i will focus on make it work for our little example, also i will try to explain the approach i choose to follow and the reasons why.
Lest start installing the bundle
composer require friendsofsymfony/oauth-server-bundle
And enable it on the AppKernel
new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
Great we have the bundle installed. To work we need to define four models that in fact are the main terms of the OAuth2 specification, Client, AccessToken, RefreshToken and AuthCode. To keep following our clean architecture lets put this models inside our Customer package, they should look like this:
Client.php
<?php namespace Customer; use FOS\OAuthServerBundle\Entity\Client as BaseClient; class Client extends BaseClient { protected $id; protected $name; public function setName($name) { $this->name = $name; } public function getName() { return $this->name; } }
I add to the client the property name so we can identify our client humanly, maybe on your business rules you can even find some other data like, the version of the client, the target platforms, etc.
AccessToken.php
<?php namespace Customer; use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken; class AccessToken extends BaseAccessToken { protected $id; protected $client; protected $customer; }
RefreshToken.php
<?php namespace Customer; use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken; class RefreshToken extends BaseRefreshToken { protected $id; protected $client; protected $customer; }
AuthCode.php
<?php namespace Customer; use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode; class AuthCode extends BaseAuthCode { protected $id; protected $client; protected $customer; }
As you can see all the classes extend from the models exported by the bundle.
But we need to integrate it with symfony and doctrine so lets create our entities and the mapping.
AppBundle/Entity/Client.php
<?php namespace AppBundle\Entity; use Customer\Client as BaseClient; class Client extends BaseClient { }
AppBundle/Entity/AccessToken.php
<?php namespace AppBundle\Entity; use Customer\AccessToken as BaseAccessToken; class AccessToken extends BaseAccessToken { }
AppBundle/Entity/RefreshToken.php
<?php namespace AppBundle\Entity; use Customer\RefreshToken as BaseRefreshToken; class RefreshToken extends BaseRefreshToken { }
AppBundle/Entity/AuthCode.php
<?php namespace AppBundle\Entity; use Customer\AuthCode as BaseAuthCode; class AuthCode extends BaseAuthCode { }
AppBundle/Resources/config/doctrine/Client.orm.yml
AppBundle\Entity\Client: type: entity table: client id: id: id: true type: integer generator: { strategy: AUTO } fields: name: type: string lenght: 255 lifecycleCallbacks: { }
AppBundle/Resources/config/doctrine/AccessToken.orm.yml
AppBundle\Entity\AccessToken: type: entity table: access_token id: id: id: true type: integer generator: { strategy: AUTO } manyToOne: user: targetEntity: Customer joinColumn: name: costumer_id referencedColumnName: id client: targetEntity: Client joinColumn: name: client_id referencedColumnName: id lifecycleCallbacks: { }
AppBundle/Resources/config/doctrine/RefreshToken.orm.yml
AppBundle\Entity\RefreshToken: type: entity table: refresh_token id: id: id: true type: integer generator: { strategy: AUTO } manyToOne: user: targetEntity: Customer joinColumn: name: costumer_id referencedColumnName: id client: targetEntity: Client joinColumn: name: client_id referencedColumnName: id lifecycleCallbacks: { }
AppBundle/Resources/config/doctrine/AuthCode.orm.yml
AppBundle\Entity\AuthCode: type: entity table: auth_code id: id: id: true type: integer generator: { strategy: AUTO } manyToOne: user: targetEntity: Customer joinColumn: name: costumer_id referencedColumnName: id client: targetEntity: Client joinColumn: name: client_id referencedColumnName: id lifecycleCallbacks: { }
Great we have our model set, lets finish the bundle configuration on config.yml.
fos_oauth_server: db_driver: orm client_class: AppBundle\Entity\Client access_token_class: AppBundle\Entity\AccessToken refresh_token_class: AppBundle\Entity\RefreshToken auth_code_class: AppBundle\Entity\AuthCode service: user_provider: fos_user.user_manager
Note that is important to add the service for the user provider since we are using FOSUserBundle, if you have any other user provider you need to put it here.
I know a lot of steps but we are almost done, just two more configurations before we can actually start coding. Lets configure our security file security.yml, that is the main reason we are going through this post.
# To get started with security, check out the documentation: # http://symfony.com/doc/current/book/security.html security: encoders: FOS\UserBundle\Model\UserInterface: bcrypt providers: fos_userbundle: id: fos_user.user_provider.username firewalls: # disables authentication for assets and the profiler, adapt it according to your needs dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false oauth_token: pattern: ^/oauth/v2/token security: false api_doc: pattern: ^/api/doc security: false api: pattern: ^/api fos_oauth: true stateless: true access_control: - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
Lets talk about this configuration line by line:
encoders: FOS\UserBundle\Model\UserInterface: bcrypt providers: fos_userbundle: id: fos_user.user_provider.username
We introduce the encoder and the user provider in the previous post about FOSUserBundle.
Firewalls, as we know; are the symfony way to secure routes by patterns so lets have a break down on the firewalls we have configured:
dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false
The development default firewall so we can access the development profiler page and the static files, no security enabled.
api_doc: pattern: ^/api/doc security: false api: pattern: ^/api fos_oauth: true stateless: true
And the firewalls from the previous posts to access the api end points and the api documentation, notice that i add a prefix “api”, to the path; trying to differentiate it from the oauth ones. Also notice that we set the authentication on the api route to be ensure with the FOSOAuthServerBundle.
oauth_token: pattern: ^/oauth/v2/token security: false
I leave to the final the oauth token end point, since this can be the controversial point. As we can see this end point do not have any security enabled, this is the default configuration provided by the bundle documentation. What this end point does is retrieve the data from the request and validate the client with the public id and the secret, as well the redirect uri, the grant type, the scope and lastly the user with the username and password.
This last part its what makes me doubt of the security since the username and the password are been sent as plain text on the request, and from my point of view this is not a good idea.
For the sake of the post lets leave this discussion for the final part of the post series, Secure the token path; and lets keep going with the configuration and coding.
access_control: - { path: ^/api, roles: [ IS_AUTHENTICATED_FULLY ] }
Finally in the access control section lets secure our api to be only accessed by fully authenticated users.
Great we have our security mechanism in place, lets test it. But first we need a client and our user authorize that client. The bundle comes with an implementation for that authorization part, that can be configured to be anything you need, its basically a page like the ones from facebook and google, where the user allow the application to gave authentication for it.
So, Why we don’t include it on our example?
Since this post series its about a RESTFul app, there is no view where the user see this allowing page, going even farther our internal API should not be allowed from our user, lets say we are not exporting our API to third party developers, so our app clients should be always allowed for our clients. For that matter i create a command that allow us to create new clients and authorized them for all our users, lets see it.
<?php namespace AppBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\HttpFoundation\Request; class CreateOAuthClientCommand extends ContainerAwareCommand { protected function configure() { $this ->setName('oauth:client:create') ->setDescription('Create OAuth Client') ->addArgument( 'name', InputArgument::REQUIRED, 'Client Name?' ) ->addArgument( 'redirectUri', InputArgument::REQUIRED, 'Redirect URI?' ) ->addArgument( 'grantType', InputArgument::REQUIRED, 'Grant Type?' ); } protected function execute(InputInterface $input, OutputInterface $output) { $container = $this->getContainer(); $oauthServer = $container->get('fos_oauth_server.server'); $name = $input->getArgument('name'); $redirectUri = $input->getArgument('redirectUri'); $grantType = $input->getArgument('grantType'); $clientManager = $container->get('fos_oauth_server.client_manager.default'); $client = $clientManager->createClient(); $client->setName($name); $client->setRedirectUris([$redirectUri]); $client->setAllowedGrantTypes([$grantType]); $clientManager->updateClient($client); $output->writeln(sprintf("<info>The client <comment>%s</comment> was created with <comment>%s</comment> as public id and <comment>%s</comment> as secret</info>", $client->getName(), $client->getPublicId(), $client->getSecret())); $customers = $container->get('doctrine')->getRepository('AppBundle:Customer')->findAll(); foreach ($customers as $customer) { $queryData = []; $queryData['client_id'] = $client->getPublicId(); $queryData['redirect_uri'] = $client->getRedirectUris()[0]; $queryData['response_type'] = 'code'; $authRequest = new Request($queryData); $oauthServer->finishClientAuthorization(true, $customer, $authRequest, $grantType); $output->writeln(sprintf("<info>Customer <comment>%s</comment> linked to client <comment>%s</comment></info>", $customer->getId(), $client->getName())); } } }
What do we did here, this command receive the client name, the redirect uri and the grant type, after create the client it retrieve all the users/customers and authorize the recently created app to all. After the execution of this command we should be able to request an access token for a user and then authenticate to query the API. Lest see it on an example:
app/console oauth:client:create client1 www.client1.com code
We create an application named client1, should see an output like this:
The client client1 was created with 88_5qhmvel4ozoko8ws0gc8404s40s40k040k04s8okoo44wcww40 as public id and 4431nwlagk8wsok00sogkkwscgk44k8g484ow04c8o4cwc000s as secret Customer 55f0369e0cdac linked to client client1
Now we can navigate to the token end point and request the access token
/oauth/v2/token?client_id=89_6488j8o9pickwwgwgos800gkws8o0wk8kwcs84skoo4c04gksw&client_secret=5vmzu0sro10kw8sosg4ccoco4ccs48wwog0kkcgwoksw800s0&grant_type=password&redirect_uri=www.client1.com&username=fabien&password=fabien
As you can see we need to include the username and password of the user in plain text. The response of this request should look like this:
{"access_token":"NDdhYTRjMmQ3MmNmZjZjOWFjYTA5NDQwZmZhNTgwMjczYzYyMDg5ODYzMWZmZjQ2MGUxMDJmNjNmNzI5Zjk4ZA","expires_in":3600,"token_type":"bearer","scope":null,"refresh_token":"YWIzMThhZDY1MGQzZGRlYjlhOGRiYTJjZTdmZjZmMWUyY2Y0YmUyZjRhMTU0YjE5N2VjYzcwZDQ4OTZlZDdlYg"}
Finally we can use the access token to query the api, lets try our old customers end point:
/api/customers
With the header:
Authorization: Bearer NDdhYTRjMmQ3MmNmZjZjOWFjYTA5NDQwZmZhNTgwMjczYzYyMDg5ODYzMWZmZjQ2MGUxMDJmNjNmNzI5Zjk4ZA
And we should see the expected response:
{ id: "55f0369e0cdac" username: "fabien" username_canonical: "fabien" email: "fabien@symfony.com" email_canonical: "fabien@symfony.com" enabled: true salt: "aru3jkvy1r4gcoock4gwocko0w8w4sk" password: "$2y$13$aru3jkvy1r4gcoock4gwoO2/lUF5PVlUHOqJIUCX9geX7jZxxjTfC" locked: false expired: false roles: [0] credentials_expired: false links: { self: { href: "/customers/55f0369e0cdac" } customers: { href: "/customers" } } }
Till now we have successfully configured an OAuth2 service, great.
Conclusion:
Security is really a hard topic to talk about, but i hope this post point you to some ideas on how to accomplish it. OAuth2 is a great protocol, i always think that if the big companies are using something, that something suppose to be good ;).
At the end i decide to add one more post to the series to talk about and of course show some code on what solution do i apply to solve the security flaw on the token path.
Series:
- Motivation
- REST Levels 0, 1, 2 ( FOSRestBundle, JMSSerializerBundle )
- REST Levels 0, 1, 2 ( FOSUserBundle )
- REST Levels 0, 1, 2 ( NelmioApiDocBundle )
- REST Levels 3 ( BazingaHateoasBundle )
- Security ( FOSOAuthServerBundle )
- Security ( Securing the token path )
- Security ( Securing the token path – FIXED )
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
[…] Security ( FOSOAuthServerBundle ) […]
LikeLike
Hello , great series , been following this and also the one made by tankist , but I fear OAuth may not be the way to go for my use case? Could you please help me with some feedback.
What I currently have is a frontend javascript app that’s tied into a REST backend made with symfony. So need a way to add security to the /api path , but somehow have a different kind of security (not OATH? but what??) for the frontend client
Can’t figure out around this … would I need a login system that would store the access key ? How would this work in practice .. I’m not looking into consuming the API with curl or httpie or postman or anything else for the time being, it’s main purpose is for the front end.
LikeLike
My advice is to use a simple variant of JSON Web Token (JWT), OAuth it self its a variant of this, but you can use a more simple strategy.
For securing the backend on SF you can implement a security mechanism like the one i introduce to verify user and password before allow request the access_token, in fact your authentication mechanism will provide an access_token that will allow the user to make request to the api secure section, this access_token does not need to be as complex as OAuth, just a simple unique token, that will be my advice. But you can even use the same authentication mechanism i introduce and keep sending the username and password in all requests, even if the password is hashed i will not recommend this approach.
On the frontend/js side you will require to store the access_token as a cookie or in local storage that most modern browsers support.
Hope this helps
LikeLike
Oh thank you. It really helps.
LikeLike
Hi, thank you for your post, really helpful !
I have done the configuration and success to get the token with grant type password. But I don’t see what to do after that ? I have the token, but how use it ?
LikeLike
Hi Abill, the token is then used as authentication mechanism on the paths under the firewall api, following the security configuration on the example. So if you try to access any path under that firewall you will need to use the header “Authorization: Bearer token” where token is the token received on the login. You can find an example of that at the end of the post trying to retrieve the customer.
LikeLike
Thank for your help 🙂
My issue is precisely that point : how to change Header and put ‘Authorization: bearer token’ in this ?
The url “/oauth/v2/token?client_id…&..” work good and give me token, but I have to do something more than just display it, but I don’t know what.
I checked all steps of tutorial except after “Finally we can use the access token to query the api”
When I try to display customers I have this message : {“error”:”access_denied”,”error_description”:”OAuth2 authentication required”}
LikeLike
Which client are you using Abill? changing headers on most clients its really easy. Please review the help of the client you are using
LikeLike
I use Postman (and chrome) for test oauth and api.
Is it symfony that change the header or the client with just the token received ?
I work on API for supply a mobile on iOS. I prepare the server side and agency work on app. So if I understand well, I need ‘password’ and ‘refresh_token’ for grant type.
LikeLike
On Postman search for the header section on the request and specify a new header with key “Authorization” and value “Bearer token” where token is the token received after the login
LikeLike
Ok perfect it works !
I thought it had to be my controller that changed the header.
Thank you very much bitgandtter
LikeLike
Hi! I’m trying to implement this but I want to use only the username and password to get the access token, not client_id and secret. Is it possible?
By the way, great post! very useful!
Thanks in advance.
LikeLike
You can maybe implement a work around that, but if you want only that simplicity maybe oauth is not the right fit for you. On that case i will just implement my own solution that generates and access_token based on username and password alone
LikeLike
I don’t want to make it simple. I just want to use the user credentials to get the API Client for that user, and the follow the old path. My idea would be:
Api Client (User credentials) —> Authorization server
Api Client Authorization server
Api Client <– (Access token + Refresh token) Authorization server
Am I missing something or doing something wrong?
LikeLike
Suppose that the client that is trying to authenticate has authorized several clients already, how will you know which of the clients is trying to perform the authentication. The client instance on oauth is the one that will let you know the grants for the user and can be also use to perform business requirements like api limit and so own. So basically you should not skip the client credentials
LikeLike
Right, got it. But then, OAuth is not used for mobile API clients? How does they authenticate? It suppose that my app will be used in a lot of mobiles, and each user will connect with his email and password… how can I make this work?
I feel like I’m missing something… I’ve read the RFC of OAuth yesterday!!! haha this is breaking my mind!
LikeLike
No problem i will try to explain. A client is not bound to an specific device (mobile or web browser). A client is an instance representative of an entity trying to authenticate a user on your platform. OAuth allows you to expose your API to thirty parties, each one of those will be a new client. Take for example Facebook API it use OAuth, when you create a new application to query theirs api that application is a client.
Now on you own platform, that is intended initially to be on house consuming, you will be just fine creating a first client, and use that client credentials on your mobile and browser application
LikeLike
Hmmm I see. So, by creating just one API client is gonna be enough to me, right?
LikeLike
yes it will be
LikeLike
Dude, you saved my life. Thank you very much.
LikeLike
Sure, no problem
LikeLike
Why you didn’t use FOSUserBundle to implement the authentication in Symfony? I read an article on Cloudways blog regarding authentication in symfony. They used FOSUserBundle with Auth0 to implement authentication. The process looked really easy and simple for me to follow.
LikeLike
Thats the beauty of software development we found new ways to do thing, ways that fit in a more proper way to our current solution. There are no black and whites we just need to find the one for us. Your advice its really good, if you were so gentleman to share the link with us we can all learn that way. Thanks
LikeLike
Hello, thanks for this it has saved me alot,
i however have one problem i cant get it to post the “grant_type” it always return invalid grant_type or missing parameters {“error”:”invalid_request”,”error_description”:”Invalid grant_type parameter or parameter missing”}
i dont know what am missing but i cant get pass that part
LikeLike
Listo amigo, tuve que hacer un cambio en la configuración,
quedando asi:
fos_oauth_server:
db_driver: orm
client_class: AppBundle\Entity\Client
access_token_class: AppBundle\Entity\AccessToken
refresh_token_class: AppBundle\Entity\RefreshToken
auth_code_class: AppBundle\Entity\AuthCode
service:
user_provider: fos_user.user_provider.username
LikeLike
Hola he seguido todo los pasos de tu tutorial, y a la hora de ejecutar el comando me da este error ..
[Symfony\Component\Debug\Exception\FatalThrowableError]
Type error: Argument 5 passed to FOS\OAuthServerBundle\Storage\OAuthStorage::__construct() must be an insta
nce of Symfony\Component\Security\Core\User\UserProviderInterface or null, instance of FOS\UserBundle\Doctr
ine\UserManager given, called in /var/www/html/proyectos/factura-digital-ec/var/cache/dev/appDevDebugProjec
tContainer.php on line 2066
gracias …
LikeLike