Symfony a RESTFul app: Security ( FOSOAuthServerBundle )

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:

  1. Motivation
  2. REST Levels 0, 1, 2 ( FOSRestBundle, JMSSerializerBundle )
  3. REST Levels 0, 1, 2 ( FOSUserBundle )
  4. REST Levels 0, 1, 2 ( NelmioApiDocBundle )
  5. REST Levels 3 ( BazingaHateoasBundle )
  6. Security ( FOSOAuthServerBundle )
  7. Security ( Securing the token path )
  8. Security ( Securing the token path – FIXED )
Advertisements

29 Comments

  1. 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.

    Like

    Reply

    1. 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

      Like

      Reply

  2. 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 ?

    Like

    Reply

    1. 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.

      Like

      Reply

      1. 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”}

        Like

      2. 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.

        Like

      3. 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

        Like

      4. Ok perfect it works !
        I thought it had to be my controller that changed the header.

        Thank you very much bitgandtter

        Like

  3. 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.

    Like

    Reply

    1. 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

      Like

      Reply

      1. 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?

        Like

      2. 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

        Like

      3. 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!

        Like

      4. 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

        Like

  4. 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.

    Like

    Reply

    1. 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

      Like

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s