Buy

Time to build a login form. And guess what? This page is no different than every other page: we'll create a route, a controller and render a template.

For organization, create a new class called SecurityController. Extend the normal Symfony base Controller and add a public function loginAction():

17 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 2
namespace AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
... lines 6 - 7
class SecurityController extends Controller
{
... lines 10 - 12
public function loginAction()
{
}
}

Setup the URL to be /login and call the route security_login:

17 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 5
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class SecurityController extends Controller
{
/**
* @Route("/login", name="security_login")
*/
public function loginAction()
{
}
}

Make sure to auto-complete the @Route annotation so you get the use statement up top.

Cool!

Every login form looks about the same, so let's go steal some code. Google for "Symfony security form login" and Find a page called How to Build a Traditional Login Form.

Adding the Login Controller

Find their loginAction(), copy its code and paste it into ours:

33 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 7
class SecurityController extends Controller
{
... lines 10 - 12
public function loginAction()
{
$authenticationUtils = $this->get('security.authentication_utils');
// 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 entered by the user
'last_username' => $lastUsername,
'error' => $error,
)
);
}
}

Notice, one thing is immediately weird: there's no form processing code inside of here. Welcome to the strangest part of Symfony's security. We will build the login form here, but some other magic layer will actually handle the form submit. We'll build that layer next.

But thanks to this handy security.authentication_utils service, we can at least grab any authentication error that may have just happened in that magic layer as well as the last username that was typed in, which will actually be an email address for us.

The Login Controller

To create the template, hit Option+enter on a Mac and select the option to create the template. Or you can go create this by hand.

You guys know what to do: add {% extends 'base.html.twig' %}. Then, override {% block body %} and add {% endblock %}. I'll setup some markup to get us started:

32 lines app/Resources/views/security/login.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
... lines 8 - 27
</div>
</div>
</div>
{% endblock %}

Great! This template also has a bunch of boilerplate code, so copy that from the docs too. Paste it here. Update the form action route to security_login:

32 lines app/Resources/views/security/login.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
<form action="{{ path('security_login') }}" method="post">
<label for="username">Username:</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
{#
If you want to control the URL the user
is redirected to on success (more details below)
<input type="hidden" name="_target_path" value="/account" />
#}
<button type="submit">login</button>
</form>
</div>
</div>
</div>
{% endblock %}

Well, it ain't fancy, but let's try it out: go to /login. There it is, in all its ugly glory.

What, No Form Class?

Now, I bet you've noticed something else weird: we are not using the form system: we're building the HTML form by hand. And this is totally ok. Security is strange because we will not handle the form submit in the normal way. Because of that, most people simply build the form by hand: you can do it either way.

But... our form is ugly. And I know from our forms course, that the form system is already setup to render using Bootstrap-friendly markup. So if we did use a real form... this would instantly be less ugly.

Ok, Ok: Let's add a Form Class

So let's do that: in the Form directory, create a new form class called LoginForm. Remove getName() - that's not needed in Symfony 3 - and configureOptions():

20 lines src/AppBundle/Form/LoginForm.php
... lines 1 - 2
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
... line 6
use Symfony\Component\Form\FormBuilderInterface;
... lines 8 - 9
class LoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 14 - 17
}
}

This is a rare time when I won't bother binding my form to a class.

Tip

If you're building a login form that will be used with Symfony's native form_login system, override getBlockPrefix() and make it return an empty string. This will put the POST data in the proper place so the form_login system can find it.

In buildForm(), let's add two things, _username and _password, which should be a PasswordType:

20 lines src/AppBundle/Form/LoginForm.php
... lines 1 - 5
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
... lines 7 - 9
class LoginForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username')
->add('_password', PasswordType::class)
;
}
}

You can name these fields anything, but _username and _password are common in the Symfony world. Again, we're calling this _username, but for us, it's an email.

Next, open SecurityController and add $form = $this->createForm(LoginForm::class):

