How to load Security Users from the Database (the Entity Provider)

Caution: You are browsing the documentation for Symfony version 2.0 which is not maintained anymore. If some of your projects are still using this version, consider upgrading to Symfony 2.5.

How to load Security Users from the Database (the Entity Provider)

The security layer is one of the smartest tools of Symfony. It handles two things: the authentication and the authorization processes. Although it may seem difficult to understand how it works internally, the security system is very flexible and allows you to integrate your application with any authentication backend, like Active Directory, an OAuth server or a database.

Introduction

This article focuses on how to authenticate users against a database table managed by a Doctrine entity class. The content of this cookbook entry is split in three parts. The first part is about designing a Doctrine User entity class and making it usable in the security layer of Symfony. The second part describes how to easily authenticate a user with the Doctrine EntityUserProvider object bundled with the framework and some configuration. Finally, the tutorial will demonstrate how to create a custom EntityUserProvider object to retrieve users from a database with custom conditions.

This tutorial assumes there is a bootstrapped and loaded Acme\UserBundle bundle in the application kernel.

The Data Model

For the purpose of this cookbook, the AcmeUserBundle bundle contains a User entity class with the following fields: id, username, salt, password, email and isActive. The isActive field tells whether or not the user account is active.

To make it shorter, the getter and setter methods for each have been removed to focus on the most important methods that come from the UserInterface.

  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
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Acme\UserBundle\Entity\User
 *
 * @ORM\Table(name="acme_users")
 * @ORM\Entity(repositoryClass="Acme\UserBundle\Entity\UserRepository")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=25, unique=true)
     */
    private $username;

    /**
     * @ORM\Column(type="string", length=32)
     */
    private $salt;

    /**
     * @ORM\Column(type="string", length=40)
     */
    private $password;

    /**
     * @ORM\Column(type="string", length=60, unique=true)
     */
    private $email;

    /**
     * @ORM\Column(name="is_active", type="boolean")
     */
    private $isActive;

    public function __construct()
    {
        $this->isActive = true;
        $this->salt = md5(uniqid(null, true));
    }

    /**
     * @inheritDoc
     */
    public function getUsername()
    {
        return $this->username;
    }

    /**
     * @inheritDoc
     */
    public function getSalt()
    {
        return $this->salt;
    }

    /**
     * @inheritDoc
     */
    public function getPassword()
    {
        return $this->password;
    }

    /**
     * @inheritDoc
     */
    public function getRoles()
    {
        return array('ROLE_USER');
    }

    /**
     * @inheritDoc
     */
    public function eraseCredentials()
    {
    }

    /**
     * @inheritDoc
     */
    public function equals(UserInterface $user)
    {
        return $this->id === $user->getId();
    }

    /**
     * @see \Serializable::serialize()
     */
    public function serialize()
    {
        return serialize(array(
            $this->id,
        ));
    }

    /**
     * @see \Serializable::unserialize()
     */
    public function unserialize($serialized)
    {
        list (
            $this->id,
        ) = unserialize($serialized);
    }
}

In order to use an instance of the AcmeUserBundle:User class in the Symfony security layer, the entity class must implement the UserInterface. This interface forces the class to implement the six following methods:

  • getUsername()
  • getSalt()
  • getPassword()
  • getRoles()
  • eraseCredentials()
  • equals()

For more details on each of these, see UserInterface.

To keep it simple, the equals() method just compares the id field but it's also possible to do more checks depending on the complexity of your data model. On the other hand, the eraseCredentials() method remains empty for the purposes of this tutorial.

Note

The Serializable interface and its serialize and unserialize methods have been added to allow the User class to be serialized to the session. This may or may not be needed depending on your setup, but it's probably a good idea. Only the id needs to be serialized, because the refreshUser() method reloads the user on each request by using the id.

Below is an export of my User table from MySQL. For details on how to create user records and encode their password, see Encoding the User's Password.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ mysql> select * from user;
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
| id | username | salt                             | password                                 | email              | is_active |
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
|  1 | hhamon   | 7308e59b97f6957fb42d66f894793079 | 09610f61637408828a35d7debee5b38a8350eebe | hhamon@example.com |         1 |
|  2 | jsmith   | ce617a6cca9126bf4036ca0c02e82dee | 8390105917f3a3d533815250ed7c64b4594d7ebf | jsmith@example.com |         1 |
|  3 | maxime   | cd01749bb995dc658fa56ed45458d807 | 9764731e5f7fb944de5fd8efad4949b995b72a3c | maxime@example.com |         0 |
|  4 | donald   | 6683c2bfd90c0426088402930cadd0f8 | 5c3bcec385f59edcc04490d1db95fdb8673bf612 | donald@example.com |         1 |
+----+----------+----------------------------------+------------------------------------------+--------------------+-----------+
4 rows in set (0.00 sec)

The database now contains four users with different usernames, emails and statuses. The next part will focus on how to authenticate one of these users thanks to the Doctrine entity user provider and a couple of lines of configuration.

Authenticating Someone against a Database

Authenticating a Doctrine user against the database with the Symfony security layer is a piece of cake. Everything resides in the configuration of the SecurityBundle stored in the app/config/security.yml file.

Below is an example of configuration where the user will enter his/her username and password via HTTP basic authentication. That information will then be checked against your User entity records in the database:

  • YAML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    # app/config/security.yml
    security:
        encoders:
            Acme\UserBundle\Entity\User:
                algorithm:        sha1
                encode_as_base64: false
                iterations:       1
    
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
    
        providers:
            administrators:
                entity: { class: AcmeUserBundle:User, property: username }
    
        firewalls:
            admin_area:
                pattern:    ^/admin
                http_basic: ~
    
        access_control:
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- app/config/security.xml -->
    <config>
        <encoder class="Acme\UserBundle\Entity\User"
            algorithm="sha1"
            encode-as-base64="false"
            iterations="1"
        />
    
        <role id="ROLE_ADMIN">ROLE_USER</role>
        <role id="ROLE_SUPER_ADMIN">ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
    
        <provider name="administrators">
            <entity class="AcmeUserBundle:User" property="username" />
        </provider>
    
        <firewall name="admin_area" pattern="^/admin">
            <http-basic />
        </firewall>
    
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
    
  • PHP
     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
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'encoders' => array(
            'Acme\UserBundle\Entity\User' => array(
                'algorithm'         => 'sha1',
                'encode_as_base64'  => false,
                'iterations'        => 1,
            ),
        ),
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array('ROLE_USER', 'ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'),
        ),
        'providers' => array(
            'administrator' => array(
                'entity' => array(
                    'class'    => 'AcmeUserBundle:User',
                    'property' => 'username',
                ),
            ),
        ),
        'firewalls' => array(
            'admin_area' => array(
                'pattern' => '^/admin',
                'http_basic' => null,
            ),
        ),
        'access_control' => array(
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

The encoders section associates the sha1 password encoder to the entity class. This means that Symfony will expect the password that's stored in the database to be encoded using this algorithm. For details on how to create a new User object with a properly encoded password, see the Encoding the User's Password section of the security chapter.

The providers section defines an administrators user provider. A user provider is a "source" of where users are loaded during authentication. In this case, the entity keyword means that Symfony will use the Doctrine entity user provider to load User entity objects from the database by using the username unique field. In other words, this tells Symfony how to fetch the user from the database before checking the password validity.

This code and configuration works but it's not enough to secure the application for active users. As of now, you can still authenticate with maxime. The next section explains how to forbid non active users.

Forbid non Active Users

The easiest way to exclude non active users is to implement the AdvancedUserInterface interface that takes care of checking the user's account status. The AdvancedUserInterface extends the UserInterface interface, so you just need to switch to the new interface in the AcmeUserBundle:User entity class to benefit from simple and advanced authentication behaviors.

The AdvancedUserInterface interface adds four extra methods to validate the account status:

  • isAccountNonExpired() checks whether the user's account has expired,
  • isAccountNonLocked() checks whether the user is locked,
  • isCredentialsNonExpired() checks whether the user's credentials (password) has expired,
  • isEnabled() checks whether the user is enabled.

For this example, the first three methods will return true whereas the isEnabled() method will return the boolean value in the isActive field.

 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
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

// ...
use Symfony\Component\Security\Core\User\AdvancedUserInterface;

class User implements AdvancedUserInterface
{
    // ...

    public function isAccountNonExpired()
    {
        return true;
    }

    public function isAccountNonLocked()
    {
        return true;
    }

    public function isCredentialsNonExpired()
    {
        return true;
    }

    public function isEnabled()
    {
        return $this->isActive;
    }
}

If you try to authenticate as maxime, the access is now forbidden as this user does not have an enabled account. The next session will focus on how to write a custom entity provider to authenticate a user with his username or his email address.

Authenticating Someone with a Custom Entity Provider

The next step is to allow a user to authenticate with his username or his email address as they are both unique in the database. Unfortunately, the native entity provider is only able to handle a single property to fetch the user from the database.

To accomplish this, create a custom entity provider that looks for a user whose username or email field matches the submitted login username. The good news is that a Doctrine repository object can act as an entity user provider if it implements the UserProviderInterface. This interface comes with three methods to implement: loadUserByUsername($username), refreshUser(UserInterface $user), and supportsClass($class). For more details, see UserProviderInterface.

The code below shows the implementation of the UserProviderInterface in the UserRepository class:

 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
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NoResultException;

class UserRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->where('u.username = :username OR u.email = :email')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->getQuery()
        ;

        try {
            // The Query::getSingleResult() method throws an exception
            // if there is no record matching the criteria.
            $user = $q->getSingleResult();
        } catch (NoResultException $e) {
            $message = sprintf(
                'Unable to find an active admin AcmeUserBundle:User object identified by "%s".',
                $username
            );
            throw new UsernameNotFoundException($message, null, 0, $e);
        }

        return $user;
    }

    public function refreshUser(UserInterface $user)
    {
        $class = get_class($user);
        if (!$this->supportsClass($class)) {
            throw new UnsupportedUserException(
                sprintf(
                    'Instances of "%s" are not supported.',
                    $class
                )
            );
        }

        return $this->find($user->getId());
    }

    public function supportsClass($class)
    {
        return $this->getEntityName() === $class
            || is_subclass_of($class, $this->getEntityName());
    }
}

