Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Creating a Login Form (Part 1)

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Creating a Login Form (Part 1)

So where’s the actual login form? Well, that’s our job - the security layer just helps us by redirecting the user here.

Oh, and there’s a really popular open source bundle called FosUserBundle that gives you a lot of what we’re about to build. The good news is that after building a login system in this tutorial, you’ll better understand how it works. So build it once here, then take a serious look at FosUserBundle.

Creating a Bundle by Hand

Let’s create a brand new shiny bundle called UserBundle for all of our user and security stuff. We could use the app/console generate:bundle task to create this, but let’s do it by hand. Seriously, it’s easy.

Just create a UserBundle directory and an empty UserBundle class inside of it. A bundle is nothing more than a directory with a bundle class:

// src/Yoda/UserBundle/UserBundle.php
namespace Yoda\UserBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class UserBundle extends Bundle
{
}

Now, just activate it in the AppKernel class and, voila! Our brand new shiny bundle is ready:

// app/AppKernel.php
// ...

public function registerBundles()
{
    $bundles = array(
        // ...
        new Yoda\UserBundle\UserBundle(),
    );

    // ...
}

Login Form Controller

To make the login page, add a Controller directory and put a new SecurityController class inside of it. Give the class a loginAction method. This will render our login form:

// src/Yoda/UserBundle/Controller/SecurityController.php
namespace Yoda\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class SecurityController extends Controller
{
    public function loginAction()
    {
    }
}

Using Annotation Routing

Before we fill in the guts of loginAction, we need a route! After watching episode 1, you probably expect me to create a routing.yml file in UserBundle and add a route there.

Ha! I’m not so predictable! Instead, we’re going to get crazy and build our routes right inside the controller class using annotations. The docs for this feature live at symfony.com under a bundle called SensioFrameworkExtraBundle. This bundle came pre-installed in our project. How thoughtful!

First, add the Route annotation namespace:

// src/Yoda/UserBundle/Controller/SecurityController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class SecurityController extends Controller
{
    // ...
}

Now, we can add the route right above the method:

// src/Yoda/UserBundle/Controller/SecurityController.php
// ...

/**
 * @Route("/login", name="login_form")
 */
public function loginAction()
{
    // ... todo still..
}

Finally, tell Symfony to look for routes in our controller by adding an import to the main routing.yml file:

# app/config/routing.yml
# ...

user_routes:
    resource: "@UserBundle/Controller"
    type: annotation

Remember that Symfony never automatically finds routing files: we always import them manually from here.

Cool - change the URL in your browser to /login. This big ugly error about our controller not returning a response is great news! No, seriously, it means that the route is working. Now let’s fill in the controller!

The loginAction Logic

Most of the login page code is pretty boilerplate. So let’s use the age-old art of copy-and-paste from the docs.

Head to the security chapter and find the login form section. Copy the loginAction and paste it into our controller. Don’t forget to add the use statements for the SecurityContextInterface and Request classes:

// src/Yoda/UserBundle/Controller/SecurityController.php
namespace Yoda\UserBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\HttpFoundation\Request;
// ...

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login")
     */
    public function loginAction(Request $request)
    {
        $session = $request->getSession();

        // get the login error if there is one
        if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(
                SecurityContextInterface::AUTHENTICATION_ERROR
            );
        } else {
            $error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR);
            $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR);
        }

        return $this->render(
            'AcmeSecurityBundle:Security:login.html.twig',
            array(
                // last username entered by the user
                'last_username' => $session->get(SecurityContextInterface::LAST_USERNAME),
                'error'         => $error,
            )
        );
    }

The method just renders a login template: it doesn’t handle the submit or check to see if the username and password are correct. Another layer handles that. It does pass the login error message to the template if there is one, but that’s it.

The Template Annotation Shortcut

The pasted code is rendering a template using our favorite render method that lives in Symfony’s base controller.

Hmm, let’s not do this. Instead, let’s use another shortcut: the @Template annotation, which is also from SensioFrameworkExtraBundle.

Anytime we use an annotation in a class for the first time, we’ll need to add a use statement for it. Copy this from the docs. Now, put @Template above the method and just return the array of variables you want to pass to Twig:

