Automatically Logging In When Registering


Towards the end of the previous video we saw how we must implement both unserialize and serialize to avoid problems when registering and logging in. There's still a couple of methods that we haven't yet looked at that we must implement as part of the UserInterface. These are:

  • getSalt
  • eraseCredentials

We're making use of bcrypt to encode our user's passwords. A salt will be created for us automatically as part of the encoding process. Now, you may be thinking - well, why don't we need to store that salt then?

Just to quickly recap, the salt is some random data that is combined with the original password to ensure that if two users use the exact same password, the resulting password hash would not be identical. This is important as if an attacker compromises one password, they cannot simply check for equality in your database to find other users with that password.

Now, if this salt is random, then surely we need to keep a record of it so that we can re-use this information when a user subsequently logs in?

Yes, we do. But bcrypt keeps this information as part of the hashed password.

Now, I'm far from a security expert, so I would strongly urge you to read this StackOverflow answer which explains it better than I ever could.

Anyway, as we don't need to store a salt, our getSalt method can simply return null. Easy enough.

Symfony's official docs also have a little more detail on this subject.

To finish up with our unimplemented methods, we have eraseCredentials.

This is perhaps the most peculiar of all the methods, as even the official docs lead to this method being skipped over. From the interface docblock, we have:

eraseCredentials()

Removes sensitive data from the user.

This is important if, at any given point, sensitive information like the plain-text password is stored on this object.

We have been careful not to supply any Doctrine annotations for persisting our $plainPassword property to the database. However, there is a possibility that our $plainPassword data may be stored in plain text inside our session. The eraseCredentials method will be called by Symfony during the authentication process to enable us to tidy up after ourselves, ensuring we don't expose any security data by accident.

Here's the specific methods for this part of the final implementation:

    public function getSalt()
    {
        return null;
    }

    public function eraseCredentials()
    {
        $this->plainPassword = null;
    }

And the class in full:

<?php

// /src/AppBundle/Entity/Member.php

namespace AppBundle\Entity;

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

/**
 * Member
 *
 * @ORM\Table(name="member")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MemberRepository")
 */
class Member implements UserInterface, \Serializable
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

    private $plainPassword;

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

    /**
     * Get id
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set username
     *
     * @param string $username
     *
     * @return Member
     */
    public function setUsername($username)
    {
        $this->username = $username;

        return $this;
    }

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

    /**
     * Set email
     *
     * @param string $email
     *
     * @return Member
     */
    public function setEmail($email)
    {
        $this->email = $email;

        return $this;
    }

    /**
     * Get email
     *
     * @return string
     */
    public function getEmail()
    {
        return $this->email;
    }

    /**
     * Set password
     *
     * @param string $password
     *
     * @return Member
     */
    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

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

    /**
     * @return mixed
     */
    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    /**
     * @param mixed $plainPassword
     * @return Member
     */
    public function setPlainPassword($plainPassword)
    {
        $this->plainPassword = $plainPassword;

        return $this;
    }

    public function serialize()
    {
        return serialize([
            $this->id,
            $this->username,
            $this->password,
        ]);
    }

    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password
            ) = unserialize($serialized);
    }

    public function getRoles()
    {
        return [
            'ROLE_USER',
        ];
    }

    public function getSalt()
    {
        return null;
    }

    public function eraseCredentials()
    {
        $this->plainPassword = null;
    }
}

And with that, we should be good to fix another issue that currently exists in our code, which is that when a user Registers, they are not immediately logged in.

Programmatically Logging In

One of the most frustrating things about software development is estimating how long things will take me to complete. I've struggled with this for years, and likely will do for as long as I continue doing what I do.

I have come to realise that I struggle with estimation because I frequently only consider the "happy path".

You know the score - you're in the daily stand up, and (one of the many) PM's asks you how long X will take to complete.

You think about it, and come up with some number of days. Only, what you meant was: 2 days if everything goes to plan, and I don't encountered any unexpected issues.

Sadly, programming is often the chore of fixing those unexpected issues :)

A brilliant example of this problem is in our Registration process.

Can we register? Yes.

Does it behave as expected? Heck no!

We can register, sure. And our credentials do end up in the database. Only, we aren't immediately logged in after registering.

And why should we be?

Remember, this is code. Cold, hard, unwavering logic. We told it to register. We didn't tell it to log us in.

That's always fun to report back in the next day's stand up.

Anyway, fortunately here we aren't having to worry about satisfying project managers.

Now, the code that we are about to cover is somewhat complex. The good news is: you don't need to fully understand this to use it. It's "good enough" to copy and paste. But we are going to go through it, and understand it. As ever, feel free to skip this if that's not your thing.

Here's the code in full, we will look at the specific part next:

<?php

// /src/AppBundle/Controller/RegistrationController.php

namespace AppBundle\Controller;

use AppBundle\Entity\Member;
use AppBundle\Form\Type\MemberType;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

