Skip to content

Instantly share code, notes, and snippets.

@florentdestremau
Last active March 10, 2023 13:17
Show Gist options
  • Star 75 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save florentdestremau/78e3828af1832c309e72b429afa5ca06 to your computer and use it in GitHub Desktop.
Save florentdestremau/78e3828af1832c309e72b429afa5ca06 to your computer and use it in GitHub Desktop.
A simple User authentication setup to copy & paste into your Symfony 3.4 install

This gist is an opiniated version of a simple user registration, login, password resetting workflow, that takes a couple of minutes to setup and just works. This is php7.x compatible only (because you should probably be using it anyway), and if you insiste on having php5.x just remove the annotations in the User.php entity file. Everything is available in the security page in the Symfony documentation: https://symfony.com/doc/3.4/security.html

update on Jan 18th, 2021: This was a nice solution back in 3.4, but since then the Symfony team has been very nice to add the MakerBundle and provider such feature with the make:user command.

{% extends 'base.html.twig' %}
{% block body %}
{{ form(form) }}
{% endblock %}
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class NewPasswordType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('password', RepeatedType::class, ['type' => PasswordType::class])
->add('submit', SubmitType::class);
}
}
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class PasswordRequestType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('send', SubmitType::class);
}
}
{% extends 'base.html.twig' %}
{% block body %}
<p>Register here</p>
{{ form(form) }}
{% endblock %}
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\User;
use AppBundle\Form\RegistrationFormType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class RegistrationController extends Controller
{
/**
* @Route("/register", name="register", methods={"GET", "POST"})
*/
public function register(
Request $request,
UserPasswordEncoderInterface $encoder,
TokenStorageInterface $tokenStorage,
EntityManagerInterface $entityManager,
SessionInterface $session
) {
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$password = $encoder->encodePassword($user, $user->getPlainPassword());
$user->setPassword($password);
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash('success', "Your accound was created");
$token = new UsernamePasswordToken($user, $password, 'main');
$tokenStorage->setToken($token);
$session->set('_security_main', serialize($token));
return $this->redirectToRoute('homepage');
}
return $this->render(
'register.html.twig',
[
'form' => $form->createView(),
]
);
}
}
{% extends 'base.html.twig' %}
{% block body %}
<p>Please enter a new password.</p>
{{ form(form) }}
{% endblock %}
{% extends 'base.html.twig' %}
{% block body %}
<p>We will send a resetting link to your account email.</p>
{{ form(form) }}
{% endblock %}
<?php
namespace AppBundle\Controller;
use AppBundle\Entity\User;
use AppBundle\Form\NewPasswordType;
use AppBundle\Form\PasswordRequestType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class ResettingController extends Controller
{
/**
* @Route("/reset_password", name="reset_password", methods={"GET", "POST"})
*/
public function resetPassword(
Request $request,
EntityManagerInterface $entityManager
) {
$form = $this->createForm(PasswordRequestType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$email = $form->get('email')->getData();
$token = bin2hex(random_bytes(32));
$user = $entityManager->getRepository(User::class)->findOneBy(['email' => $email]);
if ($user instanceof User) {
$user->setPasswordRequestToken($token);
$entityManager->flush();
// send your email with SwiftMailer or anything else here
$this->addFlash('success', "An email has been sent to your address");
return $this->redirectToRoute('reset_password');
}
}
return $this->render('reset-password.html.twig', ['form' => $form->createView()]);
}
/**
* @Route("/reset_password/confirm/{token}", name="reset_password_confirm", methods={"GET", "POST"})
*/
public function resetPasswordCheck(
Request $request,
string $token,
EntityManagerInterface $entityManager,
UserPasswordEncoderInterface $encoder,
TokenStorageInterface $tokenStorage,
SessionInterface $session
) {
$user = $entityManager->getRepository(User::class)->findOneBy(['passwordRequestToken' => $token]);
if (!$token || !$user instanceof User) {
$this->addFlash('danger', "User not found");
return $this->redirectToRoute('reset_password');
}
$form = $this->createForm(NewPasswordType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$plainPassword = $form->get('password')->getData();
$password = $encoder->encodePassword($user, $plainPassword);
$user->setPassword($password);
$user->setPasswordRequestToken(null);
$entityManager->flush();
$token = new UsernamePasswordToken($user, $password, 'main');
$tokenStorage->setToken($token);
$session->set('_security_main', serialize($token));
$this->addFlash('success', "Your new password has been set");
return $this->redirectToRoute('homepage');
}
return $this->render('reset-password-confirm.html.twig', ['form' => $form->createView()]);
}
}
security:
encoders:
AppBundle\Entity\User:
algorithm: bcrypt
providers:
app_users:
entity: { class: AppBundle\Entity\User, property: email }
role_hierarchy:
ROLE_ADMIN: ROLE_USER
firewalls:
dev:
pattern: ^/(_(profiler|wdt|error)|css|images|js)/
security: false
main:
pattern: ^/
form_login:
provider: app_users
login_path: login
check_path: login
logout:
path: /logout
target: /
anonymous: true
access_control:
- { path: ^/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/reset_password, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, role: ROLE_USER }
<?php
namespace AppBundle\Controller;
use AppBundle\Form\UserLoginType;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class SecurityController extends Controller
{
/**
* @Route("/login", name="login", methods={"GET", "POST"})
*/
public function login(AuthenticationUtils $authenticationUtils)
{
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
$form = $this->createForm(UserLoginType::class);
return $this->render(
'login.html.twig',
[
'form' => $form->createView(),
'last_username' => $lastUsername,
'error' => $error,
]
);
}
}
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
* @ORM\Table(name="app_user")
* @UniqueEntity("email")
*/
class User implements UserInterface
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(type="string", length=180, unique=true)
*/
protected $email;
/**
* @var string
*
* @ORM\Column(name="password", type="string", length=100)
*/
protected $password;
/**
* @var string|null
*/
protected $plainPassword;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $passwordRequestToken;
/**
* @var array $roles
*
* @ORM\Column(type="array")
*/
private $roles = ['ROLE_USER'];
/**
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getEmail(): string
{
return $this->email;
}
/**
* @param string $email
*
* @return User
*/
public function setEmail(string $email): User
{
$this->email = $email;
return $this;
}
/**
* @return string
*/
public function getPassword(): string
{
return $this->password;
}
/**
* @param string $password
*
* @return User
*/
public function setPassword(string $password): User
{
$this->password = $password;
return $this;
}
/**
* @return null|string
*/
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
/**
* @param null|string $plainPassword
*
* @return User
*/
public function setPlainPassword(?string $plainPassword): User
{
$this->plainPassword = $plainPassword;
return $this;
}
/**
* @return null|string
*/
public function getPasswordRequestToken(): ?string
{
return $this->passwordRequestToken;
}
/**
* @param null|string $passwordRequestToken
*
* @return User
*/
public function setPasswordRequestToken(?string $passwordRequestToken): User
{
$this->passwordRequestToken = $passwordRequestToken;
return $this;
}
/**
* @return array
*/
public function getRoles(): array
{
return $this->roles;
}
/**
* @param array $roles
*
* @return User
*/
public function setRoles(array $roles): User
{
$this->roles = $roles;
return $this;
}
/**
* @return string|null The salt
*/
public function getSalt()
{
return null;
}
/**
* @return string The username
*/
public function getUsername(): string
{
return $this->email;
}
/**
* Removes sensitive data from the user.
*/
public function eraseCredentials(): void
{
$this->plainPassword = null;
}
}
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class UserLoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username', TextType::class)
->add('_password', PasswordType::class)
->add('submit', SubmitType::class);
}
public function getBlockPrefix()
{
return null;
}
}
<?php
namespace AppBundle\Form;
use AppBundle\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\RepeatedType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class UserRegistrationType extends AbstractType
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('plainPassword', RepeatedType::class, ['type' => PasswordType::class])
->add('submit', SubmitType::class);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['data_class' => User::class]);
}
public function getBlockPrefix()
{
return null;
}
}
@martinberlin
Copy link