36 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 6
use AppBundle\Form\LoginForm;
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
... lines 16 - 23
$form = $this->createForm(LoginForm::class, [
... line 25
]);
... lines 27 - 34
}
}

And, if the user just failed login, we need to pre-populate their _username field. To pass the form default data, add a second argument: an array with _username set to $lastUsername:

36 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 23
$form = $this->createForm(LoginForm::class, [
'_username' => $lastUsername,
]);
... lines 27 - 36

Finally, skip the form processing: that will live somewhere else. Pass the form into the template, replacing $lastUsername with 'form' => $form->createView():

36 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 8
class SecurityController extends Controller
{
... lines 11 - 13
public function loginAction()
{
... lines 16 - 27
return $this->render(
'security/login.html.twig',
array(
'form' => $form->createView(),
'error' => $error,
)
);
}
}

Rendering the Form in the Template

Open up the template, Before we get to rendering, make sure our eventual error message looks nice. Add alert alert-danger:

24 lines app/Resources/views/security/login.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
... lines 14 - 19
</div>
</div>
</div>
{% endblock %}

Now, kill the entire form and replace it with our normal form stuff: form_start(form), from_end(form), form_row(form._username) and form_row(form._password):

24 lines app/Resources/views/security/login.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Login!</h1>
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
{{ form_start(form) }}
{{ form_row(form._username) }}
{{ form_row(form._password) }}
... line 18
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}

Don't forget your button! type="submit", add a few classes, say Login and get fancy with a lock icon:

24 lines app/Resources/views/security/login.html.twig
... lines 1 - 2
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
... lines 7 - 14
{{ form_start(form) }}
... lines 16 - 17
<button type="submit" class="btn btn-success">Login <span class="fa fa-lock"></span></button>
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}

We did this purely so that Ryan could get his form looking less ugly. Let's see if it worked. So much better!

Oh, while we're here, let's hook up the Login button on the upper right. This lives in base.html.twig. The login form is just a normal route, so add path('security_login'):

49 lines app/Resources/views/base.html.twig
<!DOCTYPE html>
<html>
... lines 3 - 13
<body>
... lines 15 - 19
<header class="header">
... lines 21 - 22
<ul class="navi">
... line 24
<li><a href="{{ path('security_login') }}">Login</a></li>
</ul>
</header>
... lines 28 - 46
</body>
</html>

Refresh, click that link, and here we are.

Login form complete. It's finally time for the meat of authentication: it's time to build an authenticator.

