Buy
Buy

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Hey! You've made it through almost this entire tutorial! Nice work! I have just a few more tricks to show you before we're done - and they're good ones!

Creating the Registration Form

First, I want to create a registration form. Find your code and open SecurityController. In addition to login and logout, add a new public function register():

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 38
public function register()
{
... line 41
}
}

Give it a route - /register and a name: app_register:

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 35
/**
* @Route("/register", name="app_register")
*/
public function register()
{
... line 41
}
}

Here's the interesting thing about registration. It has nothing to do with security! Think about it. What is registration? It's just a form that creates a new record in the User table. That's it! That's just database stuff.

So then... why are we even talking about this in a security tutorial? Well... to create the best user experience, there will be just a little bit of security right at the end. Because, after registration, I want to instantly authenticate the new user.

More on that later. Right now, render a template: $this->render('security/register.html.twig'):

... lines 1 - 8
class SecurityController extends AbstractController
{
... lines 11 - 35
/**
* @Route("/register", name="app_register")
*/
public function register()
{
return $this->render('security/register.html.twig');
}
}

Then... I'll cheat: in security/, copy the login.html.twig template, paste and call it register.html.twig:

{% extends 'base.html.twig' %}
{% block title %}Login!{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
{% endblock %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
<form class="form-signin" method="post">
{% if error %}
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
<input type="hidden" name="_csrf_token"
value="{{ csrf_token('authenticate') }}"
>
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">
Sign in
</button>
</form>
</div>
</div>
</div>
{% endblock %}

Let's see: change the title, delete the authentication error stuff and I am going to add a little comment here that says that we should replace this with a Symfony form later:

... lines 1 - 2
{% block title %}Register!{% endblock %}
... lines 4 - 10
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
{# todo - replace with a Symfony form! #}
<form class="form-signin" method="post">
... lines 17 - 30
</form>
</div>
</div>
</div>
{% endblock %}

We haven't talked about the form system yet, so I don't want to use it here. But, normally, I would use the form system because it handles validation and automatically adds CSRF protection.

But, to show off how to manually authenticate a user after registration, this HTML form will work beautifully. Change the h1, remove the value= on the email field so that it always starts blank and take out the CSRF token:

... lines 1 - 2
{% block title %}Register!{% endblock %}
... lines 4 - 10
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
{# todo - replace with a Symfony form! #}
<form class="form-signin" method="post">
<h1 class="h3 mb-3 font-weight-normal">Register</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
... lines 22 - 30
</form>
</div>
</div>
</div>
{% endblock %}

We do need CSRF protection on this form... but I'll skip it for now, because we'll refactor this into a Symfony form in a future tutorial.

And finally, hijack the "remember me" checkbox and turn it into a terms box. We'll say:

Agree to terms I for sure read

... lines 1 - 2
{% block title %}Register!{% endblock %}
... lines 4 - 10
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
{# todo - replace with a Symfony form! #}
<form class="form-signin" method="post">
<h1 class="h3 mb-3 font-weight-normal">Register</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me" required> Agree to terms I for sure read
</label>
</div>
... lines 28 - 30
</form>
</div>
</div>
</div>
{% endblock %}

Oh, and update the button: Register:

... lines 1 - 2
{% block title %}Register!{% endblock %}
... lines 4 - 10
{% block body %}
<div class="container">
<div class="row">
<div class="col-sm-12">
{# todo - replace with a Symfony form! #}
<form class="form-signin" method="post">
<h1 class="h3 mb-3 font-weight-normal">Register</h1>
<label for="inputEmail" class="sr-only">Email address</label>
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="_remember_me" required> Agree to terms I for sure read
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">
Register
</button>
</form>
</div>
</div>
</div>
{% endblock %}

Let's see how it looks! Move over, go to /register and... got it! Logout, then move back over and open up base.html.twig. Scroll down just a little bit to find the "Login" link. Let's create a second link that points to the new app_register route. Say, "Register":

<!doctype html>
<html lang="en">
... lines 3 - 15
<body>
... lines 17 - 22
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5">
... lines 24 - 27
<div class="collapse navbar-collapse" id="navbarNavDropdown">
... lines 29 - 40
<ul class="navbar-nav ml-auto">
{% if is_granted('ROLE_USER') %}
... lines 43 - 58
<li class="nav-item">
<a style="color: #fff;" class="nav-link" href="{{ path('app_register') }}">Register</a>
</li>
{% endif %}
</ul>
</div>
</nav>
... lines 66 - 83
</body>
</html>

Move back and check it out. Not bad!

Handing the Registration Submit

Just like with the login form, because there is no action= on the form, this will submit right back to the same URL. But, unlike login, because this is just a normal page, we are going to handle that submit logic right inside of the controller.

First, get the Request object by adding an argument with the Request type hint: the one from HttpFoundation. Below, I'm going to add another reminder to use the Symfony form & validation system later:

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
... lines 45 - 55
}
}

Then, to only process the data when the form is being submitted, add if ($request->isMethod('POST')):

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 52
}
... lines 54 - 55
}
}

Inside... our job is simple! Registration is nothing more than a mechanism to create a new User object. So $user = new User(). Then set some data on it: $user->setEmail($request->request->get('email')):

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
$user = new User();
$user->setEmail($request->request->get('email'));
... lines 48 - 52
}
... lines 54 - 55
}
}

