1

I'm pretty new to Symfony in general, I mostly used it because I needed to do something secure very fast, and also to discover Symfony 4.

I'm trying to make a secure connexion with the Security recipe but I'm facing two major problems (probably related) and a small one.

First, I tried to define the salt as nullable but it's still NOT NULL in db. Here's my definition of the column :

/**
 * @ORM\Column(name="salt", type="string", nullable=true)
 */
private $salt;

So now the big problems : Passwords I add are not hashed and trying to connect returns error 500

I tried to follow the documentation and here are : My Entity

use Doctrine\ORM\Mapping as ORM;
use PhpParser\Node\Scalar\String_;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Table(name="app_user")
 * @ORM\Entity(repositoryClass="App\Repository\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=255)
     */
    private $password;

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

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

    /**
     * @ORM\Column(name="salt", type="string", nullable=true)
     */
    private $salt;

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

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

    /**
     * @param mixed $alias
     */
    public function setAlias($alias): void
    {
        $this->alias = $alias;
    }

    public function __construct()
    {
        $this->isActive = true;
        // may not be needed, see section on salt below
//        $this->salt = md5(uniqid('', true));
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function getSalt() :String
    {
        // you *may* need a real salt depending on your encoder
        // see section on salt below
        return $this->salt;
    }

    public function getPassword()
    {
        return $this->password;
    }

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

    public function eraseCredentials()
    {
    }

    /** @see \Serializable::serialize() */
    public function serialize()
    {
        return serialize([
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
//            $this->salt
        ]);
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
//            $this->salt
            ) = unserialize($serialized, ['allowed_classes' => false]);
    }

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

    /**
     * @param mixed $id
     */
    public function setId($id): void
    {
        $this->id = $id;
    }

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

    /**
     * @param mixed $email
     */
    public function setEmail($email): void
    {
        $this->email = $email;
    }

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

    /**
     * @param mixed $isActive
     */
    public function setIsActive($isActive): void
    {
        $this->isActive = $isActive;
    }

    /**
     * @param mixed $username
     */
    public function setUsername($username): void
    {
        $this->username = $username;
    }

    /**
     * @param mixed $password
     */
    public function setPassword($password): void
    {
        $this->password = $password;
    }

    /**
     * @param mixed $salt
     */
    public function setSalt($salt): void
    {
        $this->salt = $salt;
    }
}

My Controllers

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function login(Request $request, AuthenticationUtils $authenticationUtils)
    {
        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();

        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/login.html.twig', array(
            'last_username' => $lastUsername,
            'error'         => $error,
        ));
    }
}

and

use App\Entity\User;
use App\Form\UserType;
use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;


/**
 * @Route("/user")
 */
class UserController extends Controller
{
    /**
     * @Route("/", name="user_index", methods="GET")
     */
    public function index(UserRepository $userRepository): Response
    {
        return $this->render('user/index.html.twig', ['users' => $userRepository->findAll()]);
    }