class RegistrationController extends Controller
{
    /**
     * @Route("/register", name="registration")
     * @return \Symfony\Component\HttpFoundation\Response
     * @throws \LogicException
     */
    public function registerAction(Request $request)
    {
        $member = new Member();

        $form = $this->createForm(MemberType::class, $member, [
            'action' => $this->generateUrl('handle_registration_form_submission')
        ]);

        if ($form->isSubmitted() && $form->isValid()) {

            $password = $this
                ->get('security.password_encoder')
                ->encodePassword(
                    $member,
                    $member->getPlainPassword()
                )
            ;

            $member->setPassword($password);

            $em = $this->getDoctrine()->getManager();

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

            $token = new UsernamePasswordToken(
                $member,
                $password,
                'main',
                $member->getRoles()
            );

            $this->get('security.token_storage')->setToken($token);
            $this->get('session')->set('_security_main', serialize($token));

            $this->addFlash('success', 'You are now successfully registered!');

            return $this->redirectToRoute('homepage');
        }

        return $this->render('registration/register.html.twig', [
            'registration_form' => $form->createView(),
        ]);
    }
}

The new part is this:

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;

// * snip *

$token = new UsernamePasswordToken(
    $member,
    $password,
    'main',
    $member->getRoles()
);

$this->get('security.token_storage')->setToken($token);
$this->get('session')->set('_security_main', serialize($token));

In a previous video we saw how when logging in, we would get an instance of the UsernamePasswordToken. We need to emulate this process when logging our user in after registration.

This UsernamePasswordToken takes 4 arguments, the first 3 of which are mandatory.

These are:

  • An object representing our user
  • Some credentials
  • A providerKey - huh?
  • An array of roles

Now, we know that if we have successfully registered a user, we must therefore have a variable representing this user - our $member - so we can immediately use that as our first argument.

Likewise, to successfully sign up, our $member must have provided a password. Well, we've already covered encoding the user's password, so we can simply use that.

So far, so good.

Then we hit the providerKey.

I remember spending a good long while down the wrong path of thinking this was related to our providers config in security.yml. Not so. It actually means the name of the firewall we want to be authenticated against. You can see in this line of code an example of where this providerKey is checked against.

In our case, our firewall is called main, and so we need to use the value of main.

Lastly, we optionally need to provide an array of roles.

Now, we actually have this as part of our $member, as our Member class implements UserInterface, and that means we must implement getRoles.

In our case, getRoles returns an array with one entry: ROLE_USER.

If we don't provide a value here then we implicitly get logged in with the role of ROLE_USER.

However, if we do provide a value here, it's a good shout to have one of the roles in the array be ROLE_USER - because if you don't, you won't get it, and it's pretty much the default role used throughout Symfony. You are, of course, free to omit it and do whatever you need to do, but consider this a heads up.

We've Got It, Let's Save It

Creating and populating this instance of UsernamePasswordToken is all well and good, but we need to make Symfony aware of it, and make sure it sticks around after our current request has been handled.

Fortunately, Symfony provides us with a couple of preconfigured services to do just this.

First, there's the token storage service:

php bin/console debug:container security.token_storage   

Information for Service "security.token_storage"
================================================

 ------------------ --------------------------------------------------------------------------- 
  Option             Value                                                                      
 ------------------ --------------------------------------------------------------------------- 
  Service ID         security.token_storage                                                     
  Class              Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage  
  Tags               -                                                                          
  Public             yes                                                                        
  Synthetic          no                                                                         
  Lazy               no                                                                         
  Shared             yes                                                                        
  Abstract           no                                                                         
  Autowired          no                                                                         
  Autowiring Types   -                                                                          
 ------------------ --------------------------------------------------------------------------- 

You can see the implementation here.

There's not a lot to see here, but this is where Symfony will look for our token instance.

This directly relates to:

$this->get('security.token_storage')->setToken($token);

If we forget this line - or get it wrong somehow - then we will be registered, but not logged in. However, we could then immediately use our credentials and become properly logged in. Not great UX though.

Next, we save the serialized token to the session:

$this->get('session')->set('_security_main', serialize($token));

php bin/console debug:container session               

Information for Service "session"
=================================

 ------------------ -------------------------------------------------- 
  Option             Value                                             
 ------------------ -------------------------------------------------- 
  Service ID         session                                           
  Class              Symfony\Component\HttpFoundation\Session\Session  
  Tags               -                                                 
  Public             yes                                               
  Synthetic          no                                                
  Lazy               no                                                
  Shared             yes                                               
  Abstract           no                                                
  Autowired          no                                                
  Autowiring Types   -                                                 
 ------------------ -------------------------------------------------- 

To the very best of my knowledge, the reason for doing this is to stop session hijacking attempts.

This is perhaps the most complicated part of this whole setup, and fortunately we are now done with it. As I say, in the real world, this is largely just copy / paste, so don't feel you need to memorise to it.

Code For This Course

Get the code for this course.

Episodes