Remember $request->request is the way that you get $_POST data. And, the names of the fields on our form are name="email" and name="password". But before we handle the password, add $user->setFirstName(). This field is required in the database... but, we don't actually have that field on the form. Just use Mystery for now:

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
$user = new User();
$user->setEmail($request->request->get('email'));
$user->setFirstName('Mystery');
... lines 49 - 52
}
... lines 54 - 55
}
}

In a real app, I would either add this field to the registration form, or make it nullable in the database, so it's optional.

Finally, let's set the password. But... of course! We are never ever, ever, ever going to save the plain password. We need to encode it. We already did this inside of UserFixture:

... lines 1 - 7
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
class UserFixture extends BaseFixture
{
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
protected function loadData(ObjectManager $manager)
{
$this->createMany(10, 'main_users', function($i) use ($manager) {
... lines 22 - 29
$user->setPassword($this->passwordEncoder->encodePassword(
$user,
'engage'
));
... lines 34 - 40
});
$this->createMany(3, 'admin_users', function($i) {
... lines 44 - 48
$user->setPassword($this->passwordEncoder->encodePassword(
$user,
'engage'
));
... lines 53 - 54
});
... lines 56 - 57
}
}

Ah yes, the key was the UserPasswordEncoderInterface service. In our controller, add another argument: UserPasswordEncoderInterface $passwordEncoder:

... lines 1 - 8
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
... lines 10 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
... lines 44 - 55
}
}

Below, we can say $passwordEncoder->encodePassword(). This needs the User object and the plain password that was just submitted: $request->request->get('password'):

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 48
$user->setPassword($passwordEncoder->encodePassword(
$user,
$request->request->get('password')
));
}
... lines 54 - 55
}
}

We are ready to save! Get the entity manager with $em = $this->getDoctrine()->getManager(). Then, $em->persist($user) and $em->flush():

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 48
$user->setPassword($passwordEncoder->encodePassword(
$user,
$request->request->get('password')
));
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
... lines 57 - 58
}
... lines 60 - 61
}
}

All delightfully boring code. This looks a lot like what we're doing in our fixtures.

Finally, after any successful form submit, we always redirect. Use return $this->redirectToRoute(). This is the shortcut method that we were looking at earlier. Redirect to the account page: app_account:

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 48
$user->setPassword($passwordEncoder->encodePassword(
$user,
$request->request->get('password')
));
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->redirectToRoute('app_account');
}
... lines 60 - 61
}
}

Awesome! Let's give this thing a spin! I'll register as [email protected], password engage. Agree to the terms that I for sure read and... Register! Bah! That smells like a Ryan mistake! Yep! Use $this->getDoctrine()->getManager():

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 53
$em = $this->getDoctrine()->getManager();
... lines 55 - 58
}
... lines 60 - 61
}
}

