Symfony 2: Working with the Security Layer and Database Backends


The examples over on the Symfony 2 site regarding building a security system, while decent, are somewhat incomplete and a little confusing, especially in putting all the details together for a database backed login system. While most of the code present on the two articles work for the most part, I wanted to add a few completed bits from my own system that I’m building to help out other people who might miss a few critical clues.

The holy grail for complete registration/login security systems tend to be encrypted passwords stored in a database type of backend. But let’s say for the sake of argument that you created a backend administrative system that allows a system administrator to create/and or modify the information for a user. While the UI for the registration system and CRUD-ish aspects for this type of system may differ, the code actually is quite similar.

The issue I discovered in coming up with my own form was that I had issues when it came to the how to implement the encoding algorithm. Although I managed to originally save the password correctly, when I went to login using the form and security configuration in the examples, I ended up receiving a bad credentials error. The issue I discovered was not in the login controller nor twig files, but in the security.yml configuration file and how I was saving things to the database.

So let’s start off by providing my security.yml file:

security:
    encoders:
        Kwpro\VguildBundle\Entity\User: md5

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        main:
            entity:
                class: Kwpro\VguildBundle\Entity\User
                property: username

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        login_firewall:
            pattern: ^/admin/login$
            anonymous: ~
            security: false
        secured_area:
            pattern:    ^/
            anonymous: ~
            form_login:
                login_path:  kwpro_vguild_admin_login
                check_path:  login_check
            logout:
                path: /logout
                target: /admin/login

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }

One of the first things I want to point out is where I define the encoder. This part essentially maps your entity that defines your user object to an encoding algorithm. In this case, I decided to use md5 (the example the site provides is sha512, but I’ll get to that issue in a second).

For my entity User class, the following code shows it in its entirety:

namespace Kwpro\VguildBundle\Entity;

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

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

	/**
	 * User handle
	 * 
	 * @Assert\NotBlank()
	 * @Assert\Length(
	 * 	min = "2",
	 *  max = "150"
	 * )
	 * @ORM\Column(type="string", length=150, unique=true)
	 * 
	 */
	private $username;

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

	/**
	 * 
	 * @Assert\NotBlank()
	 * @Assert\Length(
	 *   min = "4",
	 *   max = "150"
	 * )
	 * @ORM\Column(type="string", length=150)
	 */
	private $email;

	/**
	 * @Assert\NotBlank()
	 * @Assert\Length(
	 *   min = "2",
	 *   max = "150"
	 * )
	 * @ORM\Column(type="string", length=150)
	 */
	private $fullname;

	/**
	 * 
	 * @Assert\NotBlank()
	 * @Assert\Length(
	 *   min = "4",
	 *   max = "40"
	 * )
	 * @ORM\Column(type="string", length=40)
	 */
	private $password;

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

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

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

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

        return $this;
    }

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

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

        return $this;
    }

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

    /**
     * Set fullname
     *
     * @param string $fullname
     * @return User
     */
    public function setFullname($fullname)
    {
        $this->fullname = $fullname;

        return $this;
    }

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

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

        return $this;
    }

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

    public function getRoles()
    {
		return array('ROLE_ADMIN');
    }

	public function getSalt()
	{
		return $this->salt;
	}

	public function eraseCredentials()
	{
	}

    public function serialize()
    {
        return serialize(array($this->id));
    }

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

    /**
     * Set salt
     *
     * @param string $salt
     * @return User
     */
    public function setSalt($salt)
    {
        $this->salt = $salt;

        return $this;
    }

    /**
     * Set isActive
     *
     * @param boolean $isActive
     * @return User
     */
    public function setIsActive($isActive)
    {
        $this->isActive = $isActive;

        return $this;
    }

    /**
     * Get isActive
     *
     * @return boolean 
     */
    public function getIsActive()
    {
        return $this->isActive;
    }
}

For the most part, this class isn’t special looking. However, the most important thing is in the constructor where we set the salt to md5. I believe my original issue in getting the bad credential error was that the algorithm in my constructor did not match the one I set up in my security.yml file (which was sha512). Because the two didn’t match, the login would constantly fail.

With those two parts setup correctly, I can now show my addAction, which will contain the code that performs the encoding:

	public function addAction(Request $request)
	{
		$user = new User();
		$factory = $this->get('security.encoder_factory');
		$f = $this->createForm(new UserType(), $user);
		$f->handleRequest($request);
		$encoder = $factory->getEncoder($user);
		$password = $encoder->encodePassword($user->getPassword(), $user->getSalt());
		$user->setPassword($password);
		$form = null;
		$success = false;
		if ($f->isValid())
		{			
			$manager = $this->getDoctrine()->getManager();
			$manager->persist($user);
			$manager->flush();
			if ($f->get('save')->isClicked())
			{
				return $this->redirect($this->generateUrl('kwpro_vguild_admin_region_add_success'));
			}
			$form = $this->createForm(new UserType(), new User());;
			$success = true;
		}
		else
		{
			$form = $f;
		}
		return $this->render('KwproVguildBundle:AdminUser:add.html.twig', array('form' => $form->createView(), 'success' => $success));
	}

The important aspect here is where I’m calling setting the encoded password. I do this after I make the call to handleRequest. That’s because the password won’t be set properly until after this stage. If you tried making those calls before handleRequest, your password will end up being set to plain text and that obviously is a bad thing.

If you have an edit password form function, you would do something similar. The process isn’t automatic so you must explicitly do this. Now, when you go to login again, you should be able to proceed to whatever protected section you set as the default.

The problem in the example is that they didn’t really say where you should put your password encryption part in your controller. You had to a little guessing to put it in the right spot. I hope that my example shows a more complete working sample of how to handle this condition. If you had a registration form, it would be similar.

(Visited 581 times, 1 visits today)

Comments

comments