.. index:: single: Security; 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 an "in_memory" and an "entity" user provider. In this entry we'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 we show in this example - a web service.
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 :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
The methods in this interface should therefore be defined in the custom user
class: getRoles()
, getPassword()
, getSalt()
, getUsername()
,
eraseCredentials()
, equals()
.
Let's see this in action:
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php namespace Acme\WebserviceUserBundle\Security\User; use Symfony\Component\Security\Core\User\UserInterface; class WebserviceUser implements UserInterface { 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 equals(UserInterface $user) { if (!$user instanceof WebserviceUser) { 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; } }
If you have more information about your users - like a "first name" - then
you can add a firstName
field to hold that data.
For more details on each of the methods, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
Now that we have a User
class, we'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
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`,
which requires three methods to be defined: loadUserByUsername($username)
,
refreshUser(UserInterface $user)
, and supportsClass($class)
. For
more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`.
Here's an example of how this might look:
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php namespace Acme\WebserviceUserBundle\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; class WebserviceUserProvider implements UserProviderInterface { public function loadUserByUsername($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)); } public function refreshUser(UserInterface $user) { if (!$user instanceof WebserviceUser) { 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\WebserviceUserBundle\Security\User\WebserviceUser'; } }
Now we make the user provider available as a service.
.. configuration-block:: .. code-block:: yaml # src/Acme/WebserviceUserBundle/Resources/config/services.yml parameters: webservice_user_provider.class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider services: webservice_user_provider: class: "%webservice_user_provider.class%" .. code-block:: xml <!-- src/Acme/WebserviceUserBundle/Resources/config/services.xml --> <parameters> <parameter key="webservice_user_provider.class">Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider</parameter> </parameters> <services> <service id="webservice_user_provider" class="%webservice_user_provider.class%"></service> </services> .. code-block:: php // src/Acme/WebserviceUserBundle/Resources/config/services.php use Symfony\Component\DependencyInjection\Definition; $container->setParameter('webservice_user_provider.class', 'Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider'); $container->setDefinition('webservice_user_provider', new Definition('%webservice_user_provider.class%');
Tip
The real implementation of the user provider will probably have some dependencies or configuration options or other services. Add these as arguments in the service definition.
Note
Make sure the services file is being imported. See :ref:`service-container-imports-directive` for details.
In /app/config/security.yml
everything comes together. 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.
security:
providers:
webservice:
id: webservice_user_provider
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 /app/config/security.yml
.
security:
encoders:
Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
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 her password, the password is appended to the salt value and
then encoded using this algorithm before being compared to the hashed password
returned by your getPassword()
method. Additionally, depending on your
options, the password may be encoded multiple times and encoded to base64.
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:
$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 entry, but would include
sub-classing MessageDigestPasswordEncoder
and overriding the mergePasswordAndSalt
method.
Additionally, the hash, by default, is encoded multiple times and encoded
to base64. For specific details, see MessageDigestPasswordEncoder.
To prevent this, configure it in security.yml
:
security:
encoders:
Acme\WebserviceUserBundle\Security\User\WebserviceUser:
algorithm: sha512
encode_as_base64: false
iterations: 1