That's what I meant to do.

Move over and try this again: [email protected], password engage, agree to the terms that I read and... Register!

Authentication after Registration

Um... what? We're on the login form? What happened? First, according to the web debug toolbar, we are still anonymous. That makes sense: we registered, but we did not login. After registration, we were redirected to /account...

... lines 1 - 11
class SecurityController extends AbstractController
{
... lines 14 - 41
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 46 - 57
return $this->redirectToRoute('app_account');
}
... lines 60 - 61
}
}

But because we are not logged in, that sent us here.

This is not the flow that I want my users to experience. Nope, as soon as the user registers, I want to log them in automatically.

Oh, and there's also another problem. Open LoginFormAuthenticator and find onAuthenticationSuccess():

... lines 1 - 19
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 22 - 74
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->router->generate('app_homepage'));
}
... lines 83 - 87
}

We added some extra code here to make sure that if the user went to, for example, /admin/comment as an anonymous user, then, after they log in, they would be sent back to /admin/comment.

And... hey! I want that same behavior for my registration form! Imagine that you're building a store. As an anonymous user, I add some things to my cart and finally go to /checkout. But because /checkout requires me to be logged in, I'm sent to the login form. And because I don't have an account yet, I instead click to register and fill out that form. After submitting, where should I be taken to? That's easy! I should definitely be taken back to /checkout so I can continue what I was doing!

These two problems - the fact that we want to automatically authenticate the user after registration and redirect them intelligently - can be solved at the same time! After we save the User to the database, we're basically going to tell Symfony to use our LoginFormAuthenticator class to authenticate the user and redirect by using its onAuthenticationSuccess() method.

Check it out: add two arguments to our controller. First, a service called GuardAuthenticationHandler $guardHandler. Second, the authenticator that you want to authenticate through: LoginFormAuthenticator $formAuthenticator:

... lines 1 - 5
use App\Security\LoginFormAuthenticator;
... lines 7 - 10
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
... lines 12 - 13
class SecurityController extends AbstractController
{
... lines 16 - 43
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
... lines 46 - 68
}
}

Once we have those two things, instead of redirecting to a normal route use return $guardHandler->authenticateUserAndHandleSuccess():

... lines 1 - 13
class SecurityController extends AbstractController
{
... lines 16 - 43
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 48 - 59
return $guardHandler->authenticateUserAndHandleSuccess(
... lines 61 - 64
);
}
... lines 67 - 68
}
}

This needs a few arguments: the $user that's being logged in, the $request object, the authenticator - $formAuthenticator and the "provider key". That's just the name of your firewall: main:

... lines 1 - 13
class SecurityController extends AbstractController
{
... lines 16 - 43
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator)
{
// TODO - use Symfony forms & validation
if ($request->isMethod('POST')) {
... lines 48 - 59
return $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$formAuthenticator,
'main'
);
}
... lines 67 - 68
}
}

Cool! Let's try it! Click back to register. This time, make sure that you register as a different user, password engage, agree to the terms, submit and... nice! We're authenticated and sent to the correct place.

Next - we're going to start talking about a very important and very fun feature called "voters". Voters are the way to make more complex access decisions, like, determining that a User can edit this Article because they are its author, but not an Article created by someone else.