    /**
     * @Route("/new", name="user_new", methods="GET|POST")
     */
    public function new(Request $request): Response
    {
        $user = new User();
        $form = $this->createForm(UserType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($user);
            $em->flush();

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

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

    /**
     * @Route("/{id}", name="user_show", methods="GET")
     */
    public function show(User $user): Response
    {
        return $this->render('user/show.html.twig', ['user' => $user]);
    }

    /**
     * @Route("/{id}/edit", name="user_edit", methods="GET|POST")
     */
    public function edit(Request $request, User $user): Response
    {
        $form = $this->createForm(UserType::class, $user);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $this->getDoctrine()->getManager()->flush();

            return $this->redirectToRoute('user_edit', ['id' => $user->getId()]);
        }

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

    /**
     * @Route("/{id}", name="user_delete", methods="DELETE")
     */
    public function delete(Request $request, User $user): Response
    {
        if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
            $em = $this->getDoctrine()->getManager();
            $em->remove($user);
            $em->flush();
        }

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

    public function register(User $user, UserPasswordEncoderInterface $encoder)
    {
        $plainPassword = $user->getPassword();
        $encoded = $encoder->encodePassword($user, $plainPassword);
        $user->setPassword($encoded);
    }
}

and my security.yaml

security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        db_provider:
            entity:
                class: App\Entity\User
                property: username

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~
            provider: db_provider
            form_login:
                login_path: login
                check_path: login
            logout:
                path:   /logout
                target: /homepage
            pattern:    ^/admin
            http_basic: ~

    encoders:
        App\Entity\User:
            algorithm: argon2i

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN }

I tried to add this after checking if for isSubmited and isValid in my UserController::new()

$plainPassword = $user->getPassword;
$encoded = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($encoded);

But I had an error Saying that the UserPasswordEncoderInterface $encoder I passed as method argument wasn't injected when loading the form. Still I'm not sure it would be a good solution to make it work as I would have to duplicate that logic in the UserController::edit(), which does not look like Symfony-like code.

(the error :)

"Controller "App\Controller\UserController::new()" requires that you provide a value for the "$encoder" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one."

I also tried to copy/paste (that how desperate I am...) the code in my UserController and then the SecurityController but this didn't work either

public function register(UserPasswordEncoderInterface $encoder)
{
    // whatever *your* User object is
    $user = new App\Entity\User();
    $plainPassword = 'ryanpass';
    $encoded = $encoder->encodePassword($user, $plainPassword);

    $user->setPassword($encoded);
}

I'm getting this as log from the server :

"No encoder has been configured for account "App\Entity\User"."

I also tried to insert directly in my db some values, but trying to connect gave me a "Access Denied" message when entering the right password, which I think is another problem...

I really don't get where I'm wrong and I couldn't find people asking about this. I'd be sincerely grateful if you could help me.

Note : The UserController routes start with /user and is completely public as I need a user to access secured admin panel.

EDIT I'm using MySQL 5.7 and PHP 7.2 if that can be related

6
  • What about FOSUserBundle : github.com/FriendsOfSymfony/FOSUserBundle ? Commented May 4, 2018 at 7:28
  • @MathieuDormeval, thanks for your answer, but it doesn't seem to be such a great solution, for now at least. As I'm reading the Issues on their Github, rely on this repo is not a secure way to code (yet ?) Commented May 4, 2018 at 8:32
  • Does your project require you to store the salt in your database? Commented May 4, 2018 at 9:03
  • @LeonWillens according to the documentation, not at all. But I have to have it to implement the UserInterface Commented May 4, 2018 at 9:10
  • 1
    @Zyigh if you have an answer that differs significantly from the accepted one you must to add another answer (and accept it if it contains the better solution) or edit the existing one. You should never add an answer inside the question, so then remember to delete that part. Commented May 4, 2018 at 12:31

2 Answers 2

3

Since you're using Argon2i as the encoder algorithm for your entity, your $salt becomes obsolete:

Do you need to use a Salt property?

If you use bcrypt or argon2i, no. Otherwise, yes. All passwords must be hashed with a salt, but bcrypt and argon2i do this internally [...] the getSalt() method in User can just return null (it's not used). [...]

-How to Load Security Users from the Database (the Entity Provider)

Try removing the $salt property and the setter method, and let your getSalt() return null. Persist the user without encoding operations and check the persisted password.

While this can be seen as a dirty hack, it seems to be a good practice...

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks a lot. I mark that I resolved as looking for more explanation gave me most of the solution. I'm editing the question to write the complete answer I found. I would never have thought about that as it looks like a "dirty hack" (great description).
1

I finnally found a solution thanks to @LeonWillens. Actually removing the salt property and setters made me discover that the security recipe come without the validator. So I ran composer require doctrine form security validator. I added a plainText field in my Entity which is not a column

/**
 * @Assert\NotBlank()
 * @Assert\Length(max=4096)
 */
private $plainPassword;

With that, I could add this logic in UserController::new()

/**
 * @Route("/new", name="user_new", methods="GET|POST")
 */
public function new(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
    $user = new User();
    $form = $this->createForm(UserType::class, $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $password = $passwordEncoder->encodePassword($user, $user->getPlainPassword());
        $user->setPassword($password);
        $em = $this->getDoctrine()->getManager();
        $em->persist($user);
        $em->flush();

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

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

I change the encoders in my security.yaml

encoders:
    Symfony\Component\Security\Core\User\User: plaintext
    App\Entity\User:
        algorithm: argon2i

And now adding a user work perfectly. I still have problems with connexion, but no such thing as an Exception thrown

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.