Leave a comment!

  • 2017-08-14 Diego Aguiar

    Hey Mike P

    Yeah, feel free to remove it from your Form class, it is used to name the Form element and the default value is really good.

    From docs: The block prefix defaults to the underscored short class name with the "Type" suffix removed (e.g. "UserProfileType" => "user_profile").

    Cheers!

  • 2017-08-14 Mike P

    SF 3.3.6 doesn't have getName() after form creation but it does have getBlockPrefix(). Should this function be removed as well?

  • 2017-08-01 deskema

    welcome ;)

  • 2017-07-31 weaverryan

    +1 this code (as we do it in the tutorial) is safe in all versions of Symfony :). There is a different way to do it also in Symfony 3.3 - but if you're not sure, just use this way - it's totally great!

    Thanks for the comment!

  • 2017-07-31 deskema

    I had the same issue.

    public function loginAction() {

    // instead of using dependency injection fill the variable straight with get() method.
    $authUtils = $this->get('security.authentication_utils');

    // get the login error if there is one
    $error = ........

  • 2017-07-05 Victor Bocharsky

    Hey maxii123 ,

    Yea, we have one, check it out here: https://github.com/knpunive... . But as Diego said, you can create your owns very easy. Check out our screencast about PhpStorm to get more info about how to do it: https://knpuniversity.com/s...

    Cheers!

  • 2017-07-05 maxii123

    Oh, I thought KNP had a library of them somewhere. I shall google more. Thank you.

  • 2017-07-05 Victor Bocharsky

    Hey Max,

    Yes, this auto-injection feature by typehint in actions allows do not use container directly, i.e. fetch services from the container, and as a consequence, you can make more your services private, what is cool.

    Cheers!

  • 2017-07-03 Diego Aguiar

    Hey maxii123

    I believe you are talking about "live templates", if you are using PHPStorm, you can create your own templates by going to: settings > editor > live templates

    You can read more info here: https://www.jetbrains.com/h...

    Have a nice day

  • 2017-07-03 maxii123

    No. As you typed the bootstrap code it auto completed. Some sort of downloadable template for Idea editor?

  • 2017-07-03 maxii123

    Its not the request part :

    public function loginAction(Request $request, AuthenticationUtils $authUtils)

    Its the authenticationUtils parameter. That doesnt work. Interestingly the symfony docs go from:

    public function loginAction(Request $request)

    to

    public function loginAction(Request $request, AuthenticationUtils $authUtils)

    without any explanation. Is this some sort for dependency injection perhaps or "auto something or other" that saves us having to query the container?

  • 2017-06-19 weaverryan

    Woohoo! Cheers! It's a tricky transition (but ultimately optional) to the new fancy stuff ;)

  • 2017-06-19 Edouard PERRIN

    Nice catch weaverryan!! That was the right move, it works smoothly now. Guess I better take a few moments to watch the 3.3 tutorial :)
    Thanks for your help!

  • 2017-06-19 weaverryan

    Yo Edouard PERRIN!

    Ah, ok, I've got it! In order for the "service controller argument" thing to work (http://knpuniversity.com/sc..., your controller must be registered as a service. Basically, when you upgrade from Symfony 3.2 to 3.3, *if* you want to use all of the new features, you need some config changes in services.yml. We talk about this in the 3.3 tutorial: http://knpuniversity.com/sc.... Most notably, you need this bit of configuration: http://knpuniversity.com/sc.... In fact, I think you could add just those lines (I'm talking about the first code block on that page - lines 23-28) and that would do it :).

    Cheers!

  • 2017-06-15 Edouard PERRIN

    Hey weaverryan,

    Ok, so I thought it was working but trying the new code again, it still doesn't work.
    Everything works with the old way btw.

    I have the use statement; I upgraded mid-project from Symfony 3.something to 3.3.2

    Services.yml
    https://pastebin.com/RsTi71bZ

    Security.yml
    https://pastebin.com/ztPu8ZPR

    SecurityController
    https://pastebin.com/340jchpb

    LoginForm
    https://pastebin.com/3tBc5Wy0

    LoginFormAuthenticator
    https://pastebin.com/nSMfwJR6

  • 2017-06-15 weaverryan

    Hey Edouard PERRIN!

    Hmm, let's see if we can find your problem... and maybe also improve the docs at the same time :).

    Do you have the use statement for the `AuthenticationUtils` class? It would be:

    use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

    If you *do* have that, can you post your loginAction exactly? Also, is this a new project or did you upgrade from an older version of Symfony? What does your services.yml file look like?

    Cheers!

  • 2017-06-14 Edouard PERRIN

    Hey Ryan,
    Actually I got an error with the updated code from Symfony doc; I'm running 3.3.2 so I thought I'd try it :)

    Controller "AdminBundle\Controller\SecurityController::loginAction()" requires that you provide a value for the "$authUtils" 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.
    There isn't much on the official doc to help me figure it out; I know I'm supposed to pass the $authUtils argument but what is it exactly? :p

  • 2017-06-08 Victor Bocharsky

    Hey maxii123 ,

    What do you mean? You may find all the code in this screencast below the video in course script. Our code blocks are expandable, it means you can click on double arrow against the line to load more context of the file. Or you can download the whole course project code in up right corner: "Download" -> "Course Code" if you want to see the final code to the end of this course - but it's available for users who one the course or have paid subscription only. Does it makes sense for you?

    Cheers!

  • 2017-06-08 maxii123

    How did you complete that loginAction and bootrow?

  • 2017-06-07 weaverryan

    Ah, I was actually expending an error with the $autUtils part! :) As long as you have the use statement for the Request, that *should* actually pass you the request (in any Symfony version). But anyways... there *is* a legit difference in 3.3 - so I'm glad you asked!

    Cheers!

  • 2017-06-05 Robert Went

    Ah yeah, that makes sense.

    It was the controller code for the login action from the 3.3 version which shows up as default

    public function loginAction(Request $request, AuthenticationUtils $authUtils)

    $request isn't defined.

  • 2017-06-05 weaverryan

    Yo Robert Went!

    You're right that you can always copy from the finish folder :). But, I *am* curious which part changed - was it the login template? Or the login controller? I *think* I know the issue (and it's my fault - I updated the documentation on Symfony.com): In Symfony 3.3, you can fetch services in a new way (we're talking about it here: http://knpuniversity.com/sc.... The Symfony.com documentation has been updated to use this... which is cool! Except if you're using a Symfony 3.2 project... and then you try to copy from the 3.3 version of the docs (which show up by default). If I'm right, then it was the controller code that was the problem. Was that it?

    Oh, also, for other people, we do have the code on each page below the video - so if you ever want to copy a bit of code, you can grab it there really quickly.

    Cheers!

  • 2017-06-04 Robert Went

    This no longer works with the demo code from the symfony site, you need to copy it from the script or the downloads finish folder.

  • 2017-04-18 Victor Bocharsky

    Hey Ponrad,

    Good question! Yes, it's a good practice. And Symfony docs recommend to do this to protect your login form against CSRF attacks: http://symfony.com/doc/curr...

    Cheers!

  • 2017-04-17 Ponrad C

    One question guys do we need to put to the login form some csrf protection?

  • 2016-10-03 Victor Bocharsky

    Hey Andrey,

    What's your question here? :)

    I think you probably forget to pass $form to the template, here's a fix:


    return $this->render('security/login.html.twig', array(
    'last_username' => $lastUsername,
    'error' => $error,
    'form' => $form->createView(),
    ));

    P.S. Don't forget to call "createView()" method on form object before passing it to the template.

    Cheers!

  • 2016-09-30 Andrey Dvortsevoy

    Error " Variable "form" does not exist in security\login.html.twig at line 11"

    Controller Security
    namespace Admin\AdminBundle\Controller;

    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Admin\AdminBundle\Form\LoginForm;
    use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

    class SecurityController extends Controller
    {

    /**
    * @Route("/login", name="security_login")
    */
    public function loginAction()
    {
    $authenticationUtils = $this->get('security.authentication_utils');

    $error = $authenticationUtils->getLastAuthenticationError();

    // Ия пользователя
    $lastUsername = $authenticationUtils->getLastUsername();

    $form = $this->createForm(LoginForm::class, [
    '_username' => $lastUsername,
    ]);

    return $this->render(
    'security/login.html.twig',
    array(
    'last_username' => $lastUsername,
    'error' => $error,
    )
    );
    }

    }

    LoginForm Class
    namespace Admin\AdminBundle\Form;

    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\Extension\Core\Type\PasswordType;
    use Symfony\Component\Form\FormBuilderInterface;
    use Symfony\Component\OptionsResolver\OptionsResolver;
    use Symfony\Component\Form\Extension\Core\Type\SubmitType;

    class LoginForm extends AbstractType
    {
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder
    ->add('_username')
    ->add('_password', PasswordType::class);
    }
    }

    login.html.twig
    {% extends 'base.html.twig' %}
    {% block body %}
    <div class="row">
    <div class="small-12 medium-12 large-12 columns">
    <h2>Login</h2>

    {% if error %}
    <div>{{ error.messageKey|trans(error.messageDate, 'security') }}</div>
    {% endif %}

    {{ form_start(form) }}
    {{ form_widget(form) }}
    {{ form_row(form._username) }}
    {{ form_row(form._password) }}
    <button type="submit">login</button>
    {{ form_end(form) }}

    </div>
    </div>
    {% endblock %}