Skip to content

Commit fddaa3f

Browse files
committed
[cookbook][security] Finishing up the excellent new custom user provider entry by @matthiasnoback. Also removing some of the information about UserInterface and UserProviderInterface because we'll need to push this information back onto the actual PHPDoc API (which I'll do shortly)
1 parent 230168c commit fddaa3f

File tree

2 files changed

+110
-130
lines changed

2 files changed

+110
-130
lines changed

cookbook/security/custom_provider.rst

Lines changed: 103 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,160 @@
1+
.. index::
2+
single: Security; User Provider
3+
14
How to create a custom User Provider
25
====================================
36

4-
Symfony firewalls depend for their authentication on user providers.
5-
These providers are requested by the authentication layer to provide a
6-
user object for a given username. Symfony will check whether the
7-
password of this user is correct and will then generate a security token,
8-
so the user may stay authenticated during the current session. Out of
9-
the box, Symfony has an "in_memory" and an "entity" user provider.
10-
In this entry we'll see how you can create your own user provider.
11-
This may be a custom type of database, file or, in this example,
12-
a webservice.
13-
14-
Create a user class
7+
Part of Symfony's standard authentication process depends on "user providers".
8+
When a user submits a username and password, the authentication layer asks
9+
the configured user provider to return a user object for a given username.
10+
Symfony then checks whether the password of this user is correct and generates
11+
a security token so the user stays authenticated during the current session.
12+
Out of the box, Symfony has an "in_memory" and an "entity" user provider.
13+
In this entry we'll see how you can create your own user provider, which
14+
could be useful if your users are accessed via a custom database, a file,
15+
or - as we show in this example - a web service.
16+
17+
Create a User Class
1518
-------------------
1619

17-
First create the user class for your specific type of user. The class should
18-
implement :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
20+
First, regardless of *where* your user data is coming from, you'll need to
21+
create a ``User`` class that represents that data. The ``User`` can look
22+
however you want and contain any data. The only requirement is that the
23+
class implements :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
1924
The methods in this interface should therefore be defined in the custom user
20-
class:
21-
22-
``getRoles()``
23-
Return an array with the role(s) for this user, e.g. ``array('ROLE_USER')``.
24-
25-
``getPassword()``
26-
Return the password of the user (this would better be an encrypted password,
27-
see below).
28-
29-
``getSalt()``
30-
Return the "salt" that should be used for encrypting the password. Return
31-
nothing (i.e. ``null``) when the password needs no salt.
32-
33-
``getUsername()``
34-
Return the username of this user, i.e. the name by which it can authenticate.
35-
36-
``eraseCredentials()``
37-
Return nothing, but erase sensitive data from the user object. Don't erase
38-
credentials.
39-
40-
``equals(UserInterface $user)``
41-
The first argument of this method is an object which implements ``UserInterface``.
42-
When called upon a user, this method returns ``true`` when the given user is the same
43-
as itself, or ``false`` if it is not. This means you should compare several
44-
crucial properties like username, password and salt. Symfony uses this method to
45-
find out if the user should reauthenticate.
46-
47-
.. code-block:: php
25+
class: ``getRoles()``, ``getPassword()``, ``getSalt()``, ``getUsername()``,
26+
``eraseCredentials()``, ``equals()``.
27+
28+
Let's see this in action::
4829

30+
// src/Acme/WebserviceUserBundle/Security/User.php
4931
namespace Acme\WebserviceUserBundle\Security\User;
50-
32+
5133
use Symfony\Component\Security\Core\User\UserInterface;
52-
34+
5335
class WebserviceUser implements UserInterface
5436
{
5537
private $username;
5638
private $password;
5739
private $salt;
5840
private $roles;
59-
41+
6042
public function __construct($username, $password, $salt, array $roles)
6143
{
6244
$this->username = $username;
6345
$this->password = $password;
6446
$this->salt = $salt;
6547
$this->roles = $roles;
6648
}
67-
49+
6850
public function getRoles()
6951
{
7052
return $this->roles;
7153
}
72-
54+
7355
public function getPassword()
7456
{
7557
return $this->password;
7658
}
77-
59+
7860
public function getSalt()
7961
{
8062
return $this->salt;
8163
}
82-
64+
8365
public function getUsername()
8466
{
8567
return $this->username;
8668
}
87-
69+
8870
public function eraseCredentials()
8971
{
9072
}
91-
73+
9274
public function equals(UserInterface $user)
9375
{
9476
if (!$user instanceof WebserviceUser) {
9577
return false;
9678
}
97-
79+
9880
if ($this->password !== $user->getPassword()) {
9981
return false;
10082
}
101-
83+
10284
if ($this->getSalt() !== $user->getSalt()) {
10385
return false;
10486
}
105-
87+
10688
if ($this->username !== $user->getUsername()) {
10789
return false;
10890
}
109-
91+
11092
return true;
11193
}
11294
}
11395

