How to Create a Login Form

So you want a login form? That's simple. And along the way, you'll learn all the steps that happen during authentication, and how you can customize what happens at each one.

You still have to do some work, but you're going to be really happy with the result.

Tip

Click Download to get the starting or finished code of this tutorial.

Create the Login Form

Don't think about security yet! Instead, start by creating a Symfony controller with two action methods: one for rendering the login form and another that'll handle the login submit:

33 lines src/AppBundle/Controller/SecurityController.php
... lines 1 - 2
namespace AppBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class SecurityController extends Controller
{
/**
* @Route("/login", name="security_login")
*/
public function loginAction()
{
$helper = $this->get('security.authentication_utils');
return $this->render('security/login.html.twig', array(
// last username entered by the user (if any)
'last_username' => $helper->getLastUsername(),
// last authentication error (if any)
'error' => $helper->getLastAuthenticationError(),
));
}
/**
* @Route("/login_check", name="security_login_check")
*/
public function loginCheckAction()
{
// will never be executed
}
}

So far, this is just a lovely, but boring set of actions. The only interesting parts are the last_username and error variables. Where are those coming from? You'll see. Also, loginCheckAction() doesn't do anything - and it never will. Another layer will handle the login submit.

Next, create the login template:

25 lines app/Resources/views/security/login.html.twig
{% extends 'base.html.twig' %}
{% block body %}
<form action="{{ path('security_login_check') }}" method="post">
{% if error %}
<div class="alert alert-danger">
{{ error.messageKey|trans(error.messageData) }}
</div>
{% endif %}
<div>
<label for="username">Username</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="_password" />
</div>
<br/>
<button type="submit">Login</button>
</form>
{% endblock %}

This form submits to the /login_check URL and the field names are _username and _password. Remember these - they'll be important in a minute (see getCredentials()).

Installing Guard

Read the short Installation chapter to make sure you've got the bundle installed and enabled.

Creating an Authenticator

With Guard, the whole authentication process - fetching the username/password POST values, validating the password, redirecting after success, etc - is handled in a single class called an "Authenticator". Your authenticator can be as crazy as you want, as long as it implements KnpU\Guard\GuardAuthenticatorInterface.

For login forms, life is easier, thanks to a convenience class called AbstractFormLoginAuthenticator. Create a new FormLoginAuthenticator class, make it extend this class, and add all the missing methods (from the interface and abstract class):

37 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 2
namespace AppBundle\Security;
use KnpU\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
public function getCredentials(Request $request)
{
// TODO: Implement getCredentials() method.
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// TODO: Implement getUser() method.
}
public function checkCredentials($credentials, UserInterface $user)
{
// TODO: Implement checkCredentials() method.
}
protected function getLoginUrl()
{
// TODO: Implement getLoginUrl() method.
}
protected function getDefaultSuccessRedirectUrl()
{
// TODO: Implement getDefaultSuccessRedirectUrl() method.
}
}

Your mission: fill in each method. We'll get to that in a second.

To fill in those methods, we're going to need some services. To keep this tutorial simple, let's pass the entire container into our authenticator:

45 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 5
use Symfony\Component\DependencyInjection\ContainerInterface;
... lines 7 - 10
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
... lines 19 - 43
}

Tip

For seasoned-Symfony devs, you can of course inject only the services you need.

Registering your Authenticator

Before filling in the methods, let's tell Symfony about our fancy new authenticator. First, register it as a service:

10 lines app/config/services.yml
... lines 1 - 5
services:
app.form_login_authenticator:
class: AppBundle\Security\FormLoginAuthenticator
arguments: ["@service_container"]

Next, update your security.yml file to use the new service:

24 lines app/config/security.yml
security:
... lines 2 - 13
firewalls:
... lines 15 - 18
main:
anonymous: ~
knpu_guard:
authenticators:
- app.form_login_authenticator

Your firewall (called main here) can look however you want, as long as it has a knpu_guard section under it with an authenticators key that includes the service name that you setup a second ago (app.form_login_authenticator in my example).

I've also setup my "user provider" to load my users from the database:

24 lines app/config/security.yml
security:
encoders:
# Our user class and the algorithm we'll use to encode passwords
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
AppBundle\Entity\User: bcrypt
providers:
# Simple example of loading users via Doctrine
# To load users from somewhere else: http://symfony.com/doc/current/cookbook/security/custom_provider.html
database_users:
entity: { class: AppBundle:User, property: username }
... lines 13 - 24

In a minute, you'll see where that's used.

Filling in the Authenticator Methods

Your authenticator is now being used by Symfony. So let's fill in each method:

getCredentials()

