Buy

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!

  • 2016-08-16 weaverryan

    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/scree... - it has all the latest and greatest ways of doing things :).

    Cheers!

  • 2016-08-16 PJoy

    Seems that SecurityContextInterface is deprecated...

  • 2016-06-22 Victor Bocharsky

    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.

    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.

    You can find explanation of other methods on The Guard Authenticator Methods page.

    Cheers!

  • 2016-06-22 Daniel Weise

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

  • 2016-06-18 weaverryan

    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/weaver...

    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!

  • 2016-06-17 cj5

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

  • 2016-06-01 FODHIL ASMA

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

  • 2016-05-31 weaverryan

    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/scree...

    Cheers!

  • 2016-05-29 FODHIL ASMA

    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

  • 2016-05-11 BondashMaster

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

  • 2016-05-11 weaverryan

    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!

  • 2016-05-10 BondashMaster

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

  • 2016-05-10 BondashMaster

    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 @

  • 2016-01-04 weaverryan

    That's awesome! Thanks for sharing your efforts!

  • 2016-01-02 Scott Collier

    Thanks again!

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

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

  • 2015-12-28 weaverryan

    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/knpuniversi..., with step-by-step commits: https://github.com/knpuniversi...

    Good luck!

  • 2015-12-28 Scott Collier

    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!

  • 2015-12-28 weaverryan

    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/sym... 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/sym....

    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/onlinespace...

    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/weaverryan/...

    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!

  • 2015-12-25 Scott Collier

    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/weav.... 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/onlinespace...

    Thanks in advance,
    Scott

  • 2015-12-14 weaverryan

    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!

  • 2015-12-10 Marco Bobbi

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

  • 2015-09-03 Syed

    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.

  • 2015-09-03 weaverryan

    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 :)

  • 2015-08-31 Syed

    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*

  • 2015-08-30 Syed

    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/knp-...

    Any ideas? :/

  • 2015-07-23 weaverryan

    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!

  • 2015-07-23 dpatterson

    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?