Symfony a RESTFul app: Security ( Securing the token path )

In the previous post we set up a basic symfony project aiming to develop a RESTFul solution, we also talk about the security and how to implement an OAuth2 service, but we also saw a security flaw in one of the most important end points, the /oauth/v2/token one which will deliver the access token to the user.

As always all the code on this series can be found on this github repository under the symfony2_restful_example branch.

Rest Authentication:

As we discuss earlier, the main concern is that the token end point suppose to receive the username and the password of the user/customer in plain text, also the end point is not protected with any authentication mechanism. In my personal opinion this is not a good idea, so what do we did to fix this security issue. The main idea goes with this points:

  • Implement a new authentication mechanism that receive the username and the hashed password of the user/customer as header parameters.
  • Secure the token path with the previous authentication mechanism.

Lo lets start.

Implementing a new Authentication Mechanism:

The symfony documentation explain to us how to implement an authentication mechanism using as example the WSSE specification, we will use the same tutorial for our classes so i will leave to you the documentation page and will show the final classes. Notice that i change the name of the classes from Wsse to Rest and make some tweaks like removing properties and parameters where are not needed.

AppBundle/Security/Authentication/Token/RestUserToken.php

<?php

namespace AppBundle\Security\Authentication\Token;

use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;

class RestUserToken extends AbstractToken
{
    private $password;

    public function __construct($password)
    {
        $this->password = $password;
        parent::__construct();
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getCredentials()
    {
        return '';
    }
}

AppBundle/Security/Authentication/Provider/RestProvider.php

<?php

namespace AppBundle\Security\Authentication\Provider;

use AppBundle\Security\Authentication\Token\RestUserToken;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class RestProvider implements AuthenticationProviderInterface
{
    private $userProvider;

    public function __construct(UserProviderInterface $userProvider)
    {
        $this->userProvider = $userProvider;
    }

    public function authenticate(TokenInterface $token)
    {
        $user = $this->userProvider->loadUserByUsername($token->getUsername());

        if ($user && $user->getPassword() === $token->getPassword()) {
            $authenticatedToken = new RestUserToken($user->getRoles());
            $authenticatedToken->setUser($user);

            return $authenticatedToken;
        }

        throw new AuthenticationException('The REST authentication failed.');
    }

    public function supports(TokenInterface $token)
    {
        return $token instanceof RestUserToken;
    }
}

AppBundle/Security/Firewall/RestListener.php

<?php

namespace AppBundle\Security\Firewall;

use AppBundle\Security\Authentication\Token\RestUserToken;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;

class RestListener implements ListenerInterface
{
    protected $tokenStorage;
    protected $authenticationManager;

    public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager)
    {
        $this->tokenStorage = $tokenStorage;
        $this->authenticationManager = $authenticationManager;
    }

    public function handle(GetResponseEvent $event)
    {
        $request = $event->getRequest();
        $username = $request->headers->get('username');
        $password = $request->headers->get('password');

        if (!$username && !$password) {
            return;
        }

        $token = new RestUserToken($password);
        $token->setUser($username);

        try {
            $authToken = $this->authenticationManager->authenticate($token);
            $this->tokenStorage->setToken($authToken);

            return;
        } catch (AuthenticationException $failed) {
            $token = $this->tokenStorage->getToken();
            if ($token instanceof RestUserToken) {
                $this->tokenStorage->setToken(null);
            }
            return;
        }
    }
}

AppBundle/DependencyInjection/Security/Factory/RestFactory.php

<?php

namespace AppBundle\DependencyInjection\Security\Factory;

use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;