68 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 6
use Symfony\Component\HttpFoundation\Request;
... lines 8 - 9
use Symfony\Component\Security\Core\Security;
... lines 11 - 13
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 22
public function getCredentials(Request $request)
{
if ($request->getPathInfo() != '/login_check') {
return;
}
$username = $request->request->get('_username');
$request->getSession()->set(Security::LAST_USERNAME, $username);
$password = $request->request->get('_password');
return array(
'username' => $username,
'password' => $password
);
}
... lines 38 - 66
}

The getCredentials() method is called on every single request and its job is either to fetch the username/password from the request and return them.

So, from here, there are 2 possibilities:

# Conditions Result Next Step
A) Return non-null value Authentication continues getUser()
B) Return null Authentication is skipped Nothing! But if the user is anonymous and tries to access a secure page, getLoginUrl() will be called

A) If the URL is /login_check (that's the URL that our login form submits to), then we fetch the _username and _password post parameters (these were our form field names) and return them. Whatever you return here will be passed to a few other methods later. In this case - since we returned a non-null value from getCredentials() - the getUser() method is called next.

B) If the URL is not /login_check, we return null. In this case, the request continues anonymously - no other methods are called on the authenticator. If the page the user is accessing requires login, they'll be redirected to the login form: see getLoginUrl().

Tip

We also set a Security::LAST_USERNAME key into the session. This is optional, but it lets you pre-fill the login form with this value (see the SecurityController::loginAction from earlier).

getUser()

If getCredentials() returns a non-null value, then getUser() is called next. Its job is simple: return a user (an object implementing UserInterface):

68 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 11
use Symfony\Component\Security\Core\User\UserProviderInterface;
... line 13
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 38
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['username'];
return $userProvider->loadUserByUsername($username);
}
... lines 45 - 66
}

The $credentials argument is whatever you returned from getCredentials() and the $userProvider is whatever you've configured in security.yml under the providers key. My provider queries the database and returns the User entity.

There are 2 paths from there:

# Conditions Result Next Step
A) Return a User object Authentication continues checkCredentials()
B) Return null or throw an AuthenticationException Authentication fails Redirect to getLoginUrl()

A) If you return some User object (using whatever method you want) - then you'll continue on to checkCredentials().

B If you return null or throw any Symfony\Component\Security\Core\Exception\AuthenticationException, authentication will fail and the user will be redirected back to the login page: see getLoginUrl().

checkCredentials()

If you return a user from getUser(), then checkCredentials() is called next. Your job is simple: check if the username/password combination is valid. If it isn't, throw a BadCredentialsException (or any AuthenticationException):

68 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 8
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
... line 10
use Symfony\Component\Security\Core\User\UserInterface;
... lines 12 - 13
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 45
public function checkCredentials($credentials, UserInterface $user)
{
$plainPassword = $credentials['password'];
$encoder = $this->container->get('security.password_encoder');
if (!$encoder->isPasswordValid($user, $plainPassword)) {
// throw any AuthenticationException
throw new BadCredentialsException();
}
}
... lines 55 - 66
}

Like before, $credentials is whatever you returned from getCredentials(). And now, the $user argument is what you just returned from getUser(). To check the user, you can use the security.password_encoder, which automatically hashes the plain password based on your security.yml configuration.

Want to do some other custom checks beyond the password? Go crazy! Based on what you do, there are 2 paths:

# Conditions Result Next Step
A) do anything except throwing an AuthenticationException Authentication successful Redirect the user (may involve getDefaultSuccessRedirectUrl())
B) Throw any type of AuthenticationException Authentication fails Redirect to getLoginUrl()

If you don't throw an exception, congratulations! You're user is now authenticated, and will be redirected somewhere...

getDefaultSuccessRedirectUrl()

Your user is now authenticated. Woot! But, where should we redirect them? The AbstractFormLoginAuthenticator class takes care of most of this automatically. If the user originally tried to access a protected page (e.g. /admin) but was redirected to the login page, then they'll now be redirected back to that URL (so, /admin).

But what if the user went to /login directly? In that case, you'll need to decide where they should go. How about the homepage?

68 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 13
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 61
protected function getDefaultSuccessRedirectUrl()
{
return $this->container->get('router')
->generate('homepage');
}
}

This fetches the router service and redirects to a homepage route (change this to a real route in your application). But note: this method is only called if there isn't some previous page that user should be redirected to.

getLoginUrl()

If authentication fails in getUser() or checkCredentials(), the user will be redirected back to the login page. In this method, you just need to tell Symfony where your login page lives:

68 lines src/AppBundle/Security/FormLoginAuthenticator.php
... lines 1 - 13
class FormLoginAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 55
protected function getLoginUrl()
{
return $this->container->get('router')
->generate('security_login');
}
... lines 61 - 66
}

In our case, the login page route name is security_login.

Customize!

Try it out! You should be able to login, see login errors, and control most of the process. So what else can we customize?

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?