Very nice. I will try this also in Symfony 5!
Since some months I decided not to use FOS User anymore

@florentdestremau
Copy link
Author

@martinberlin Since SF4 this gist is pretty much useless as you have the MakerBundle which handles exactly this purpose. I would rather recommend using the makerBundle version now :)

@Imdzimis
Copy link

Imdzimis commented Feb 5, 2020

@florentdestremau with MakerBundle you can make users, authentication, login form, but password resetting and email confirmation you should make by yourself ? or am i wrong ?

@florentdestremau
Copy link
Author

@Imdzimis yes if I recall correctly it's exactly as you say. MakerBundle makes the "simplest" authentication files. Email confirmation is pretty easy to add as well as password resetting:

Password reset:

  • add "password_reset_token" field to your user
  • new route with a simple Email form
  • new controller action where you find your user, set a random password token, send an email with the token
  • new route for password resetting such as /reset-password/confirm/{token} where you find the user from the token in your database
  • new action where you handle form submission and set again the password. You can also log in your user right away

Done !

Email confirmation:

  • add "email_confirmation_token" field to your user ,set randomly in your constructor (we use bin2hex(random_bytes(32)))
  • add a boolean "confirmed" (or a "status" field, depends on your implementation) to your user
  • send an email on registration (using and Event Listener would be my recommendation) with the token
  • new route /confirm-email/{token} where you find your user in database from the token, set the "confirmed" to true, flush, and redirect to homepage :)

