Using a custom authentication provider in Symfony2

17,129

So I will answer my own question because I found the solution to my problem and I'll tell you how I solved it.

There was some mistake in my example and I understood them searching in the Symfony code.

Like the key returned by the getKey method of the Factory class. I found that the api one I've created was for me not an other parameter to my security.yml file, but a replacement to the http_basic one. That's why I'm having some trouble using two providers instead of just one, because I got two keys (api and http_basic) which both used a provider. In fact I think it's the reason to that problem.

To make it simple I follow the Symfony tutorial, except for the token class but I replaced the code of the new classes by the code of the Symfony classes. In a kind of way I recreated the http basic authentication of Symfony to make it posssible to overload. And here I am, I could do what I want, configure a different type of http authentication based on the Symfony one but with several changes.

This story helped me because know I know that the best way to understand Symfony principles is to go deeper in the code and look after.

Share:
17,129
Maxime
Author by

Maxime

Software engineer specialised in web technologies.

Updated on July 20, 2022

Comments

  • Maxime
    Maxime almost 2 years

    I'm working on a Symfony2 application with an API available for other applications. I want to secure the access to the API. For this part I have no problem.

    But I have to make this connection available not with the usual login/password couple but just with an API key.

    So I went to the official site and its awesome cookbook for creating a custom authentication provider, just what I need I said to myself.

    The example was not what I needed but I decided to adapt it to my needs.

    Unfortunately I didn't succeed.

    I'll give you my code and I will explain my problem after.

    Here is my Factory for creating the authentication provider and the listener:

    <?php
    
    namespace Pmsipilot\UserBundle\DependencyInjection\Security\Factory;
    
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\Config\Definition\Builder\NodeDefinition;
    use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
    
    class ApiFactory implements SecurityFactoryInterface
    {
      /**
       * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
       * @param string $id
       * @param aray $config
       * @param string $userProvider
       * @param string $defaultEntryPoint
       * @return array
       */
      public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
      {
        $providerId = 'security.authentification.provider.api.'.$id;
        $container
          ->setDefinition($providerId, new DefinitionDecorator('api.security.authentification.provider'))
          ->replaceArgument(0, new Reference($userProvider))
        ;
    
        $listenerId = 'security.authentification.listener.api.'.$id;
        $listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentification.listener'));
    
        return array($providerId, $listenerId, $defaultEntryPoint);
      }
    
      /**
       * @return string
       */
      public function getPosition()
      {
        return 'http';
      }
    
      /**
       * @return string
       */
      public function getKey()
      {
        return 'api';
      }
    
      /**
       * @param \Symfony\Component\Config\Definition\Builder\NodeDefinition $node
       * @return void
       */
      public function addConfiguration(NodeDefinition $node)
      {
      }
    }
    

    Next my listener code:

    <?php
    
    namespace Pmsipilot\UserBundle\Security\Firewall;
    
    use Symfony\Component\HttpFoundation\Response;
    use Symfony\Component\HttpKernel\Event\GetResponseEvent;
    use Symfony\Component\Security\Http\Firewall\ListenerInterface;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\SecurityContextInterface;
    use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Pmsipilot\UserBundle\Security\WsseUserToken;
    use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
    use Symfony\Component\Security\Core\Exception\BadCredentialsException;
    
    class ApiListener implements ListenerInterface
    {
      protected $securityContext;
      protected $authenticationManager;
    
      /**
       * Constructor for listener. The parameters are defined in services.xml.
       *
       * @param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
       * @param \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface $authenticationManager
       */
      public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
      {
        $this->securityContext = $securityContext;
        $this->authenticationManager = $authenticationManager;
      }
    
      /**
       * Handles login request.
       *
       * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
       * @return void
       */
      public function handle(GetResponseEvent $event)
      {
        $request = $event->getRequest();
    
        $securityToken = $this->securityContext->getToken();
    
        if($securityToken instanceof AuthenticationToken)
        {
          try
          {
            $this->securityContext->setToken($this->authenticationManager->authenticate($securityToken));
          }
          catch(\Exception $exception)
          {
            $this->securityContext->setToken(null);
          }
        }
      }
    }
    

    My authentication provider code:

    <?php
    
    namespace Pmsipilot\UserBundle\Security\Authentication\Provider;
    
    use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
    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\Token\TokenInterface;
    
    class ApiProvider implements AuthenticationProviderInterface
    {
      private $userProvider;
    
      /**
       * Constructor.
       *
       * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
       */
      public function __construct(UserProviderInterface $userProvider)
      {
        $this->userProvider = $userProvider;
      }
    
      /**
       * @param string $username
       * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
       * @return mixed
       * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
       */
      protected function retrieveUser($username, UsernamePasswordToken $token)
      {
        $user = $token->getUser();
        if($user instanceof UserInterface)
        {
          return $user;
        }
    
        try
        {
          $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());
    
          if(!$user instanceof UserInterface)
          {
            throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
          }
    
          return $user;
        }
        catch (\Exception $exception)
        {
          throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
        }
      }
    
      /**
       * @param TokenInterface $token
       * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
       * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
       */
      function authenticate(TokenInterface $token)
      {
        $username = $token->getUsername();
        if(empty($username))
        {
          throw new AuthenticationServiceException('No username given.');
        }
    
        try
        {
          $user = $this->retrieveUser($username, $token);
    
          if(!$user instanceof UserInterface)
          {
            throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
          }
    
          $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
          $authenticatedToken->setAttributes($token->getAttributes());
    
          return $authenticatedToken;
        }
        catch(\Exception $exception)
        {
          throw $exception;
        }
      }
    
      /**
       * @param TokenInterface $token
       * @return bool
       */
      public function supports(TokenInterface $token)
      {
        return true;
      }
    }
    

    To use these two objects I used a yml file to configure them:

    <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">
    
      <services>
        <service id="pmsipilot.api.security.authentication.factory" class="Pmsipilot\UserBundle\DependencyInjection\Security\Factory\ApiFactory" public="false">
          <tag name="security.listener.factory" />
        </service>
      </services>
    </container>
    

    Now the authentication provider code:

    <?php
    
    namespace Pmsipilot\UserBundle\Security\Authentication\Provider;
    
    use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
    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\Token\TokenInterface;
    
    class ApiProvider implements AuthenticationProviderInterface
    {
      private $userProvider;
    
      /**
       * Constructor.
       *
       * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
       */
      public function __construct(UserProviderInterface $userProvider)
      {
        $this->userProvider = $userProvider;
      }
    
      /**
       * @param string $username
       * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
       * @return mixed
       * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
       */
      protected function retrieveUser($username, UsernamePasswordToken $token)
      {
        $user = $token->getUser();
        if($user instanceof UserInterface)
        {
          return $user;
        }
    
        try
        {
          $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());
    
          if(!$user instanceof UserInterface)
          {
            throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
          }
    
          return $user;
        }
        catch (\Exception $exception)
        {
          throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
        }
      }
    
      /**
       * @param TokenInterface $token
       * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
       * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
       */
      function authenticate(TokenInterface $token)
      {
        $username = $token->getUsername();
        if(empty($username))
        {
          throw new AuthenticationServiceException('No username given.');
        }
    
        try
        {
          $user = $this->retrieveUser($username, $token);
    
          if(!$user instanceof UserInterface)
          {
            throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
          }
    
          $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
          $authenticatedToken->setAttributes($token->getAttributes());
    
          return $authenticatedToken;
        }
        catch(\Exception $exception)
        {
          throw $exception;
        }
      }
    
      /**
       * @param TokenInterface $token
       * @return bool
       */
      public function supports(TokenInterface $token)
      {
        return true;
      }
    }
    

    Just FYI my user provider:

    <?php
    
    namespace Pmsipilot\UserBundle\Security\Provider;
    
    use Propel\PropelBundle\Security\User\ModelUserProvider;
    use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
    use \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
    
    class ApiProvider extends ModelUserProvider
    {
      /**
       * Constructeur
       */
      public function __construct()
      {
        parent::__construct('Pmsipilot\UserBundle\Model\User', 'Pmsipilot\UserBundle\Proxy\User', 'username');
      }
    
      /**
       * @param string $apikey
       * @return mixed
       * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
       */
      public function loadUserByApiKey($apikey)
      {
        $queryClass = $this->queryClass;
        $query      = $queryClass::create();
    
        $user = $query
          ->filterByApiKey($apikey)
          ->findOne()
        ;
    
        if(null === $user)
        {
          throw new UsernameNotFoundException(sprintf('User with "%s" api key not found.', $apikey));
        }
        $proxyClass = $this->proxyClass;
        return new $proxyClass($user);
      }
    }
    

    And for the configuration part my security.yml:

    security:
      factories:
        PmsipilotFactory: "%kernel.root_dir%/../src/Pmsipilot/UserBundle/Resources/config/security_factories.xml"
    
      providers:
        interface_provider:
          id: pmsipilot.security.user.provider
        api_provider:
          id: api.security.user.provider
    
      encoders:
        Pmsipilot\UserBundle\Proxy\User: sha512
    
      firewalls:
        assets:
          pattern:                ^/(_(profiler|wdt)|css|images|js|favicon.ico)/
          security:               false
    
        api:
          provider:               api_provider
          access_denied_url:      /unauthorizedApi
          pattern:                ^/api
          api:                    true
          http_basic:             true
          stateless:              true
    
        interface:
          provider:               interface_provider
          access_denied_url:      /unauthorized
          pattern:                ^/
          anonymous:              ~
          form_login:
            login_path:           /login
            check_path:           /login_check
            use_forward:          true
            default_target_path:  /
          logout:                 ~
    
      access_control:
        - { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: SUPER_ADMIN }
    

    Wow it's a lot of code, I hope it's not too boring.

    My problem here is that my custom authentication provider is called by the two firewalls api and interface instead of just by the api one. And of course they don't behave as I wanted.

    I didn't find anything about such an issue. I know I made a mistake, otherwise it will be working, but where and why I don't know.

    I also found this tutorial but it didn't help much more.

    Of course, don't hesitate to suggest me if there is another solution for using another authentication provider than the default one.