Symfony2 authentication provider: authenticate against webservice

The past few days I have really be struggeling with the Symfony2 security component. It is the most complex component of Symfony2 if you ask me! On the symfony.com website there is a pretty neat cookbook article about creating a custom authentication provider. Despite the fact that it covers the subject pretty well, it lacks support for form-based authentication use cases. In the current Symfony2 project I’m working on, we’re dealing with a web service that we need to authenticate against. So the cookbook article was nothing more then a good introduction unfortunately.

Using DaoAuthenticationProvider as example

Since we don’t want to reinvent the wheel, a good place to start is by investigating the providers that are in the Symfony2 core. The DaoAuthenticationProvider is a very good example, and used by the default form login. We are going to add a few pieces of code, so we can use the listener and configuration settings. The only thing we want to change are the authentication itself and the user provider. If you take a look at the link above, you will see the only thing we need to change is the checkAuthentication method. But, a few more steps are needed in order to make things function correctly. Let’s begin! :)

We also need a UserProvider!

First things first: we need a custom user provider. The task of the user provider is load the user from a source so the authentication process can continue. Because a user can already be registered at the webservice a traditional database user provider won’t work. We need to create a local record for every user that registers or logs in and doesn’t have an account. So basically the user provider is only responsible for loading and creating a user record. In this example I save the user immediately when there is no record; probably you want to do this after authenticating.

The code for the use provider looks like this:

<?php

namespace Acme\DemoBundle\Security\Core\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Acme\DemoBundle\Service\Service;
use Acme\DemoBundle\Entity\User;
use Doctrine\ORM\EntityManager;

class WebserviceUserProvider implements UserProviderInterface
{
    private $service;
    private $em;

    public function __construct(Service $service, EntityManager $em)
    {
        $this->service  = $service;
        $this->em       = $em;
    }

    public function loadUserByUsername($username)
    {
        // Do we have a local record?
        if ($user = $this->findUserBy(array('email' => $username))) {
            return $user;
        }

        // Try service
        if ($record = $this->service->getUser($username)) {
            // Set some fields
            $user = new User();
            $user->setUsername($username);
            return $user;
        }

        throw new UsernameNotFoundException(sprintf('No record found for user %s', $username));
    }

    public function refreshUser(UserInterface $user)
    {
        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\DemoBundle\Entity\User';
    }

    protected function findUserBy(array $criteria)
    {
        $repository = $this->em->getRepository('Acme\DemoBundle\Entity\User');
        return $repository->findOneBy($criteria);
    }
}

We add it to our services configuration in app/config/services.yml:

<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <parameters>
        <parameter key="security.user.provider.acme.service.class">Acme\DemoBundle\Security\Core\User\WebserviceUserProvider</parameter>
        <parameter key="acme.service.class">Acme\DemoBundle\Service\Service</parameter>
    </parameters>

    <services>
        <service id="acme_demo_webservice" class="%acme.service.class%">
        </service>
        <service id="acme_demo_user_provider" class="%security.user.provider.acme.service.class%">
            <argument type="service" id="acme_demo_webservice" />
            <argument type="service" id="doctrine.orm.entity_manager" />
        </service>
    </services>
</container>

Creating the AuthenticationProvider

As I said earlier we are going to base our provider on the DaoAuthenticationProvider. In my bundle I created a new class called ServiceAuthenticationProvider. Like our example we are extending the abstract UserAuthenticationProvider. Besides the checkAuthentication method we also must implement the retrieveUser method. We inject the service through the constructor, so the class looks like this:

<?php
namespace  Acme\DemoBundle\Security\Core\Authentication\Provider;

use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider;
use Acme\DemoBundle\Service\Service;

class EpsAuthenticationProvider extends UserAuthenticationProvider
{
    private $encoderFactory;
    private $userProvider;
    private $service;

    /**
     * @param Service $service
     * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider
     * @param UserCheckerInterface $userChecker
     * @param $providerKey
     * @param EncoderFactoryInterface $encoderFactory
     * @param bool $hideUserNotFoundExceptions
     */
    public function __construct(Service $service, UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true)
    {
        parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
        $this->encoderFactory   = $encoderFactory;
        $this->userProvider     = $userProvider;
        $this->service          = $service;
    }

    /**
     * {@inheritdoc}
     */
    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        $currentUser = $token->getUser();