To finish the implementation, the configuration of the security layer must be changed to tell Symfony to use the new custom entity provider instead of the generic Doctrine entity provider. It's trivial to achieve by removing the property field in the security.providers.administrators.entity section of the security.yml file.

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # app/config/security.yml
    security:
        # ...
        providers:
            administrators:
                entity: { class: AcmeUserBundle:User }
        # ...
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
    
        <provider name="administrator">
            <entity class="AcmeUserBundle:User" />
        </provider>
    
        <!-- ... -->
    </config>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    // app/config/security.php
    $container->loadFromExtension('security', array(
        ...,
        'providers' => array(
            'administrator' => array(
                'entity' => array(
                    'class' => 'AcmeUserBundle:User',
                ),
            ),
        ),
        ...,
    ));
    

By doing this, the security layer will use an instance of UserRepository and call its loadUserByUsername() method to fetch a user from the database whether he filled in his username or email address.

Managing Roles in the Database

The end of this tutorial focuses on how to store and retrieve a list of roles from the database. As mentioned previously, when your user is loaded, its getRoles() method returns the array of security roles that should be assigned to the user. You can load this data from anywhere - a hardcoded list used for all users (e.g. array('ROLE_USER')), a Doctrine array property called roles, or via a Doctrine relationship, as you'll learn about in this section.

Caution

In a typical setup, you should always return at least 1 role from the getRoles() method. By convention, a role called ROLE_USER is usually returned. If you fail to return any roles, it may appear as if your user isn't authenticated at all.

In this example, the AcmeUserBundle:User entity class defines a many-to-many relationship with a AcmeUserBundle:Group entity class. A user can be related to several groups and a group can be composed of one or more users. As a group is also a role, the previous getRoles() method now returns the list of related groups:

 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
// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
// ...

class User implements AdvancedUserInterface, \Serializable
{
    /**
     * @ORM\ManyToMany(targetEntity="Group", inversedBy="users")
     *
     */
    private $groups;

    public function __construct()
    {
        $this->groups = new ArrayCollection();
    }

    // ...

    public function getRoles()
    {
        return $this->groups->toArray();
    }

    /**
     * @see \Serializable::serialize()
     */
    public function serialize()
    {
        return serialize(array(
            $this->id,
        ));
    }

    /**
     * @see \Serializable::unserialize()
     */
    public function unserialize($serialized)
    {
        list (
            $this->id,
        ) = unserialize($serialized);
    }
}

The AcmeUserBundle:Group entity class defines three table fields (id, name and role). The unique role field contains the role name used by the Symfony security layer to secure parts of the application. The most important thing to notice is that the AcmeUserBundle:Group entity class extends the Role:

 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
// src/Acme/Bundle/UserBundle/Entity/Group.php
namespace Acme\UserBundle\Entity;

use Symfony\Component\Security\Core\Role\Role;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="acme_groups")
 * @ORM\Entity()
 */
class Group extends Role
{
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="name", type="string", length=30)
     */
    private $name;

    /**
     * @ORM\Column(name="role", type="string", length=20, unique=true)
     */
    private $role;

    /**
     * @ORM\ManyToMany(targetEntity="User", mappedBy="groups")
     */
    private $users;

    public function __construct()
    {
        $this->users = new ArrayCollection();
    }

    // ... getters and setters for each property

    /**
     * @see RoleInterface
     */
    public function getRole()
    {
        return $this->role;
    }
}

To improve performances and avoid lazy loading of groups when retrieving a user from the custom entity provider, the best solution is to join the groups relationship in the UserRepository::loadUserByUsername() method. This will fetch the user and his associated roles / groups with a single query:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/Acme/UserBundle/Entity/UserRepository.php
namespace Acme\UserBundle\Entity;

// ...

class UserRepository extends EntityRepository implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        $q = $this
            ->createQueryBuilder('u')
            ->select('u, g')
            ->leftJoin('u.groups', 'g')
            ->where('u.username = :username OR u.email = :email')
            ->setParameter('username', $username)
            ->setParameter('email', $username)
            ->getQuery();

        // ...
    }

    // ...
}

The QueryBuilder::leftJoin() method joins and fetches related groups from the AcmeUserBundle:User model class when a user is retrieved with his email address or username.

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License .