// src/Yoda/UserBundle/Controller/SecurityController.php
// ...

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class SecurityController extends Controller
{
    /**
     * @Route("/login", name="login_form")
     * @Template()
     */
    public function loginAction()
    {
        // ...

        return array(
            // last username entered by the user
            'last_username' => $session->get(SecurityContextInterface::LAST_USERNAME),
            'error'         => $error,
        );
    }
}

With @Template, Symfony renders a template automatically, and passes the variables we’re returning into it. It’s cool, saves us some typing and supports the rebel forces.

Leave a comment!

39
Login or Register to join the conversation

I am having an issue getting form login to work in Symfony 3 using Guard for authentication and mysql database.

I have read everything on this site, symfony.com and your awesome slideshare @ http://www.slideshare.net/w.... But, I think I am missing something.

What I am trying to do:

- Allow user to log in

- Redirect user to home page upon login

I have created a login form. But, when I try to log in with the users I have created, I am redirected back to the login page and the user is only authenticated as Anonymous.

I also have a registration page. And, when I register a new user, I am able to authenticate the user in the registerAction function of the RegistrationController and the user is sent to the home page as expected and shows as authenticated as the user.

But, after logging the user out and trying to log in the newly created user, I am redirected back to the login page and the user is only authenticated as Anonymous.

Trying to debug, the checkCredentials function in the FormLoginAuthenticator seems to pass and is returning true and the onAuthenticationSuccess function is called.

I am relatively new to Symfony and am sure that I am missing something. But, after looking over it many times, I can't seem to figure out what is going on.

Anyway you could take a quick look at it?

https://github.com/onlinesp...

Thanks in advance,
Scott

1 Reply

Hey Scott!

Yea, I think this issue :). Actually, when you login, you *are* logged in for a moment, but then you lose the login on the next request. You can see this by setting intercept_redirects (https://github.com/symfony/... to true temporarily. After login, it will stop and *not* redirect you. And on this page, you'll see that you *were* successfully authenticated.

So, why is this lost on the next request? It's a gotcha (one that hopefully we can make less of a gotcha in the future - it's tricky). Three things happen at the beginning of each request after you're logged in:

A) The User object is deserialized from the session (that's when the User::unserialize method is called)

B) That User object is used to query for a new one: refreshUser is called on your user provider. Usually, you use the id of the serialized User to query for a fresh one.

C) (and here's the tricky one): the deserialized User object is compared with the fresh User object. If they are not deemed "equal", you are logged out. What??? The purpose of this is to allow you to change your password in the database and cause a remote bad person who has stolen your account to be automatically logged out. Basically, you need to make sure that several properties are serialized: username, password, salt (not relevant in your case) and any values that fuel the AdvancedUserInterface methods. As long as these are serialized, then the two User objects will look equal. Here's the code in Symfony that checks this: https://github.com/symfony/....

The most common problem is not serializing some properties you need in User::serialize(). But you did a perfect job! The issue is smaller, and I've described it more on this pull request: https://github.com/onlinesp...

I hope that helps! Also, to make your code simpler, you could delete your UserProvider (and associated code in UserRepository) and instead user the built-in entity user provider (you have some commented-out code for this - it looks like you *were* using it. Anymore, I don't really think that *anyone* needs a custom user provider if they're using Doctrine. If you need to do some crazy query for your User in your "authenticator", just call a custom method on your repository. You're already doing this: https://github.com/weaverry...

Also, you can remove the "form_login" stuff if you're using guard as your login form - it's just not needed. You *will* still you need your login routes, template, etc.

Cheers!

1 Reply

I really appreciate your help with this and all the other stuff you do for the community!

I thought that was what the application was doing (logging in and logging back out) but didn't know about the intercept_redirects setting. Your explanation makes sense.

I merged the change in the pull request, deleted the UserRepository and references to it, and removed the "form_login" stuff and everything is working. Yay!

Now off to implement API and Facebook authentication!

Reply

Wow, that's awesome!!!

It's on my todo list to add some things for social (Facebook) auth, but I haven't gotten there quite yet. I *do*, however, have a WIP code implementation of Facebook auth using the KnpGuardBundle (which is very close to the core Guard). If it's helpful, you can see that final code here: https://github.com/knpunive..., with step-by-step commits: https://github.com/knpunive...

Good luck!

Reply

Thanks again!

I was able to implement Login, API, and Facebook authentication. Initial versions here: https://github.com/onlinesp...

Wouldn't have been able to finish this so quickly without your help!

1 Reply

That's awesome! Thanks for sharing your efforts!

Reply
Default user avatar
Default user avatar Sergio Valije Guiadanes | posted 5 years ago

Hi everyone. I asked this before in an old thread, but I think this is a better idea to put my question in here

I'm having this problem after following the steps and I'm a bit blocked. My symfony version is 3.2.8

Here is the output I got when I try to run my application

InvalidConfigurationException in ArrayNode.php line 317:
Unrecognized option "knpu_guard" under "security.firewalls.main"

Do I missed something ?

Thanks in advance!

Reply
Default user avatar
Default user avatar Sergio Valije Guiadanes | Sergio Valije Guiadanes | posted 5 years ago

I can ask myself: The article must be a bit deprecated because knpu_guard is no longer accepted. If you have this problem use "guard" instead as a key in the security.yml file

Reply

Hey Sergio Valije Guiadanes!

Yes, you're right! Guard was accepted into Symfony's core in version 2.8 - before, that, it was a third-party bundle and was configured via knpu_guard. There are a few other differences as well with the new version in core - so just be careful! It's on our list to properly update our Guard tutorial for the version in Symfony's core.

Cheers!

Reply
Default user avatar

Followed this tutorial to the 'T' and still cannot login. No errors, just tells me 'Username could not be found.'

Reply

Oh no!

Let's debug this :). How far are you through the tutorial? I'm asking because in the beginning, we're still loading users from our "hardcoded" list in security.yml. Later, we load from the database. But in more recent versions of Symfony, we've *changed* what the out-of-box security.yml file looks like - and it *no* longer includes these "hardcoded" users by default. That *might* be the cause of the problem. Here's what the entire security.yml file originally looked like in this tutorial at the end of the next chapter (so, after we finish the login form stuff): https://gist.github.com/wea...

Or, it could be something entirely different - let me know what your setup looks like :). The good news is that this error tells us that the User couldn't be found - so we can rule out there being some problem with checking the user's password.

Cheers!

1 Reply
Default user avatar
Default user avatar Dan Costinel | posted 5 years ago

Hello guys!

Right now, the only way I saw is to create a SecurityController and inside it a loginAction() method which has it's own path (/login), and which, at the end, will render a security/login.html.twig template containing the login form. Also in security.yml there is an access_control with - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } entry.

How can I change all this behavior? As I want to have the login and register forms in my homepage, and not each of them in their separate pages.

Tried to use, in my index.html.twig (which is my homepage template), something like:

{% block body %}
{# ... #}
{% render(controller('AppBundle:Security:login')) %}
{# ... #}
{% endblock %}

but this throws an error:
Unexpected "render" tag (expecting closing tag for the "block" tag defined near line X) in default/index.html.twig at line Y.

I also tried to change the template of the loginAction() method to 'default/index.html.twig', and write the login form in this template, as:
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}

<form action="{{ path('login') }}" method="post">
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">

<div class="form-group">
<label for="email">Your E-mail</label>
<input type="email" class="form-control" name="email" id="email">
</div>
<div class="form-group">
<label for="first_name">Your First Name</label>
<input type="text" class="form-control" name="first_name" id="first_name" value="{{ last_username }}">
</div>
<div class="form-group">
<label for="password">Your Password</label>
<input type="password" class="form-control" name="_password" id="password">
</div>
<button type="button" class="btn btn-primary">Submit</button>
</form>
The errors where:
1. Variable "error" does not exist in default/index.html.twig at line Z. (tried to fix this by adding "if error is defined" - the error was gone, but I think is not the right way to fix it)
2. Variable "last_username" does not exist in default/index.html.twig at line T. (and I don't know why this variable is not being send from loginAction() to 'default/index.html.twig' as is specified there)

I'll appreciate any suggestion!
Thanks!

Reply

Hey Dan,

You should use "print something - {{ }}" syntax instead of "do something {% %}", here's a right solution:


{{ render(controller('AppBundle:Security:login')) }}

This will fix "Unexpected render tag..." error.

Use is defined test or default Twig filter in template:


{% if error is defined %}
{# or #}
{% if error|default(false) %}

Btw, you could only render both forms on homepage, but process them on its separate pages, just use the correct action in your forms. If user leaves some validation errors - he'll end up on form login page/ registration page. But if user fills form correct - he will be processed and won't see login/registration pages at all.

Cheers!

Reply
Default user avatar
Default user avatar Dan Costinel | Victor | posted 5 years ago

It worked! Cheers!

Reply
Default user avatar

Seems that SecurityContextInterface is deprecated...

Reply

Yes it is! But it's a simple deprecation :) - the constants were moved to a class called Security. So, the new way is:

use Symfony\Component\Security\Core\Security;

// ...
Security::AUTHENTICATION_ERROR

Btw - this is the tutorial for Symfony 2 - we have an updated Security tutorial for Symfony 3: http://knpuniversity.com/sc... - it has all the latest and greatest ways of doing things :).

Cheers!

Reply
Default user avatar
Default user avatar Daniel Weise | posted 5 years ago

To complete the authentication is necessary to return the value true on method checkCredentials

Reply

Hey, Daniel!

Are you talking about checkCredentials() method which should be implemented from AbstractGuardAuthenticator? If so, then yes, you should return "true" to cause authentication success.

<blockquote>If getCredentials() returns a non-null value, then this method is called and its return value is passed here as the $credentials argument. Your job is to return an object that implements UserInterface. If you do, then checkCredentials() will be called. If you return null (or throw an AuthenticationException) authentication will fail.
</blockquote>

You can find explanation of other methods on <a href="http://symfony.com/doc/current/cookbook/security/guard-authentication.html#the-guard-authenticator-methods&quot;&gt;The Guard Authenticator Methods</a> page.

Cheers!

1 Reply
Default user avatar
Default user avatar FODHIL ASMA | posted 5 years ago

when i submit submit the form to the "login_check" path i get this error:

ClassNotFoundException in appDevDebugProjectContainer.php line 1654: Attempted to load class "FormLoginAuthenticator" from namespace "Arably\RestBundle\Security".
Did you forget a "use" statement for another namespace?

I use symfony2.8, FosUserBundle as the user provider and mongodb odm

and this is my services file:

services:
form_login_authenticator:
class: Arably\RestBundle\Security\FormLoginAuthenticator
autowire: true

FormLoginAuthenticator class:

namespace Arably\RestBundle\Security;

use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;

use Symfony\Component\HttpFoundation\Request;

use Symfony\Component\HttpFoundation\Response;

use Symfony\Component\Security\Core\User\UserInterface;

use Symfony\Component\Security\Core\User\UserProviderInterface;

class FormLoginAuthenticator extends AbstractGuardAuthenticator
{
The methods implemented
}

SecurityController:

namespace Arably\RestBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class SecurityController extends Controller

{

public function loginAction()

{

}

public function loginCheckAction()

{

// will never be executed

}

}

i am using a rest API so i don't have a login page provided by the server

Reply

Hi there!

If you're building an API, then there's limited useful ness to using FOSUserBundle (you can use it still for your User document, but you won't use any of its other features). In this case, your error is just because you have a bad namespace (or something) with your authenticator. The namespace and services.yml look good to me. So, does this file live in the correct location? It should be "src/Arably/RestBundle/Security/FormLoginAuthenticator.php".

Btw, we do have a tutorial about API authentication - it might be helpful! http://knpuniversity.com/sc...

Cheers!

1 Reply
Default user avatar

Thanks for you response. I finally implemented the check login method and generated the JWT in it.

1 Reply
Default user avatar
Default user avatar cristicitea | posted 5 years ago

What do I use for the roles field in the user table? And how to I tell what role is the user logged in as after login?

Reply

Hey cristicitea ,

What do you mean in the first question? What type of the roles field should you use? or something different?

Actually, you should add a role for user before he will be logged in. The most common way is to set some role, like ROLE_USER when user is doing registration, i.e. when you're creating a new user in the DB. So when users are logging in - they _already_ have some role. Then you can easily check wether a logged in user has the required role with isGranted() method, i.e. $this->isGranted('ROLE_USER') in your controller (but only if you extends the Symfony\Bundle\FrameworkBundle\Controller\Controller class) or use @security.authorization_checker service otherwise.

Cheers!

Reply
Default user avatar
Default user avatar cristicitea | Victor | posted 5 years ago | edited

so, do I literally add ROLE_USER in the roles field in the database? Also, will the security file catch that role in the`
access_control`
automatically?

for example, if i am logged in as an admin, will this code understand that I am logged in as admin?

 { path: ^/test, roles: ROLE_ADMIN }```

Reply

Hey cristicitea ,

Yes, you should store user roles in database, for example when you store user information during registration. And yes, Symfony app should understand, but try it to double check - sometimes you could miss something in config. Take a look at our course Symfony Security: Beautiful Authentication, Powerful Authorization - it has nice example about dynamic user roles at the end.

Cheers!

Reply
Default user avatar
Default user avatar cristicitea | Victor | posted 5 years ago | edited

ok, I figure it out. the roles field in the db takes an array`
["ROLE_USER"]`
. And using an annotation works as well`
@Security("is_granted('ROLE_USER')")`

Reply

Yes, that's great! Actually, I use both access_control list and @Security annotation to be sure with proper security. Even more, you can double check if user has access with custom call of isGranted() method in actions. ;)

Cheers!

Reply
Default user avatar
Default user avatar dpatterson | posted 5 years ago

Color me a bit confused. The FormLoginAuthenticator::getCredentials() method includes this line:
$request->getSession()->set(Security::LAST_USERNAME, $username);

but I don't see a use for Anything\...\Security.

Where does it come from?

Reply

There's a use statement at the top of that file:

use Symfony\Component\Security\Core\Security;

I just forgot to "show" it in the code-block. I'll fix that now!

Reply
Default user avatar
Default user avatar BondashMaster | posted 5 years ago

This is my app config routing:

user:
resource: "@UserBundle/Resources/config/routing.yml"
prefix: /

app:
resource: "@AppBundle/Controller/"
type: annotation

user_routes:
resource: "@UserBundle/Controller"
type: annotation

I'm still getting the error:
No route found for "GET /login"

Any idea. I read the other comments but I check twice and I have all my @

Reply
Default user avatar

I also use the php bin/console debug:router command and I get the /login listed :/

Reply

Hey dash!

You already guessed my first debugging technique - bin/console debug:router. If you see /login listed, then you should not be getting this "No route found for GET /login" error. I mean, really, it's basically impossible - so something *very* strange is happening! I would click the web debug toolbar to go into the profiler. Then, go to the Routing tab. This will show you all of the routes that were searched. Do you see /login there?

Cheers!

1 Reply
Default user avatar
Default user avatar BondashMaster | weaverryan | posted 5 years ago

Dude. My browser save /Login instead of /login. Dammit XD

1 Reply
Default user avatar

Is anybody else having difficulty getting the route /login to register with Symfony? I get a "No route found for "GET /login". Also the route isn't listed when using route:debug!

If it helps, you can view my Controller and routing file on my GitHub - https://github.com/sjhuda/k...

Any ideas? :/

Reply
Default user avatar

Soooo, after a day of debugging...I looked at the other annotation routes in the Event Controller and I missed the @ infront of 'Route'. *facepalm*

Reply

Bah, bummer! But good debugging! And sorry for the late reply! I especially liked that you ran router:debug - that is always what I do first when something happens that I don't expect with routing.

Good luck! At least you won't make that mistake again :)

Reply
Default user avatar

No worries on the delay. It actually gave me a better understanding of Routing (and especially annotations).

Keep up the good work, love all your videos.

Reply
Default user avatar
Default user avatar Marco Bobbi | posted 5 years ago

Hi i have a problem! I i use SecurityContextInterface the code give me an error! but SecurityContextInterface is not deprecated??

Reply

Hey Marco!

What's the error? The SecurityContextInterface *is* deprecated, but it obviously still exists (unless you're using Symfony 3.0, then it's removed). Either way, use the Symfony\Component\Security\Core\Security class instead. We'll be releasing updated tutorials with all the new classes you should use soon :).

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "~2.4", // v2.4.2
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.0.1
        "symfony/assetic-bundle": "~2.3", // v2.3.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.5
        "symfony/monolog-bundle": "~2.4", // v2.5.0
        "sensio/distribution-bundle": "~2.3", // v2.3.4
        "sensio/framework-extra-bundle": "~3.0", // v3.0.0
        "sensio/generator-bundle": "~2.3", // v2.3.4
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
        "ircmaxell/password-compat": "~1.0.3", // 1.0.3
        "phpunit/phpunit": "~4.1" // 4.1.0
    }
}
userVoice