        if ($currentUser instanceof UserInterface) {
            if ($currentUser->getPassword() !== $user->getPassword()) {
                throw new BadCredentialsException('The credentials were changed from another session.');
            }
        } else {
            if (!$presentedPassword = $token->getCredentials()) {
                throw new BadCredentialsException('The presented password cannot be empty.');
            }

            if (! $this->service->authenticate($token->getUser(), $presentedPassword)) {
                throw new BadCredentialsException('The presented password is invalid.');
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    protected function retrieveUser($username, UsernamePasswordToken $token)
    {
        $user = $token->getUser();
        if ($user instanceof UserInterface) {
            return $user;
        }

        try {
            $user = $this->userProvider->loadUserByUsername($username);

            if (!$user instanceof UserInterface) {
                throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
            }

            return $user;
        } catch (UsernameNotFoundException $notFound) {
            throw $notFound;
        } catch (\Exception $repositoryProblem) {
            throw new AuthenticationServiceException($repositoryProblem->getMessage(), $token, 0, $repositoryProblem);
        }
    }

Note the call to $this->service->authenticate where the magic happens. The retrieveUser method receives a User instance from our user provider. Although this is not really clear in the code above, it will be after configuration in the service container. We use the configuration from the Symfony core and adjust it to our needs:

<service id="security.authentication_provider.acme_demo_webservice" class="%security.authentication.provider.acme_service.class%" abstract="true" public="false">
  <argument type="service" id="acme_demo_webservice" />
  <argument /> <!-- User Provider -->
  <argument type="service" id="security.user_checker" />
  <argument /> <!-- Provider-shared Key -->
  <argument type="service" id="security.encoder_factory" />
  <argument>%security.authentication.hide_user_not_found%</argument>
</service>

Please note the empty arguments. Look a bit strange, huh? These will be magically filled when the container is build by our Factory! This is a bit tricky, and the cookbook explains pretty wel, so I suggest to take a look there. We are extending the FormLoginFactory because we want to change it bit:

<?php

namespace Acme\DemoBundle\DependencyInjection\Factory;

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

class SecurityFactory extends FormLoginFactory
{
    public function getKey()
    {
        return 'webservice-login';
    }

    protected function getListenerId()
    {
        return 'security.authentication.listener.form';
    }

    protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
    {
        $provider = 'security.authentication_provider.acme_demo_webservice.'.$id;
        $container
            ->setDefinition($provider, new DefinitionDecorator('security.authentication_provider.acme_demo_webservice'))
            ->replaceArgument(1, new Reference($userProviderId))
            ->replaceArgument(3, $id)
        ;

        return $provider;
    }
}

Add the builder in the Acme\DemoBundle\AcmeDemoBundle.php file:

<?php

namespace Acme\DemoBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Acme\DemoBundle\DependencyInjection\Factory\SecurityFactory;

class AcmeDemoBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $extension = $container->getExtension('security');
        $extension->addSecurityListenerFactory(new SecurityFactory());
    }
}

Finally, change your security config:

jms_security_extra:
    secure_all_services: false
    expressions: true

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:
        acme_provider:
            id: acme_demo_user_provider

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/demo/secured/login$
            security: false

        secured_area:
            pattern:    ^/demo/secured/
            webservice-login:
                check_path: /demo/secured/login_check
                login_path: /demo/secured/login
                provider: acme_provider
            logout:
                path:   /demo/secured/logout
                target: /demo/


    access_control:

The webservice-login key activates our authentication provider. The user provider is defined under providers as acme_provider with the corresponding service id.
I used the AcmeDemo bundle from symfony-standard repository, so you could just copy paste most of my code to see everything in action! Only thing you need to provide yourself is a dummy webservice.

Happy coding!

32 thoughts on “Symfony2 authentication provider: authenticate against webservice

  1. Onno

    Nice post, did put me on the right track.. I only get one error.

    $extension->addSecurityListenerFactory(new SecurityFactory());
    The method addSecurityListenerFactory does not exist. Which version of symfony do you use?

    Reply
    1. ricbra Post author

      Hi there! Sorry for the late response but the spam comments are really killing me on my blog… I missed your comment.

      I am using Symfony 2.1 master branch at the moment. Are you on 2.0?

      Reply
    2. Tim

      To get this to work with Symfony 2.0.x follow the instructions on .
      You do not need to edit Acme\DemoBundle\AcmeDemoBundle.php, instead you should create the file “security_factories.yml” according to the Symfony cookbook.

      Reply
  2. Giovanni Toraldo

    Great, I made it work, thanks for your article!
    However I’ve used .yml for service configuration:

    security.authentication_provider.acme_demo_webservice:
    class: %acme.service.class%
    arguments: ["@acme_demo_webservice", "", "@security.user_checker", "", "@security.encoder_factory"]

    Reply
    1. ricbra Post author

      The Acme\DemoBundle\Service\Service is just an imaginary webservice to authenticate against. It should be the PHP client for the webservice you are using.

      Reply
        1. ricbra Post author

          Hi Samurai K,

          The source of the client I’m using is private so I cannot share it. In the first place: Why are you trying to authenticate with a webservice when you don’t have a webservice and/or client? Do you have a webservice which provides authentication? What is your use case?

          I can supply an example of a client but I don’t see the actual use of it.

          Reply
  3. Sybio

    Hello ricbra, really nice article that helped me to understand how to set a custom user form. In addition, I would like to know if it’s possible to change the comportment of “$this->userProvider->loadUserByUsername”, because i need also to load a user by his username AND his websiteId (a second property of my User class). Do you have an idea how to perform this ?

