Reputation: 425
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
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
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