Skip to content

Commit 6892bab

Browse files
Hugo Hamonweaverryan
Hugo Hamon
authored andcommitted
added new cookbook entry about how to use a custom Doctrine repository object as an EntityUserProvider provider.
1 parent 9016514 commit 6892bab

File tree

1 file changed

+326
-7
lines changed

1 file changed

+326
-7
lines changed

cookbook/security/entity_provider.rst

Lines changed: 326 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,329 @@
1-
How to load Security Users from the Database (the entity Provider)
1+
.. index::
2+
single: Security; User Provider
3+
4+
How to load Security Users from the Database (the Entity Provider)
25
==================================================================
36

4-
This article has not been written yet, but will soon. If you're interested
5-
in writing this entry, see :doc:`/contributing/documentation/overview`.
7+
The security layer is one of the most smart tools of Symfony. It handles two
8+
things: the authentication and the authorization processes. Although it seems
9+
a bit quiet complex to understand how everything works internally, this security
10+
system is very flexible and allows you to plug your application to any
11+
authentication backends like an Active Directory, an OAuth server or a database.
12+
13+
Introduction
14+
------------
15+
16+
This article focuses on how to authenticate users against a database table
17+
managed by a Doctrine entity class. The content of this cookbook entry is split
18+
in three parts. The first part is about designing a Doctrine ``User`` entity
19+
class and make it usable in the security layer of Symfony. The second part
20+
describes how to easily authenticate a user with the Doctrine
21+
`EntityUserProvider`_ object bundled with the framework and some configuration.
22+
Finally, the tutorial will demonstrate how to create a custom
23+
`EntityUserProvider`_ object to retrieve users from a database with custom
24+
conditions.
25+
26+
This tutorial assumes there is a bootstrapped and loaded
27+
``Acme\Bundle\UserBundle`` bundle in the application kernel.
28+
29+
The Data Model
30+
--------------
31+
32+
For the purpose of this cookbook, the ``AcmeUserBundle`` bundle contains a
33+
``User`` entity class with the following fields: ``id``, ``username``, ``salt``,
34+
``password``, ``role`` and ``isActive``. The ``role`` field stores the role
35+
string assigned to the user and the ``isActive`` value tells whether or not the
36+
user account is active.
37+
38+
To make it shorter, the getter and setter method for each have been removed to
39+
focus on the most important methods that come from the `UserInterface`_.
40+
41+
.. code-block:: php
42+
43+
// src/Acme/Bundle/UserBundle/Entity/User.php
44+
45+
namespace Acme\Bundle\UserBundle\Entity;
46+
47+
use Symfony\Component\Security\Core\User\UserInterface;
48+
use Doctrine\ORM\Mapping as ORM;
49+
50+
/**
51+
* Acme\Bundle\UserBundle\Entity\User
52+
*
53+
* @ORM\Table()
54+
* @ORM\Entity(repositoryClass="Acme\Bundle\UserBundle\Entity\UserRepository")
55+
*/
56+
class User implements UserInterface
57+
{
58+
/**
59+
* @ORM\Column(name="id", type="integer")
60+
* @ORM\Id()
61+
* @ORM\GeneratedValue(strategy="AUTO")
62+
*/
63+
private $id;
64+
65+
/**
66+
* @ORM\Column(name="username", type="string", length=25, unique=true)
67+
*/
68+
private $username;
69+
70+
/**
71+
* @ORM\Column(name="salt", type="string", length=40)
72+
*/
73+
private $salt;
74+
75+
/**
76+
* @ORM\Column(name="password", type="string", length=40)
77+
*/
78+
private $password;
79+
80+
/**
81+
* @ORM\Column(name="role", type="string", length=20)
82+
*/
83+
private $role;
84+
85+
/**
86+
* @ORM\Column(name="is_active", type="boolean")
87+
*/
88+
private $isActive;
89+
90+
public function __construct()
91+
{
92+
$this->role = 'ROLE_USER';
93+
$this->isActive = true;
94+
}
95+
96+
public function getRoles()
97+
{
98+
return array($this->role);
99+
}
100+
101+
public function equals(UserInterface $user)
102+
{
103+
return $user->getUsername() === $this->username;
104+
}
105+
106+
public function eraseCredentials()
107+
{
108+
}
109+
110+
public function getUsername()
111+
{
112+
return $this->username;
113+
}
114+
115+
public function getSalt()
116+
{
117+
return $this->salt;
118+
}
119+
120+
public function getPassword()
121+
{
122+
return $this->password;
123+
}
124+
}
125+
126+
In order to use an instance of the ``AcmeUserBundle:User`` class in the Symfony
127+
security layer, the entity class must implement the `UserInterface`_. This
128+
interface forces the class to implement the six following methods:
129+
130+
* ``getUsername()`` returns the unique username,
131+
* ``getSalt()`` returns the unique salt,
132+
* ``getPassword()`` returns the encoded password,
133+
* ``getRoles()`` returns an array of associated roles,
134+
* ``equals()`` compares the current object with an other `UserInterface`_
135+
instance,
136+
* ``eraseCredentials()`` removes sensible information stored in the
137+
`UserInterface`_ object.
138+
139+
To keep it simple, the ``equals()`` method just compares the ``username`` field
140+
but it's also possible to make more checks depending on the complexity of your
141+
data model. In the other hand, the ``eraseCredentials()`` method remains empty
142+
as we don't care about it in this tutorial.
143+
144+
Below is an export of my ``User`` table from MySQL.
145+
146+
.. code-block::text
147+
148+
mysql> select * from user;
149+
+----+----------+------------------------------------------+------------------------------------------+-----------------+-----------+
150+
| id | username | salt | password | role | is_active |
151+
+----+----------+------------------------------------------+------------------------------------------+-----------------+-----------+
152+
| 1 | hhamon | 7308e59b97f6957fb42d66f894793079c366d7c2 | 09610f61637408828a35d7debee5b38a8350eebe | ROLE_SUPERADMIN | 1 |
153+
| 2 | jsmith | ce617a6cca9126bf4036ca0c02e82deea081e564 | 8390105917f3a3d533815250ed7c64b4594d7ebf | ROLE_ADMIN | 1 |
154+
| 3 | maxime | cd01749bb995dc658fa56ed45458d807b523e4cf | 9764731e5f7fb944de5fd8efad4949b995b72a3c | ROLE_ADMIN | 0 |
155+
| 4 | donald | 6683c2bfd90c0426088402930cadd0f84901f2f4 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | ROLE_USER | 1 |
156+
+----+----------+------------------------------------------+------------------------------------------+-----------------+-----------+
157+
4 rows in set (0.00 sec)
158+
159+
The database now contains four users with different roles and statuses. The next
160+
part will focus on how to authenticate one these users thanks to the Doctrine
161+
entity user provider and a couple of lines of configuration.
162+
163+
Authenticating Someone Against a Database
164+
-----------------------------------------
165+
166+
Authenticating a Doctrine user against the database with the Symfony security
167+
layer is a piece of cake. Everything resides in the configuration of the
168+
``app/config/security.yml`` file.
169+
170+
Below is an example of configuration to authenticate the user with an HTTP basic
171+
authentication connected to the database.
172+
173+
.. code-block::yaml
174+
175+
# app/config/security.yml
176+
security:
177+
encoders:
178+
Acme\Bundle\UserBundle\Entity\User:
179+
algorithm: sha1
180+
encode_as_base64: false
181+
iterations: 1
182+
183+
role_hierarchy:
184+
ROLE_ADMIN: ROLE_USER
185+
ROLE_SUPERADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
186+
187+
providers:
188+
administrators:
189+
entity: { class: AcmeUserBundle:User, property: username }
190+
191+
firewalls:
192+
admin_area:
193+
pattern: ^/admin
194+
http_basic: ~
195+
196+
access_control:
197+
- { path: ^/admin, roles: ROLE_ADMIN }
198+
199+
The ``encoders`` section associates the ``sha1`` password encoder to the entity
200+
class. It allows Symfony to check your user credentials by calling the
201+
``isPasswordValid()`` method of the encoder object.
202+
203+
The ``providers`` section defines an ``administrators`` provider. The ``entity``
204+
keyword means Symfony will use the Doctrine entity user provider. This provider
205+
is configured to use the ``AcmeUserBundle:User`` model class and retrieve a
206+
user instance by using the ``username`` unique field. In other words, this
207+
configuration tells Symfony how to fetch the user from the
208+
database before checking the password validity.
209+
210+
This code and configuration works but it's not enough to secure the application
211+
for **active** users, who **own** ``ROLE_ADMIN`` or ``ROLE_SUPERADMIN`` role.
212+
As of now, we still can authenticate with both users ``maxime`` and
213+
``donald``...
214+
215+
Authenticating Someone with a Custom Entity Provider
216+
----------------------------------------------------
217+
218+
To limit access to the administration area to active people with ``ROLE_ADMIN``
219+
or ``ROLE_SUPERADMIN``, the best way is to write a custom entity provider that
220+
fetches a user with a custom SQL query.
221+
222+
The good news is that a Doctrine repository object can act as an entity user
223+
provider if it implements the `UserProviderInterface`_. This interface comes
224+
with three methods to implement:
225+
226+
* ``loadUserByUsername()`` that fetches and returns a `UserInterface`_
227+
instance by its unique username. Otherwise, it must throw a
228+
`UsernameNotFoundException`_ exception to indicate the security layer
229+
there is no user matching the credentials.
230+
* ``refreshUser()`` that refreshes and returns a `UserInterface`_ instance.
231+
Otherwise it must throw a `UnsupportedUserException`_ exception to
232+
indicate the security layer we are unable to refresh the user.
233+
* ``supportsClass()`` must return ``true`` if the fully qualified class name
234+
passed as its sole argument is supported by the entity provider.
235+
236+
The code below shows the implementation of the `UserProviderInterface`_ in the
237+
``UserRepository`` class.
238+
239+
.. code-block::php
240+
241+
// src/Acme/Bundle/UserBundle/Entity/UserRepository.php
242+
243+
namespace Acme\Bundle\UserBundle\Entity;
244+
245+
use namespace Symfony\Component\Security\Core\User\UserInterface;
246+
use namespace Symfony\Component\Security\Core\User\UserProviderInterface;
247+
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
248+
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
249+
use Doctrine\ORM\EntityRepository;
250+
251+
class UserRepository extends EntityRepository implements UserProviderInterface
252+
{
253+
public function loadUserByUsername($username)
254+
{
255+
$q = $this
256+
->getAdministratorQueryBuilder()
257+
->where('u.username = :username')
258+
->setParameter('username', $username)
259+
->getQuery()
260+
;
261+
262+
try {
263+
// The Query::getSingleResult() method throws an exception
264+
// if there is no record matching the criteria.
265+
$user = $q->getSingleResult();
266+
} catch (\Exception $e) {
267+
throw new UsernameNotFoundException(sprintf('Unable to find an active admin AcmeUserBundle:User object identified by "%s".', $username), null, 0, $e);
268+
}
269+
270+
return $user;
271+
}
272+
273+
public function refreshUser(UserInterface $user)
274+
{
275+
$username = $user->getUsername();
276+
277+
try {
278+
$user = $this->loadUserByUsername($username);
279+
} catch (UsernameNotFoundException $e) {
280+
throw new UnsupportedUserException(sprintf('Unable to refresh active admin AcmeUserBundle:User object identified by "%s".', $username), null, 0, $e);
281+
}
282+
283+
return $user;
284+
}
285+
286+
public function supportsClass($class)
287+
{
288+
return 'Acme\Bundle\UserBundle\Entity\User' === $class;
289+
}
290+
291+
private function getAdministratorQueryBuilder()
292+
{
293+
$qb = $this
294+
->createQueryBuilder('u')
295+
->where('u.isActive = :status')
296+
->andWhere('u.role IN (:role)')
297+
->setParameter('status', true)
298+
->setParameter('role', array('ROLE_ADMIN', 'ROLE_SUPERADMIN'))
299+
;
300+
301+
return $qb;
302+
}
303+
}
304+
305+
To finish the implementation, the configuration of the security layer must be
306+
changed to tell Symfony to use the new custom entity provider instead of the
307+
generic Doctrine entity provider. It's trival to achieve by removing the
308+
``property`` variable in the ``security.providers.administrators.entity`` in the
309+
``security.yml`` file.
310+
311+
.. code-block::yaml
312+
313+
# app/config/security.yml
314+
security:
315+
# ...
316+
providers:
317+
administrators:
318+
entity: { class: AcmeUserBundle:User }
319+
# ...
320+
321+
By doing this, the security layer will use an instance of ``UserRepository`` and
322+
call its ``loadUserByUsername()`` method to fetch an active administrator user
323+
from the database.
6324

7-
This topic is meant to be a full working example of how to use the ``entity``
8-
user provider with the security component. It should show how to create the
9-
``User`` class, implement the methods, mapping, etc - everything you'll need
10-
to get a fully-functionality entity user provider working.
325+
.. _`EntityUserProvider`: http://api.symfony.com/2.0/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.html
326+
.. _`UserInterface`: http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserInterface.html
327+
.. _`UserProviderInterface`: http://api.symfony.com/2.0/Symfony/Component/Security/Core/User/UserProviderInterface.html
328+
.. _`UsernameNotFoundException`: http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/UsernameNotFoundException.html
329+
.. _`UnsupportedUserException`: http://api.symfony.com/2.0/Symfony/Component/Security/Core/Exception/UnsupportedUserException.html

0 commit comments

Comments
 (0)