114-
Create a user provider
96+
If you have more information about your users - like a "first name" - then
97+
you can add a ``firstName`` field to hold that data.
98+
99+
For more details on each of the methods, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
100+
101+
Create a User Provider
115102
----------------------
116103

117-
Next we will create a user provider, in this case a ``WebserviceUserProvider``.
118-
It provides the firewall with instances of ``WebserviceUser``. The user provider
119-
has to implement the
120-
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`,
121-
which requires three methods to be defined:
122-
123-
``loadUserByUsername($username)``
124-
Does the actual loading of the user: it looks for a user with the given username
125-
in any way that seems appropriate to it and returns a user object (in our example
126-
a ``WebserviceUser``). If the user was not found, this method must throw a
127-
``UsernameNotFoundException``.
128-
129-
``refreshUser(UserInterface $user)``
130-
Refreshes the information of the given user. It must check if the given user object
131-
is an instance of the user class that is supported by this specific user provider.
132-
If not, an ``UnsupportedUserException`` should be thrown.
133-
134-
``supportsClass($class)``
135-
Should return ``true`` if this user provider can handle users of the given class,
136-
``false`` if not.
137-
138-
.. code-block:: php
104+
Now that we have a ``User`` class, we'll create a user provider, which will
105+
grab user information from some web service, create a ``WebserviceUser`` object,
106+
and populate it with data.
139107

108+
The user provider is just a plain PHP class that has to implement the
109+
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`,
110+
which requires three methods to be defined: ``loadUserByUsername($username)``,
111+
``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For
112+
more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`.
113+
114+
Here's an example of how this might look::
115+
116+
// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
140117
namespace Acme\WebserviceUserBundle\Security\User;
141-
118+
142119
use Symfony\Component\Security\Core\User\UserProviderInterface;
143120
use Symfony\Component\Security\Core\User\UserInterface;
144-
145121
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
146122
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
147-
123+
148124
class WebserviceUserProvider implements UserProviderInterface
149125
{
150126
public function loadUserByUsername($username)
151127
{
152-
try {
153-
// make a call to your webservice here:
154-
// throw a WebserviceUserNotFoundException (or something alike) if you did not find the requested user
155-
// $user = ...;
156-
157-
return new WebserviceUser($user->getUsername(), $user->getPassword(), $user->getSalt(), $user->getRoles())
158-
} catch(WebserviceUserNotFoundException $e) {
128+
// make a call to your webservice here
129+
// $userData = ...
130+
// pretend it returns an array on success, false if there is no user
131+
132+
if ($userData) {
133+
// $password = '...';
134+
// ...
135+
136+
return new WebserviceUser($username, $password, $salt, $roles)
137+
} else {
159138
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
160139
}
161140
}
162-
141+
163142
public function refreshUser(UserInterface $user)
164143
{
165144
if (!$user instanceof WebserviceUser) {
166145
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
167146
}
168-
147+
169148
return $this->loadUserByUsername($user->getUsername());
170149
}
171-
150+
172151
public function supportsClass($class)
173152
{
174153
return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser';
175154
}
176155
}
177156

178-
Create a service for the user provider
157+
Create a Service for the User Provider
179158
--------------------------------------
180159

181160
Now we make the user provider available as service.
@@ -212,16 +191,21 @@ Now we make the user provider available as service.
212191
213192
$container->setDefinition('webservice_user_provider', new Definition('%webservice_user_provider.class%');
214193
215-
.. note::
194+
.. tip::
216195

217196
The real implementation of the user provider will probably have some
218-
dependencies or configuration options. Add these as arguments in the
219-
service definition.
197+
dependencies or configuration options or other services. Add these as
198+
arguments in the service definition.
199+
200+
.. note::
220201

221-
Modify `security.yml`
222-
---------------------
202+
Make sure the services file is being imported. See :ref:`service-container-imports-directive`
203+
for details.
223204

224-
In ``app/config/security.yml`` everything comes together. Add the user provider
205+
Modify ``security.yml``
206+
-----------------------
207+
208+
In ``/app/config/security.yml`` everything comes together. Add the user provider
225209
to the list of providers in the "security" section. Choose a name for the user provider
226210
(e.g. "webservice") and mention the id of the service you just defined.
227211

@@ -241,3 +225,25 @@ users, e.g. by filling in a login form. You can do this by adding a line to the
241225
security:
242226
encoders:
243227
Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
228+
229+
The value here should correspond with however the passwords were originally
230+
encoded when creating your users (however those users were created). When
231+
a user submits her password, the password is appended to the salt value and
232+
then encoded using this algorithm before being compared to the hashed password
233+
returned by your ``getPassword()`` method.
234+
235+
.. sidebar:: Specifics on how passwords are encoded
236+
237+
Symfony uses a specific method to combine the salt and encode the password
238+
before comparing it to your encoded password. If ``getSalt()`` returns
239+
nothing, then the submitted password is simply encoded using the algorithm
240+
you specify in ``security.yml``. If a salt *is* specified, then the following
241+
value is created and *then* hashed via the algorithm:
242+
243+
``$password.'{'.$salt.'}';``
244+
245+
If your external users have their passwords salted via a different method,
246+
then you'll need to do a bit more work so that Symfony properly encodes
247+
the password. That is beyond the scope of this entry, but would include
248+
sub-classing ``MessageDigestPasswordEncoder`` and overriding the ``mergePasswordAndSalt``
249+
method.

cookbook/security/entity_provider.rst

Lines changed: 7 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.. index::
22
single: Security; User Provider
3+
single: Security; Entity Provider
34

45
How to load Security Users from the Database (the Entity Provider)
56
==================================================================
@@ -127,30 +128,15 @@ focus on the most important methods that come from the
127128
In order to use an instance of the ``AcmeUserBundle:User`` class in the Symfony
128129
security layer, the entity class must implement the
129130
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. This
130-
interface forces the class to implement the six following methods:
131-
132-
* ``getUsername()`` returns the unique username,
133-
* ``getSalt()`` returns the unique salt,
134-
* ``getPassword()`` returns the encoded password,
135-
* ``getRoles()`` returns an array of associated roles,
136-
* ``equals()`` compares the current object with another
137-
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`
138-
instance,
139-
* ``eraseCredentials()`` removes sensitive information stored in the
140-
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` object.
131+
interface forces the class to implement the six following methods: ``getRoles()``,
132+
``getPassword()``, ``getSalt()``, ``getUsername()``, ``eraseCredentials()``,
133+
``equals()``. For more details on each of these, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`.
141134

