Registration and Password Hashing
Registration and Password Hashing
~16 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Without registration, NOBODY can log in – time for new users to sign themselves up, with securely hashed passwords.
Why passwords are NEVER stored in plain text
Achtung: A plain-text password in the database means: on EVERY database leak (hack, misconfiguration, careless staff access), ALL passwords are IMMEDIATELY compromised – AND, since many users reuse passwords, potentially their accounts on OTHER services too. A hash is a ONE-WAY transformation: the hash can be computed from the password, but NOT the reverse.
Creating the registration controller
php bin/console make:registration-formInteractively asks for the user entity, whether email verification is wanted (for this chapter: no, chapter 38 covers sending emails separately) and generates a controller, form type, AND template.
<?php
declare(strict_types=1);
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', TextType::class, ['label' => 'Name'])
->add('email', EmailType::class, ['label' => 'Email'])
->add('plainPassword', PasswordType::class, [
'label' => 'Password',
'mapped' => false,
'constraints' => [
new Assert\NotBlank(message: 'Please enter a password.'),
new Assert\Length(
min: 8,
minMessage: 'Your password must be at least {{ limit }} characters long.',
),
],
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults(['data_class' => User::class]);
}
}'mapped' => false is DECISIVE: the plainPassword field does NOT get mapped automatically to a User property (the User entity, after all, only has a HASHED password field, chapter 26) – we read the plain-text value manually and hash it ourselves, instead of letting it end up unhashed in $user->password.
The registration controller
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register')]
public function register(
Request $request,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $entityManager,
): Response {
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$plainPassword = $form->get('plainPassword')->getData();
$user->setPassword(
$passwordHasher->hashPassword($user, $plainPassword)
);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', 'Registration successful! You can now log in.');
return $this->redirectToRoute('app_login');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form,
]);
}
}UserPasswordHasherInterface is a service provided by Symfony (autowiring, chapter 8) – hashPassword($user, $plainPassword) AUTOMATICALLY uses the algorithm configured in security.yaml ('auto' from chapter 26 currently picks bcrypt or Argon2id, depending on the PHP environment).
Why $user as hashPassword()'s first argument?
Some hash algorithms (bcrypt among them) factor USER-SPECIFIC data (e.g. a "salt") into the computation – two users with an IDENTICAL password end up with DIFFERENT hashes in the database because of this, which makes targeted "rainbow table" attacks considerably harder.
Rendering the registration form
{% extends 'base.html.twig' %}
{% block body %}
<h1>Register</h1>
{{ form(registrationForm) }}
{% endblock %}Achtung: unique: true on the entity's email field (chapter 26) prevents DUPLICATE registrations at the database level – additionally add a #[Assert\Unique] or UniqueEntity constraint on the form type, so the user sees a READABLE error message instead of a raw database error if the email is already taken.