Symfony2: How to create a UserProvider

Posted on by Matthias Noback

After writing this article I've turned it into an official Cookbook article. After some time it has been unrecognizably rewritten by someone else.

Symfony2 firewalls depend for their authentication on UserProviders. These providers are requested by the authentication layer to provide a User object, for a given username. Symfony will check whether the password of this User is correct (i.e. verify it's password) and will then generate a security token, so the user may stay authenticated during the current session. Out of the box, Symfony has a "in_memory" user provider and an "entity" user provider. In this post I'll show you how to create your own UserProvider. The UserProvider in this example, tries to load a Yaml file containing information about users in the following format:

username:
    password: secret
    roles: [ROLE_USER]

First create the YamlUser class in /src/Acme/YamlUserBundle/Security/User/YamlUser.php. This class should implement the UserInterface. The methods in this interface should therefore be defined in the YamlUser class. These methods are getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials() and equals(). I didn't find out what eraseCredentials should do yet, but the other methods speak for themselves. getRoles() should always return an array, getSalt() may return nothing.

namespace Acme\YamlUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;

class YamlUser implements UserInterface
{
    protected $username;
    protected $password;
    protected $roles;

    public function __construct($username, $password, array $roles)
    {
        $this->username = $username;
        $this->password = $password;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

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

    public function getSalt()
    {
    }

    public function getUsername()
    {
        return $this->username;
    }    

    public function eraseCredentials()
    {
    }

    public function equals(UserInterface $user)
    {
        if (!$user instanceof YamlUser) {
            return false;
        }

        if ($this->password !== $user->getPassword()) {
            return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        return true;
    }
}

Next we will create a UserProvider, in this case the YamlUserProvider. This is the provider of YamlUser instances. It has to implement the UserProviderInterface, which requires three methods: loadUserByUsername($username), refreshUser(User $user) and supportsClass($class).

loadUserByUsername($username) does the actual loading of the user: it looks for a user with the given username in any way it deems appropriate and returns a User instance (in our example a YamlUser). If the user can't be found, this method must throw a UsernameNotFoundException.

refreshUser(User $user) refreshes the information of the given user. It must check if the given User is an instance of the User class that is supported by this specific UserProvider. If not, an UnsupportedUserException should be thrown.

supportsClass($class) should return true if this UserProvider can handle users of the given class, false if not.

The implementation for YamlUser would be something like this:

namespace Acme\YamlUserBundle\Security\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 Symfony\Component\Yaml\Yaml;

class YamlUserProvider implements UserProviderInterface
{
    protected $users;

    public function __construct($yml_path)
    {
        $userDefinitions = Yaml::parse($yml_path);

        $this->users = array();

        foreach ($userDefinitions as $username => $attributes) {
            $password = isset($attributes['password']) ? $attributes['password'] : null;
            $roles = isset($attributes['roles']) ? $attributes['roles'] : array();

            $this->users[$username] = new YamlUser($username, $password, $roles);
        }
    }

    public function loadUserByUsername($username)
    {
        if (!isset($this->users[$username])) {
            throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
        }

        $user = $this->users[$username];

        return new YamlUser($user->getUsername(), $user->getPassword(), $user->getRoles());
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof YamlUser) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getUsername());        
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\YamlUserBundle\Security\User\YamlUser';
    }
}

As you can see, the UserProvider constructor depends on one argument, the path to the Yaml file that contains the information about the users.

Now we must let Symfony know that a new UserProvider was born: we will define the YamlUserProvider as a service in /src/Acme/YamlUserBundle/Resources/config/services.xml. We also should provide this service with the necessary constructor argument, i.e. the path to the Yaml file.

<?xml version="1.0" ?>

<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="yaml_user_provider.class">Acme\YamlUserBundle\Security\User\YamlUserProvider</parameter>
    </parameters>

    <services>
        <service id="yaml_user_provider" class="%yaml_user_provider.class%">
            <argument>%kernel.root_dir%/Resources/users.yml</argument>
        </service>
    </services>

</container>

Now create the file /app/Resources/users.yml containing the user data, e.g.

matthias:
    password: 'kd98d7gl'
    roles: [ROLE_USER]
liesbeth:
    password: '97dnlo9d'
    roles: [ROLE_ADMIN]

In app/config/security.yml everything comes together. Add the Yaml user provider: choose a name (e.g. "yaml") and supply the id of the service you just defined:

security:
    providers:
        yaml:
            id: yaml_user_provider

Symfony also needs to know how to encode passwords that are supplied by users in the login form. In our case, the YamlUser's password is not encoded in any way; it is stored in plain text. Please not that this would normally not be a recommended way to store password.

We define which encoder to use for the YamlUser by adding a line to the "encoders" in /app/config/security.yml:

security:
    encoders:
        Acme\YamlUserBundle\Security\User\YamlUser: plaintext

Now visit the secured area of the demo (/app_dev.php/demo/secured) and try to login with the credentials you wrote down in app/Resources/users.yml. Everything should work now!

PHP Security Symfony2 user provider authentication
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
McMurphy 510

Trying to follow this in Symfony 4. I think I've made all the requisite changes between Symfony 2 and 4, however, I'm still a bit confused. From where does DatabaseUserProvider::loadUserByUsername() get called?

Matthias Noback

If you configure your class as a service and configure it as a user provider in security.yml, Symfony will call that method upon login.

McMurphy 510

Thanks,

I had both `login_path` and `check_path` set to `/login` in the firewall. This works when using doctrine, but did not work here.

I changed `check_path` to `/process_login` and created an empty route in my controller, and now everything kicks off as it should.

Matthias Noback

Cool, good luck!

Alexis

Hello,
you wrote "Out of the box, Symfony has a "in_memory" user provider and an "entity" user provider".

Can we use "in_memory" with a custom user provider ?

giusepe

Thanks for your example !

Pierrick

Hello,
thanks a lot for your interesting post, very well detailed!
I am a newbie with Symfony2 and I would like to use your UserProvider for automatically login a user (by an url) for development purposes.
But it does not work because of firewall problem I think understand: the token is not persistent...
If you have any advice about this kind of problem ?
Thanks in advance

Chris

Hi,

Nice article very useful.
Looks like your YamlUser class is missing the $roles property though?

Matthias Noback

Thanks, I will add the property.