class RestFactory implements SecurityFactoryInterface
{
    public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
    {
        $providerId = 'security.authentication.provider.rest.' . $id;
        $container
            ->setDefinition($providerId, new DefinitionDecorator('rest.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider));

        $listenerId = 'security.authentication.listener.rest.' . $id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('rest.security.authentication.listener'));

        return array($providerId, $listenerId, $defaultEntryPoint);
    }

    public function getPosition()
    {
        return 'pre_auth';
    }

    public function getKey()
    {
        return 'rest';
    }

    public function addConfiguration(NodeDefinition $node)
    {
    }
}

Done, we have our classes in place, one thing to notice on the authentication method of the RestProvider class we validate that the password retrieved from the header is the same hashed password of the user that is trying to get his access token.

Lets configure the services on services.yml

rest.security.authentication.provider:
    class: AppBundle\Security\Authentication\Provider\RestProvider
    arguments: [""]
    public: false

rest.security.authentication.listener:
    class: AppBundle\Security\Firewall\RestListener
    arguments: ["@security.token_storage", "@security.authentication.manager"]
    public: false

Great all is in place just one extra configuration is required on the oauth_token firewall

oauth_token:
    pattern:      ^/oauth/v2/token
    rest:         true

Now we can try access get the access token as before and we will get an authentication error since we need to specify the username and hashed password on the header, example:

/oauth/v2/token?client_id=86_5sq6mu44srwo0kko8s4sckw40ksss8sw40wwgook40g0884ggw&client_secret=3er1dln3wa2o0oco0s44wkocwog0kg08sc80osck04wc4w44co&grant_type=password&redirect_uri=www.example.com&username=fabien&password=fabien

with header:

username: fabien
password: $2y$13$560vs8wzb2g40wg4cggs4uw66bA6G4VD4VW3KZnNmPNmGAe.JNRa2

And voila we got authenticated and receive the expected response

{
  access_token: "YzY2OWVlNDhjZDg0ZDhkMDEzNDg4ODhmOGU1MWM5YjZkYWNlYzEzODM5ZDc1N2JmZjJkZWUzNDZlMjExYmMwYQ"
  expires_in: 3600
  token_type: "bearer"
  scope: null
  refresh_token: "NTljMzhhMjRmOWY2NmFkNjAxMjM4YjlmMjdiOTIyNjQ1ODczZjI0NjZlZGVmOWZjOWI5NmQ0ZTQ4MmE4NDJkZA"
}

Conclusion:

Now we have a more secure RESTFul app that will allow us to build SPA and mobile Apps and expose our business rules so we can grow in the modern information era.

This method maybe is not what you really need, and exactly that is the point; as all the posts in this series the main point is to gave you some directions on what a RESTFul app should look so you can decide on your own if agree with it and build it.

I really hope this series has been helpful to you. Be free to write some comments or share it if you like it. Thanks for you reading time.

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

11 Comments

  1. Thanks for the tutorial you providing… It really helped me to get FOSOAuthServerBundle working.

    The configuration you are providing for securing oauth_token seems to be incomplete. In fact, I am getting the following error **No authentication listener registered for firewall “oauth_token”** when trying to get an access token for the client.

    May be you should specify a way on how to login since the security is activated.

    One more thing, You said it is not good send the password in plain text in the request for getting an access token, but the password is still there!!! Is it supposed to be sent in the header only???

    One last thing, how to generate the hash for the password? A link on that would be great.

    Like

    Reply

    1. Hello, a pleasure to help.

      Regarding the listener, please review the configuration, the important value to notice is the type “rest: true” on firewall that refers to the:

      public function getKey() { return ‘rest’; }

      function of the RestFactory class.

      The steps to login are in fact explained after that condiguration, sending the request to the /oauth/v2/token endpoint with the new headers.

      About the plain password on request you should read the next post https://bitgandtter.wordpress.com/2015/09/30/symfony-a-restful-app-security-securing-the-token-path-fixed/

      About the last question, the password can be hash with any algorithm of your choose i advice to use bcrypt but that is really up to the business requirements.

      Cheers

      Like

      Reply

  2. Hi, just to tell you that you miss telling us to add the function build in the bundle class.
    I mean this one:

    public function build(ContainerBuilder $container)
    {
    parent::build($container);
    $extension = $container->getExtension(‘security’);
    $extension->addSecurityListenerFactory(new RestFactory());
    }

    Without it, Symfony displays this error: unrecognized option rest under security.firewalls.oauth_token

    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