142135
To keep it simple, the ``equals()`` method just compares the ``username`` field
143136
but it's also possible to do more checks depending on the complexity of your
144137
data model. On the other hand, the ``eraseCredentials()`` method remains empty
145138
as we don't care about it in this tutorial.
146139

147-
.. note::
148-
149-
The ``eraseCredentials()`` method is important if, during your authentication
150-
process, you store some sort of sensitive information on the user (e.g.
151-
the raw password of the user). This is called after authentication, and
152-
allows you to remove any of that information.
153-
154140
Below is an export of my ``User`` table from MySQL. For details on how to
155141
create user records and encode their password, see :ref:`book-security-encoding-user-password`.
156142

@@ -300,21 +286,9 @@ whose username *or* email field matches the submitted login username.
300286
The good news is that a Doctrine repository object can act as an entity user
301287
provider if it implements the
302288
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. This
303-
interface comes with three methods to implement:
304-
305-
* ``loadUserByUsername()`` that fetches and returns a
306-
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`
307-
instance by its unique username. Otherwise, it must throw a
308-
:class:`Symfony\\Component\\Security\\Core\\Exception\\UsernameNotFoundException`
309-
exception to indicate the security layer
310-
there is no user matching the credentials.
311-
* ``refreshUser()`` that refreshes and returns a
312-
:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` instance.
313-
Otherwise it must throw a
314-
:class:`Symfony\\Component\\Security\\Core\\Exception\\UnsupportedUserException`
315-
exception to indicate that we are unable to refresh the user.
316-
* ``supportsClass()`` must return ``true`` if the fully qualified class name
317-
passed as its sole argument is supported by the entity provider.
289+
interface comes with three methods to implement: ``loadUserByUsername($username)``,
290+
``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For
291+
more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`.
318292

319293
The code below shows the implementation of the
320294
:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface` in the

0 commit comments

Comments
 (0)