How to Create a custom User Provider
Edit this pageWarning: You are browsing the documentation for Symfony 3.4, which is no longer maintained.
Read the updated version of this page for Symfony 6.3 (the current stable version).
How to Create a custom User Provider
Part of Symfony's standard authentication process depends on "user providers".
When a user submits a username and password, the authentication layer asks
the configured user provider to return a user object for a given username.
Symfony then checks whether the password of this user is correct and generates
a security token so the user stays authenticated during the current session.
Out of the box, Symfony has four user providers: memory
, entity
,
ldap
and chain
. In this article you'll see how you can create your
own user provider, which could be useful if your users are accessed via a
custom database, a file, or - as shown in this example - a web service.
Create a User Class
First, regardless of where your user data is coming from, you'll need to
create a User
class that represents that data. The User
can look
however you want and contain any data. The only requirement is that the
class implements UserInterface.
The methods in this interface should therefore be defined in the custom user
class: getRoles(),
getPassword(),
getSalt(),
getUsername(),
eraseCredentials().
It may also be useful to implement the
EquatableInterface interface,
which defines a method to check if the user is equal to the current user. This
interface requires an isEqualTo()
method.
This is how your WebserviceUser
class looks in action:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
// src/AppBundle/Security/User/WebserviceUser.php
namespace AppBundle\Security\User;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class WebserviceUser implements UserInterface, EquatableInterface
{
private $username;
private $password;
private $salt;
private $roles;
public function __construct($username, $password, $salt, array $roles)
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
}
public function getRoles()
{
return $this->roles;
}
public function getPassword()
{
return $this->password;
}
public function getSalt()
{
return $this->salt;
}
public function getUsername()
{
return $this->username;
}
public function eraseCredentials()
{
}
public function isEqualTo(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
return false;
}
if ($this->password !== $user->getPassword()) {
return false;
}
if ($this->salt !== $user->getSalt()) {
return false;
}
if ($this->username !== $user->getUsername()) {
return false;
}
return true;
}
}
If you have more information about your users - like a "first name" - then
you can add a firstName
field to hold that data.
Create a User Provider
Now that you have a User
class, you'll create a user provider, which will
grab user information from some web service, create a WebserviceUser
object,
and populate it with data.
The user provider is just a plain PHP class that has to implement the
UserProviderInterface,
which requires three methods to be defined: loadUserByUsername($username)
,
refreshUser(UserInterface $user)
, and supportsClass($class)
. For
more details, see UserProviderInterface.
Here's an example of how this might look:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
// src/AppBundle/Security/User/WebserviceUserProvider.php
namespace AppBundle\Security\User;
use AppBundle\Security\User\WebserviceUser;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
return $this->fetchUser($username);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof WebserviceUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
$username = $user->getUsername();
return $this->fetchUser($username);
}
public function supportsClass($class)
{
return WebserviceUser::class === $class;
}
private function fetchUser($username)
{
// make a call to your webservice here
$userData = ...
// pretend it returns an array on success, false if there is no user
if ($userData) {
$password = '...';
// ...
return new WebserviceUser($username, $password, $salt, $roles);
}
throw new UsernameNotFoundException(
sprintf('Username "%s" does not exist.', $username)
);
}
}
Create a Service for the User Provider
Now you make the user provider available as a service. If you're using the default services.yml configuration, this happens automatically.
Modify security.yml
Everything comes together in your security configuration. Add the user provider
to the list of providers in the "security" section. Choose a name for the user provider
(e.g. webservice
) and mention the id
of the service you just defined.
1 2 3 4 5 6 7
# app/config/security.yml
security:
# ...
providers:
webservice:
id: AppBundle\Security\User\WebserviceUserProvider
1 2 3 4 5 6 7 8 9 10 11 12 13 14
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<!-- ... -->
<provider name="webservice" id="AppBundle\Security\User\WebserviceUserProvider"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12
// app/config/security.php
use AppBundle\Security\User\WebserviceUserProvider;
$container->loadFromExtension('security', [
// ...
'providers' => [
'webservice' => [
'id' => WebserviceUserProvider::class,
],
],
]);
Symfony also needs to know how to encode passwords that are supplied by website users, e.g. by filling in a login form. You can do this by adding a line to the "encoders" section in your security configuration:
1 2 3 4 5 6
# app/config/security.yml
security:
# ...
encoders:
AppBundle\Security\User\WebserviceUser: bcrypt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<!-- ... -->
<encoder class="AppBundle\Security\User\WebserviceUser"
algorithm="bcrypt"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11
// app/config/security.php
use AppBundle\Security\User\WebserviceUser;
$container->loadFromExtension('security', [
// ...
'encoders' => [
WebserviceUser::class => 'bcrypt',
],
// ...
]);
The value here should correspond with however the passwords were originally
encoded when creating your users (however those users were created). When
a user submits their password, it's encoded using this algorithm and the result
is compared to the hashed password returned by your getPassword()
method.
Specifics on how Passwords are Encoded
Symfony uses a specific method to combine the salt and encode the password
before comparing it to your encoded password. If getSalt()
returns
nothing, then the submitted password is simply encoded using the algorithm
you specify in security.yml
. If a salt is specified, then the following
value is created and then hashed via the algorithm:
1
$password.'{'.$salt.'}'
If your external users have their passwords salted via a different method,
then you'll need to do a bit more work so that Symfony properly encodes
the password. That is beyond the scope of this article, but would include
sub-classing MessageDigestPasswordEncoder
and overriding the
mergePasswordAndSalt()
method.
Additionally, you can configure the details of the algorithm used to hash passwords. In this example, the application sets explicitly the cost of the bcrypt hashing:
1 2 3 4 5 6 7 8
# app/config/security.yml
security:
# ...
encoders:
AppBundle\Security\User\WebserviceUser:
algorithm: bcrypt
cost: 12
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://symfony.com/schema/dic/services"
xsi:schemaLocation="http://symfony.com/schema/dic/services
https://symfony.com/schema/dic/services/services-1.0.xsd">
<config>
<!-- ... -->
<encoder class="AppBundle\Security\User\WebserviceUser"
algorithm="bcrypt"
cost="12"/>
</config>
</srv:container>
1 2 3 4 5 6 7 8 9 10 11 12 13
// app/config/security.php
use AppBundle\Security\User\WebserviceUser;
$container->loadFromExtension('security', [
// ...
'encoders' => [
WebserviceUser::class => [
'algorithm' => 'bcrypt',
'cost' => 12,
],
],
]);