Buy

Creating a Login Form (Part 2)

Ok, we’re almost done, seriously!

Creating the Template

Copy the template code from the docs and create the login.html.twig file:

{# src/Yoda/UserBundle/Resources/views/Security/login.html.twig #}
{% if error %}
    <div>{{ error.message }}</div>
{% endif %}

<form action="{{ path('login_check') }}" 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>

This prints the login error message if there is one and has a form with _username and _password fields. When we submit, Symfony is going to be looking for these fields, so their names are important.

Tip

You can of course change these form field names to something else. Google for the username_parameter and password_parameter options.

Let’s make this extend our base.html.twig template. I’ll also add in a little bit of extra markup:

{# src/Yoda/UserBundle/Resources/views/Security/login.html.twig #}
{% extends '::base.html.twig' %}

{% block body %}
<section class="login">
    <article>

        {% if error %}
            <div>{{ error.message }}</div>
        {% endif %}

        <form action="{{ path('login_check') }}" 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" />

            <button type="submit">login</button>
        </form>

    </article>
</section>
{% endblock %}

Handling Login: login_check

Route check! Controller check! Template check! Let’s try it! Oh boy, an error:

Unable to generate a URL for the named route “login_check” as such route does not exist.

Ah, the copied template code has a form that submits to a route called login_check. Let’s create another action method and use @Route to create that route:

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

/**
 * @Route("/login_check", name="login_check")
 */
public function loginCheckAction()
{
}

Call me crazy, but I’m going to leave this action method completely blank. Normally, it means that if you went to /login_check it would execute this controller and cause an error since we’re not returning anything.

Configuring login_path and check_path

But this controller will never be executed. Before I show you, open up security.yml and look at the form_login configuration:

# app/config/security.yml
# ...

firewalls:
    secured_area:
        pattern:    ^/
        form_login:
            check_path: _security_check
            login_path: /my-login-url
        # ...

login_path is the URL or route name the user should be sent to when they hit a secured page. Change this to be login_form: the name of our loginAction route. check_path is the URL or route name that the login form will be submitted to. Change this to be login_check.

In your browser, try going to /new. Yes! Now we’re redirected to /login, thanks to the login_path config key. The page looks just terrible, but it’s working.

Using and Understanding the Login Process

Now, let me show you one of the strangest parts of Symfony’s security system. When we login using user and userpass... it works! We can see our username in the web debug toolbar and even a role assigned to us. What the heck just happened?

When we submit, Symfony’s security system intercepts the request and processes the login information. This works as long as we POST _username and _password to the URL /login_check. This URL is special because its route is configured as the check_path in security.yml. The loginCheckAction method is never executed, because Symfony intercepts POST requests to that URL.

If the login is successful, the user is redirected to the page they last visited or the homepage. If login fails, the user is sent back to /login and an error is shown.

And where did the user and userpass stuff come from? Actually, right now the users are just being loaded directly from security.yml:

# app/config/security.yml
# ...
providers:
    in_memory:
        memory:
            # this was here when we started: 2 hardcoded users
            users:
                user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }

In a minute, we’ll load users from the database instead.

Leave a comment!

  • 2016-07-07 weaverryan

    Yes, this is correct! If you're using the "in memory" users, this tells Symfony that you're not encoding their passwords (so whatever passwords you have in security.yml *are* the real passwords - they're not encoded).

    We actually see (and fix) this error in the latest security tutorial as well :) http://knpuniversity.com/scree...

    Cheers!

  • 2016-07-06 lipe1233

    in version 2.8 I needed to add these lines to memory users run without exception:

    #config/security.yml

    security:
    encoders:
    Symfony\Component\Security\Core\User\User: plaintext

    #more configurations...

    without these lines, i receive follow exception: No encoder has been configured for account "Symfony\Component\Security\Core\User\User".

  • 2015-09-03 weaverryan

    Hey there!

    There might be a simple answer :). What version of Symfony are you using (you can look at the symfony/symfony line in composer.json)? The security.authentication_utils stuff is new in Symfony 2.6. Previously, you needed all the code that you wrote above (and that we include in this screencast). But in Symfony 2.6, all the code is put into the security.authentication_utils service, just to be easier. But ultimately, both are doing the same thing.

    Does that clear it up?

  • 2015-08-31 wardy484

    Using the following seems to work:
    use Symfony\Bundle\FrameworkBundle\Controller\Controller;
    use Symfony\Component\Security\Core\SecurityContext;

    class SecurityController extends Controller
    {
    public function loginAction()
    {
    // get the error if any (works with forward and redirect -- see below)
    if ($this->get('request')->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
    $error = $this->get('request')->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
    } else {
    $error = $this->get('request')->getSession()->get(SecurityContext::AUTHENTICATION_ERROR);
    }

    return $this->render('SecurityBundle:Security:login.html.twig', array(
    // last username entered by the user
    'last_username' => $this->get('request')->getSession()->get(SecurityContext::LAST_USERNAME),
    'error' => $error,
    ));
    }
    }
    However I would like to know what my initial issue was.

  • 2015-08-30 wardy484

    Hi! I'm getting the following error:
    You have requested a non-existent service "security.authentication_utils". Did you mean this: "security.authentication.manager"?

    I've tried the following use statements but I've had no luck fixing the error!

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

  • 2015-08-18 Nord Wind

    Thank you! Now I understand my mistake about missing "/". And also about logout action (why my code is never been executed). This is my inattention.

    Thank you again,

    Sincerely, Yury

  • 2015-08-18 weaverryan

    Hey Nord!

    Yes, good solution with adding the requirements, so that /{id} doesn't match every other URL, like /login. It's not a problem for me in the video, only because my routes are all /{id}/ (with a closing /), so that makes them different than /login. But, adding a requirement - or keeping the prefix - is a better way :). And good use of the security.authentication_utils - that's a nice, clean way in newer version of Symfony!

    For /logout, what you have should work fine, but you also shouldn't need to have any logic in your controller. As long as you activate the "logout" key under your firewall show "path" option is set to either /logout out "logout" (the name of your logout route), then Symfony will automatically intercept all requests to /logout and perform the logout logic. So - though it seems odd - your logoutAction() will never be executed.

    Cheers!

  • 2015-08-11 Nord Wind

    I have problem with routing in Symfony 2.7. When I set default page is EventController:IndexAction by removing /event in route annotation (setting "/"), controller throw an error: "Could not find Entity Event" when I try redirect to /login in security.yml.
    I solve this problem by adding requirements annotation in every route with parameters: for example @Route("/{id}", name="event_delete", requirements={"id":"\d+"}). Now it`s work as expected.

    Also I use $helper = $this->get('security.authentication_utils'); from cookbook. Diego said about before in this conversation.
    And write logout method in SecurityController

    /**
    * @Route("/logout", name="logout")
    * @Method("GET")
    */
    public function logoutAction()
    {

    $this->get('security.token_storage')->setToken(null);
    $this->get('request')->getSession()->invalidate();

    }

  • 2015-01-29 weaverryan

    @nam797 if you're getting this error, run "php app/console router:debug" to see a list of all of your routes. Is there a route with the path "/login"?

    Edison Sorry I missed your original comment! Glad you got it working :)

  • 2015-01-29 Edison

    Hello nam797! Oh yes, sorry for not posting back the solution, was really weird. Thanks!

  • 2015-01-29 nam797

    Did you get this figured out?

  • 2015-01-15 Edison

    Hi Again Ryan!
    I've already done successfully the Starwarsevents project, now I'm trying to make a new one following again this screencast series, you know, keeping the shape and something else to proof myself.... everything went smooth and straighforward, til I created manually the userbundle according to instructions here....well the fact is that seems the Controller is not loading the routes, I'm building my routes right in the controller class using annotations....as hint for you...I'm stack (first 9 minutes episode 2) in the point where it supposed to return this error : THE CONTROLLER MUST RETURN A RESPONSE (NULL GIVEN). DID YOU FORGET TO ADD A RETURN STATEMENT SOMEWHERE IN YOUR CONTROLLER?..... I've added the use statement in the class, the annotations above the method indexaction, plugged it in appkernel..(All same process as I did at starwarsevents project)......trying this url: http://localhost:8000/login .I'm just getting back this poor message **No route found for "GET /login"** .....

    Please help me out with one of your wonderful tips!!

    Thanks Again Number 1

  • 2015-01-13 Diego Aguiar

    In symfony 2.6 this is much simpler, you just need this method:

    public function loginAction()
    {
    $helper = $this->get('security.authentication_utils');

    return $this->render('AcmeSecurityBundle:Security:login.html.twig', array(
    'last_username' => $helper->getLastUsername(),
    'error' => $helper->getLastAuthenticationError(),
    ));
    }