Zyigh
Zyigh

Reputation: 425

Password doesn't get hashed, and trying to connect return error 500 Symfony

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

Upvotes: 1

Views: 3046

Answers (2)

Zyigh
Zyigh

Reputation: 425

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

Upvotes: 1

Leon Willens
Leon Willens

Reputation: 356

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...

Upvotes: 3

Related Questions