Symfony2 Pre-registration and Invite System

We have discussed Symfony 2 development in previous SitePoint articles and we built a clone of my own personal Symfony app (Part 1, 2 and 3). But Symfony 2 is a gigantic framework and there are plenty more topics we can cover.


In this article series of two parts, we are going to talk about a very important area of web application development: Authentication and Authorization. Symfony2 has a very detailed elaboration of these two topics in its official documentation. Anyone who is seriously looking into this is encouraged to read through that official document after absorbing the essentials from this tutorial.

A typical User Management flow may have the following tasks:

  • A built-in user will be generated upon application installation and will be granted the root equivalent privilege.
  • Any new user can either register via a form or can only register via invitation (which is the approach discussed in this article).
  • After registration, a user record is stored into the underlying database/table.
  • Optionally, the app will put this new user in a “pending” status and send out a confirmation email. A user will only be “activated” when they click the link in the email with a confirmation token. This approach is not used in this article because we are inviting users and the site is a “closed circle” site.
  • A user logs in. The app will verify the user name and password.
  • Optionally, the app can do some post-login activities. In this case, we will update the user’s last login date/time in the database, and redirect them.
  • The user can explicitly choose to logout.

The underlying user table

Although Symfony supports in-memory user authentication, that is not recommended in a real-world application. Most of the time, we’ll tap into other resources (database, LDAP, etc) to persist the user credentials. We will use an SQL database in our app.

First, let’s create a table to store user information:

CREATE TABLE `user` ( 
	`username` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 
	`password` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 
	`email` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 
	`created` DATETIME NOT NULL, 
	`logged` DATETIME NULL, 
	`roles` VARCHAR( 25 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 
	`gravatar` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, 
	`active` TINYINT( 1 ) NOT NULL, 
	`homepage` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
	 PRIMARY KEY ( `id` )

Out of these defined fields, username and password are used to grant/deny a login request. This is called authentication.

After a successful login, the user will be granted a ROLE, which is defined by the roles field. Different roles will have different access rights when visiting URIs. This is called authorization.

username, password and roles together form the cornerstones of our security system.

Configuration: security.yml

Symfony uses a security.yml file to hold all the settings and configuration related to app security.

Below are the contents of our security.yml file (located in app/config):

            entity: { class: AppBundle:User, property: username }
            algorithm: bcrypt
            cost: 12    
            pattern: ^/(_(profiler|wdt|error)|css|images|js)/
            security: false

            anonymous: ~
            http_basic: ~
                login_path: /login
                check_path: /login_check    
                path: /logout
                target: /login
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/preregister, roles: IS_AUTHENTICATED_ANONYMOUSLY }            
        - { path: ^/create, roles: IS_AUTHENTICATED_ANONYMOUSLY }                
        - { path: ^/invite, roles: [ROLE_ADMIN] }
        - { path: ^/, roles: [ROLE_USER, ROLE_ADMIN] }

Under security, we have four sub-sections:

providers: those responsible for providing user credentials

We will store user information in a table, reflected in Symfony as an entity (a PHP class) with necessary getters/setters to manipulate the properties (table fields).

In this case, the entity is AppBundle:User, which comes from src/AppBundle/Entity/User.php and the property we use to verify a login request is coming from the username.

Symfony supports using other fields or one of several fields as the identifier of a user. A common scenario is the user being able to log in using either the username or the email field. Please refer to Symfony’s official documentation if you want to know more on this.

How the password is encoded: encoders

Storing passwords in plain text form is never a good idea. In our case, we specify that we will use the bcrypt method with 12 loops. This will produce a reasonably strong hash with reasonable cost (of machine calculation time – the longer it takes for a password hash to be generated, the harder it is for a brute force attack to succeed).

Login and logout functionality: firewalls/default

We must specify how we log in. Symfony can use HTTP Basic login (that pops up a dialog for username/password input) or login forms. In our case, we use a form:

	login_path: /login
    check_path: /login_check

Of course, we let the user have the option to explicitly log out:

	path: /logout
    target: /login

The target setting here tells the app where to go (in this case back to the login page) after the user logs out.

To make the above settings work properly, Symfony requires three routes in the routing.yml file:

    path: /logout
    path: /login
    defaults: { _controller: AppBundle:Security:login}    
    path: /login_check

In particular, the /login path must be associated with a proper action in a controller (AppBundle:Security:login). The other two can be defined with a name and a path.


In this sub-section, we define the mapping between URI patterns and the access level required.

In general, we have 3 different levels:

  • IS_AUTHENTICATED_ANONYMOUSLY – this means anybody. No access control will be applied when a URI is visited with this access right.
  • ROLE_USER – a regular user that passes the login verification can access this URI.
  • ROLE_ADMIN – a superuser who has some special privileges can access this URI.

The names (ROLE_USER and ROLE_ADMIN) are artificial. Developers can apply their own set of names to be used in the app (for example, using USER and ADMIN is also fine).

A common pitfall when defining the access control is making the control too strict and unusable.

No matter how strict the app wants the access control to be, the app MUST at least allow anonymous access to the login page. And like Symfony’s routes mapping, we MUST define the more specific rules earlier.

We have to free up access to all the routes related to user registration (register, preregister, create) and login (login) to allow anonymous access. These routes are defined first.

To restrict the inviting functionality to a user (invite), we state that this job can only be done by an administrator.

Finally, we “close” the whole site (^/, except for those URI patterns defined earlier) for everyone but “insiders” (ROLE_USER or ROLE_ADMIN is required).

The AppBundle:User entity

My preference is to have the AppBundle:User entity generated by importing an existing db structure. This entity by itself can’t be used by the authentication process yet. We need some further modifications:


namespace AppBundle\Entity;

use Symfony\Component\Security\Core\User\UserInterface;

 * User
class User implements UserInterface, \Serializable

     * @var integer
    private $id;

//All the table properties follow...

    public function getId()
        return $this->id;

//All the getters/setters follow...

     * Get roles
     * @return [string]
    public function getRoles()
        return [$this->roles];

    public function eraseCredentials()

    public function serialize()

    public function unserialize($serialized)
    public function getSalt()


The User class must implement two interfaces: Symfony\Component\Security\Core\User\UserInterface and \Serializable. This will require us to define additional methods including eraseCredentials, serialize, unserialize and getSalt.

A note on the definition of the getRoles method. roles is a field in our user table so the getter is automatically generated like this:

public function getRoles()
        return $this->roles;

This is because we define the roles field as simple as a VARCHAR(255), which will return the string we stored. However, Symfony requires roles in an array format, so even though we only have one string-based role saved for our user at this time, we accommodate a multiple-roles system and the roles-as-array requirement by exploding this string. That way, if we ever want a user to have multiple roles, we can save them as a comma separated list (ROLE_ADMIN, ROLE_BLOGGER, ROLE_ACCOUNTANT). Thus, we tweak the method like this:

public function getRoles()
        return explode(',', $this->roles);

It’s time to move on to the logic, but before that, let’s create the very first root user with ultimate permissions.

It is easy to populate all fields in the user table for a user, except one: the password field. We are using bcrypt to hash our password so we cannot put a plain text password in it. To help us through this tedious task, we use this online tool. My sample root user’s SQL INSERT statement (hash calculated for the password “test”) follows below:

(`username`, `password`, `email`, `roles`, `active`) 
VALUES ('root', '$2a$12$3INF/uK0.pmZAMFW0In0uOrfpq.yaVIK0xwmWO8Yjxhs4m8CC1Ei2', '', 'ROLE_ADMIN', '1');

You have an invitation from the ADMIN

When an administrator visits the /invite page, they can put in an email address to invite someone. The app will then build an email and send it. The email will contain a link for the recipient to register and a unique invitation code to verify the invitation is coming from a trusted source. That’s all. In the backend, the recipient’s email, invitation code and an expiration date will be stored in the database table.

The method code of doing so is below:

public function doinviteAction(Request $req)

        $email  = $req->get('email');
        $userid = $req->get('user');

        $hash = $this->setInvite($userid, $email);
        $this->sendMail($email, $hash);

        $url = $this->generateUrl('invite');
        return $this->redirect($url);

	private function setInvite($userid, $email)
        $em        = $this->getDoctrine()->getManager();
        $user_repo = $em->getRepository('AppBundle:User');
        $user      = $user_repo->find($userid); //The user who initiates the invitation

        $invite = new Invite();

        $now = new \DateTime();
        $int = new \DateInterval('P1D');
        $invite->setExpires($now); //Set invitation expirary 

        $random = rand(10000, 99999);

        $invite->setHash($random); //A random number is used but stricter method to create a verification code can be used. 


        return $random;

    private function sendMail($email, $hash)
        $mailer = $this->get('mailer');

        $message = $mailer->createMessage()
                ->setSubject('Someone invites you to join')
                $this->renderView('AppBundle:Default:email.html.twig', ['email' => $email, 'hash' => $hash]), 'text/html'


We get the user who sends the invitation ($userid) and the user whom they intend to invite ($email). We then create the invitation record and generate a random number associated with this invitation as the invitation code. All the information required to send an invitation (including the invitation code) will be saved and persisted. Finally, we return the invitation code.

After the invitation is saved, we send the email. I am using the default Symfony mail service to do this. Please note that for the email body, I am using a Twig template to render the rich content.

Since only an administrator can invite another person, we need to control page contents based on user authorization. The default template engine used in Symfony, Twig, has a helper for that:

{% if is_granted('ROLE_ADMIN') %}
	<li><a href="{{path('invite')}}"><i class="large-icon-share"></i>&nbsp;&nbsp;Invite</a></li>
{% endif %}

In this template, we use the is_granted helper to restrict the display of a menu item for admin users only.

Registration starts

When a recipient receives an invitation from the admin, they can click the link and verify the invitation.

A valid invitation must satisfy the following conditions:

  1. The email is actually invited.
  2. The invitation is not expired.
  3. The confirmation code is verified and paired with that email.

All the above is done in the respective controller action:

public function preregisterAction(Request $req)
        $email = $req->get('email');
        $invitationCode   = $req->get('code');

            $this->verify($email, $invitationCode);
        catch (\Exception $e)
            $this->addFlash('error', $e->getMessage());
            return $this->redirectToRoute('error');

        $registration = new User();
        $form         = $this->createForm(new RegistrationType(), $registration, ['action' => $this->generateUrl('create'), 'method' => 'POST']);

        return $this->render('AppBundle:Default:register2.html.twig', ['form' => $form->createView(), 'email' => $email]);

    private function verify($email, $code)
        $repo    = $this->getDoctrine()->getManager()->getRepository('AppBundle:Invite');
        $invites = $repo->findBy(['invited' => $email]);

        if (count($invites) == 0)
            throw new \Exception("This email is not invited.");
        $invite = $invites[0];

        $exp = $invite->getExpires();
        $now = new \DateTime();
        if ($exp < $now)
            throw new \Exception("Invitation expires.");

        if ($invite->getCode() !== $code)
            throw new \Exception('Wrong invitation code.');

The verify method does the verification and throws an exception when something is wrong. In the preregisterAction method, we will catch that exception and add a flash message in the error page to alert the user. However, if the verification goes well, the registration process will go to the next step by displaying a registration form.


This brings us to the end of Part 1 of Symfony 2 Security Management, which covers the app setup (database and security.yml) and the pre-registration stage wherein a user verifies with the app that they are indeed invited.

In our Part 2, we will look into the next two steps: registration and login. We will also develop our post-login handler to do some application-specific actions after a user successfully logs in.

Stay tuned!


No Reader comments