Leave a comment!

  • 2019-03-04 weaverryan

    Hey Ian!

    Great question! It's 100% up to you. Honestly, the only reason it's *typically* done in one action is because I subjectively made the decision many years ago to document the form component using this strategy on Symfony.com. And now... that's what you see everywhere ;). The downside of just one action is it's a bit harder to understand the flow. The benefit of one action is that two actions will require a little bit more duplication - a little bit more coding. But, it will work either way - there is no problem with separating the GET and POST into separate actions, and I get this question pretty regularly. Feel free to do what feels best for you :).

    Cheers!

  • 2019-03-02 Ian

    I guess this question is more about routing and forms, but I see this pattern a lot in tutorials and in the Symfony docs where we just have a form post to the same route that it's viewed from, and we have a single action that conditionally handles both GET and POST requests. That feels weird to me. Wouldn't it make more sense to have two separate actions, one tied to GET requests and the other to POST requests? And in this particular case, wouldn't it be more appropriate for the registration form to POST to something like /users? I know it's sort of a side-issue for this authentication lesson, but it always trips me up a bit. Maybe the convention has to do with how the Symfony form component works with the handleRequest and isSubmitted methods.

  • 2019-02-28 Алексей Суворов

    yep! It works for me, thanks!

  • 2019-02-28 weaverryan

    Hey Алексей Суворов !

    Ah, that's an edge case I didn't think of when designing Guard! So, you're right - when you use this manual authentication method, it does not go through the remember me process. So, you will need to do it manually - let me see if I can help :). It's actually a bit trickier than it should be :/.

    1) In your controller, add two new arguments RememberMeServicesInterface $rememberMeServices and TokenStorageInterface $tokenStorage

    2) For authentication, do this:


    $response = $guardHandler->authenticationUserAndHandleSuccess(...);
    $rememberMeServices->loginSuccess($request, $response, $tokenStorage->getToken());

    return $response;

    3) To make the $rememberMeServices argument work, go into config/services.yaml. Add a key under services._defaults.bind:


    services:
    _defaults:
    # ...

    bind:
    $rememberMeServices: '@security.authentication.rememberme.services.simplehash.main'

    Where the "main" part at the end should match your firewall name in config/packages/security.yaml (it's called "main" by default).

    Let me know if that works! We should really make that easier - it's just not something that occurred to me before!

    Cheers!

  • 2019-02-28 Алексей Суворов

    i set `always_remember_me: true` in security.yaml and use `$guardHandler->authenticateUserAndHandleSuccess`, but after this rememberme cookie wasn't created

  • 2019-02-25 Diego Aguiar

    You only have to activate it in your security configuration. Give it a look to the docs: https://symfony.com/doc/cur...

    Cheers!

  • 2019-02-25 Алексей Суворов

    Hey! How can I add remember me to authentication after registration?

  • 2018-12-31 weaverryan

    Hey commenter!

    It depends :). The short answer, if you're following the code of this tutorial, is no - you do NOT need this. In our set up, every page is available anonymously... and THEN we start adding security to secure individual pages.

    But, there is one important thing to check in your app. In OUR app, at this point, the access_control in security.yaml is empty. This means that, when each page loads, there are NO access_control entries to deny access. That means that every page is accessible anonymously (well, at least until you hit the security checks in the controllers themselves). However, in your app, if you DO have some access_control, then they may be causing access to be denied on your registration page. In that case, yes, you may need to add this entry to "whitelist" this one page. We actually talk about this in good detail in a super old tutorial (though the access_control behavior has NOT changed): https://symfonycasts.com/sc...

    Let me know if that make sense... or if I just confused ;)

    Cheers!

  • 2018-12-29 commenter

    Going to register route won't work when you are
    logged out, if the route is not available for anonymous user. Am I right
    ?
    I mean we need to add :
    - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    in access_control

  • 2018-11-22 Victor Bocharsky

    Hey Sasa,

    Good question! So, you can do it in onAuthenticationSuccess() - there you have access to $request, so you can check if user is on registration page and if so - redirect him to a different route :)

    Cheers!

  • 2018-11-21 Sasa Milivojevic

    Hi! What if I want to redirect user after registration on different route from user who is logging in?

  • 2018-10-29 weaverryan

    Yo Matt Johnson!

    Ha! Great question and... I have no answer :D. I keep going "back and forth" between what I like better: $this->getDoctrine()->getManager() or EntityManagerInterface $e. There's no technical reason for it.

    But, you're right that, on a GET request, type-hinting it would technically be wasteful. I don't usually worry about these things - it's kind of a micro-optinmization.. and we probably are using the entity manager somewhere else anyways. However, it's a very good detail to notice :).

    Cheers!

  • 2018-10-27 Matt Johnson

    Why not just include EntityManagerInterface $em in the params? The only reason I can think is that we don't necessarily need it (if POST isn't submitted).