    I posted my problem on the symfony forum here: http://forum.symfony-project.org/viewtopic.php?f=23&t=68449

    Now I know that I need to replace my SecurityFactoryInterface by a FormLoginFactory, that’s not so bad ;)

    Reply
    1. ricbra Post author

      Hi Sybio!

      The “loadUserByUSername” method is defined in the Symfony\Component\Security\Core\User\UserProviderInterface and you cannot change the method signature I’m afraid.

      You could create your own UserProvider and then inject the request via the container so you could read the current website through the request instead. Or won’t that work?

      Good luck! :)

      Reply
  4. Brad

    You don’t even know how much help you’ve already been! I’ve been struggling for days to find a way to auth with an external api. Thanks to you I’m up and running. A quick question though:

    What is the best way to keep the passed password encrypted? Currently I have set the encoder to plaintext as in your example, and I persist the User to the DB once the API has validated the credentials (in checkAuthentication() ).

    If I “$user->setPassword($presentedPassword);” with plaintext encoding I have no problem going forward (the user continues to be authenticated) however if I encode the password with the encoder factory set to sha512 or similar and then set “$user->setPassword($encodedPassword);”, the user no longer is authenticated passed the login_check route.

    I think it has something to do with the fact that the User’s password has changed (from plain to encoded) however the credentials in the token have not changed and so it boots the user out of the authenticated state.

    Any suggestions?

    Reply
    1. ricbra Post author

      I wasn’t storing the password (they aren’t of any use, because authentication happens somewhere else) so that’s why I don’t encode them. Why are you storing them anyway?

      Best solution in your case is to have a plaintextPassword property (not mapped to Doctrine) and an encodedPassword property which you persist.

      Reply
  5. Brad

    I’m storing them because down the road I’m planning on building a replacement for the web service I’m currently authenticating against. since all the users will be stored, when we make the switch, there won’t be any users to migrate.

    thanks for the tip!

    Reply
  6. Sylvain

    Thanks a lot for your article, ricbra. I spent days trying to implement the same kind of authentication without success. The Symfony cookbook article is not so simple to understand. Yours was in contrary very helpful.

    Thanks again.

    Reply
  7. Marie

    Hi!
    Thanks for your article!

    I have a question though. Is it normal that loadUserByUsername (which calls the WS) is called twice at each page (once in refreshUser ans then once in the custom Provider)? I don’t know if this is a good thing.

    In my case, once I have retrived my users data on login, I don’t actually need to call the WS again.
    The problem I have is that if I do not call loadUserByUsername in refreshUser, my Listener and Porivder are never called, thus my token is never verified and my session never expires.

    Is that normal? Can you help me, please?

    Reply
    1. ricbra Post author

      Hi Marie,

      That’s normal. I suggest to keep a local record of your users (in database or in at least in session) so you only connect once to your webservice.

      Reply
      1. Andrés

        Hi Ricbra. Thanks for this amazing article!

        I have the same question as Marie. I am still a newbie with Symfony, so i don´t understand very well how to keep the user in session to avoid connecting again to the webservice. Could you please tell me how to do it.

        Reply
  8. Pingback: Security authentication service hard to debug | StackAnswer.com

    1. ricbra Post author

      Hi Tom,

      You’re correct. EpsAuthenticationProvider and ServiceAuthenticationProvider are the same, I copied the code more or less from my project at work that time :) .

      Reply
      1. Tom N

        Do you have any idea why I would be getting an error:

        Unrecognized options “webservice_login” under “security.firewalls.main” ?

        Reply
  9. Tom N

    Ricbra,
    Let’s say I want to this a step further and want to use my web service to also return roles that user is part of, where should I do this? Should I do this in WebserviceUserProvider and set the roles in there also?

    Reply
  10. Pingback: Caillarec & Coste: Symfony2 : introduire une logique personnalisé durant le login

  11. Matt

    Hi Ricbra,

    Great post, it has really helped me to get this far, thank you. I am now struggling with how to actually get the user information from the web service. I am looking to send the username and password to the external API which will then feed back the information for me to populate a user object.

    In your instance, the user data is populated before the API is called and it is just authenticated against the API afterwards. How would you go about collecting user data from the API?

    Thanks,
    Matt

    Reply
  12. Google

    Hi, Neat post. There is a problem along with your site in internet explorer, would check this?
    IE nonetheless is the marketplace leader and a huge portion of other folks will leave out your excellent writing because of this problem.

    Reply
  13. http://www.team-wasp.com

    Fantastic blog! Do you have any tips and
    hints for aspiring writers? I’m hoping to start my own site soon but I’m
    a little lost on everything. Would you suggest starting with a free platform like WordPress or go for a paid option? There are so many choices out
    there that I’m completely overwhelmed .. Any suggestions?
    Kudos!

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>