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="https://symfony.com/schema/dic/services"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://symfony.com/schema/dic/services https://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!
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?
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.
Thanks,I had both
login_path
andcheck_path
set to/login
in the firewall. This works when using doctrine, but did not work here.I changedcheck_path
to/process_login
and created an empty route in my controller, and now everything kicks off as it should.Cool, good luck!
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 ?
Thanks for your example !
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
Hi,
Nice article very useful.
Looks like your YamlUser class is missing the $roles property though?
Thanks, I will add the property.