Symfony2 Pre-registration and Invite System

Share this article

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.

symfony-logo

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` ( 
	`id` INT( 255 ) AUTO_INCREMENT NOT NULL, 
	`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):

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

        default:
            anonymous: ~
            http_basic: ~
            form_login:
                login_path: /login
                check_path: /login_check    
            logout:    
                path: /logout
                target: /login
    
    access_control:
        - { 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:

form_login:
	login_path: /login
    check_path: /login_check

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

logout:    
	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:

logout:
    path: /logout
    
login:
    path: /login
    defaults: { _controller: AppBundle:Security:login}    
        
login_check:
    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.

access_control

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:

<?php

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()
    {
        return;
    }

    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:

INSERT INTO `user` 
(`username`, `password`, `email`, `roles`, `active`) 
VALUES ('root', '$2a$12$3INF/uK0.pmZAMFW0In0uOrfpq.yaVIK0xwmWO8Yjxhs4m8CC1Ei2', 'root@somewhere.com', '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();
        $invite->setInvited($email);
        $invite->setWhoinvite($user);

        $now = new \DateTime();
        $int = new \DateInterval('P1D');
        $now->add($int);
        $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. 

        $em->persist($invite);
        $em->flush();

        return $random;
    }

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

        $message = $mailer->createMessage()
                ->setSubject('Someone invites you to join thisdomain.com')
                ->setFrom('root@thisdomain.com')
                ->setTo($email)
                ->setBody(
                $this->renderView('AppBundle:Default:email.html.twig', ['email' => $email, 'hash' => $hash]), 'text/html'
        );

        $mailer->send($message);
    }

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');

        try
        {
            $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.

Conclusion

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!

Frequently Asked Questions about Symfony2 Pre-Registration Invite System

What is the Symfony2 Pre-Registration Invite System?

The Symfony2 Pre-Registration Invite System is a feature that allows website administrators to send out invitations to potential users before they register on the site. This system is built using Symfony2, a popular PHP framework. The invite system is a great way to control the number of users on your site and to create an exclusive community. It also allows you to track who has accepted your invitation and who hasn’t, providing valuable insights into user behavior.

How does the Symfony2 Pre-Registration Invite System work?

The Symfony2 Pre-Registration Invite System works by generating unique invitation codes for each potential user. These codes are sent out via email and can be used to access the registration page of the website. Once the code is used, it becomes invalid, preventing it from being used again. This ensures that only those who have been invited can register on the site.

How can I implement the Symfony2 Pre-Registration Invite System on my website?

Implementing the Symfony2 Pre-Registration Invite System on your website requires knowledge of PHP and the Symfony2 framework. You will need to create a new Symfony2 project, set up the database, and create the necessary entities and forms. You will also need to set up the email system to send out the invitations. The detailed steps can be found in the article.

Can I customize the Symfony2 Pre-Registration Invite System?

Yes, the Symfony2 Pre-Registration Invite System is highly customizable. You can modify the invitation email template, the registration form, and the behavior of the system to suit your needs. You can also add additional features such as tracking the number of invitations sent and the number of registrations.

What are the benefits of using the Symfony2 Pre-Registration Invite System?

The Symfony2 Pre-Registration Invite System offers several benefits. It allows you to control the number of users on your site, create an exclusive community, and track user behavior. It also provides a level of security, as only those with a valid invitation code can register.

Is the Symfony2 Pre-Registration Invite System secure?

Yes, the Symfony2 Pre-Registration Invite System is secure. The invitation codes are unique and can only be used once, preventing unauthorized access to the registration page. Additionally, Symfony2 is a robust and secure framework that follows best practices for web development.

Can I use the Symfony2 Pre-Registration Invite System for other purposes?

Yes, while the Symfony2 Pre-Registration Invite System is primarily used for user registration, it can be adapted for other purposes. For example, you could use it to invite users to a private event or to access exclusive content on your site.

What if a user loses their invitation code?

If a user loses their invitation code, you can generate a new one for them. This can be done through the admin panel of the Symfony2 Pre-Registration Invite System.

Can I track the status of the invitations sent?

Yes, the Symfony2 Pre-Registration Invite System allows you to track the status of the invitations sent. You can see who has accepted the invitation and who hasn’t, providing valuable insights into user behavior.

What are the prerequisites for implementing the Symfony2 Pre-Registration Invite System?

To implement the Symfony2 Pre-Registration Invite System, you need to have a working knowledge of PHP and the Symfony2 framework. You also need to have a server where you can host your Symfony2 project.

Taylor RenTaylor Ren
View Author

Taylor is a freelance web and desktop application developer living in Suzhou in Eastern China. Started from Borland development tools series (C++Builder, Delphi), published a book on InterBase, certified as Borland Expert in 2003, he shifted to web development with typical LAMP configuration. Later he started working with jQuery, Symfony, Bootstrap, Dart, etc.

authenticationauthorizationBrunoSloginOOPHPPHPsecuritysymfony2
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week