Done !

@martinberlin
Copy link

I tried this anyways and it works with minimal updates on Symfony 5 too. If anyone is interested:
https://github.com/martinberlin/cale-integrations/tree/feature/usermanager -> In this branch

I will try also MakerBundle but is nice to make it yourself so there is more learning in the process

@vittore
Copy link

vittore commented Jun 29, 2020

Thanks for your code.

@crmpicco
Copy link

I tried this anyways and it works with minimal updates on Symfony 5 too. If anyone is interested:
https://github.com/martinberlin/cale-integrations/tree/feature/usermanager -> In this branch

I will try also MakerBundle but is nice to make it yourself so there is more learning in the process

This link appears to be dead :-(

@crmpicco
Copy link

Can anyone confirm if this gist works with Symfony 4.4?

@martinberlin
Copy link

Yes it does. That branch does not exist anymore but is already integrated in the project and it works

@crmpicco
Copy link

Yes it does. That branch does not exist anymore but is already integrated in the project and it works

Great, looking forward to trying this out.

@florentdestremau
Copy link
Author

as stated above, I would recommend to use the MakerBundle now that it makes a more complete and up-to-date version of this.

@martinberlin
Copy link

I completely agree with Florent. Also if you don't want to use the MakerBundle it's also possible to do your own User manager from scratch. There are cookbooks and many pages on this and ultimately if you do it, then you learn a lot in the process.

@phtmgt
Copy link

phtmgt commented Apr 10, 2021

Thanks for the solution. Unfortunately, we are missing the RegistrationFormType.php file. Any chance you can include it as well?

@florentdestremau
Copy link
Author

https://gist.github.com/florentdestremau/78e3828af1832c309e72b429afa5ca06#file-userregistrationtype-php I think the naming isn't right but the file is there.

As said earlier, I'm happy it helps but as of 3.4+ you should probably use the MakerBundle commands which are doing exactly the same 😃

@phtmgt
Copy link

phtmgt commented Apr 12, 2021

Thanks, I went with MakerBundle. The issue with it is that it does not play nice when you have FOSUserBundle already operational (i.e., it insists on naming the entity file User.php). This is why this gist is helpful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment