You are browsing the Symfony 4 documentation, which changes significantly from Symfony 3.x. If your app doesn't use Symfony 4 yet, browse the Symfony 3.4 documentation.

How to Build a Login Form

4.2 version
Maintained Unmaintained

How to Build a Login Form

If you're looking for the form_login firewall option, see Using the form_login Authentication Provider.

Ready to create a login form? First, make sure you've followed the main Security Guide to install security and create your User class.

Generating the Login Form

Creating a powerful login form is easy thanks to the make:auth command from MakerBundle. Depending on your setup, you may be asked different questions and your generated code may be slightly different:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ php bin/console make:auth

What style of authentication do you want? [Empty authenticator]:
 [0] Empty authenticator
 [1] Login form authenticator
> 1

The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> LoginFormAuthenticator

Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
> SecurityController

 created: src/Security/LoginFormAuthenticator.php
 updated: config/packages/security.yaml
 created: src/Controller/SecurityController.php
 created: templates/security/login.html.twig

New in version 1.8: Support for login form authentication was added to make:auth in MakerBundle 1.8.

This generates three things: (1) a login route & controller, (2) a template that renders the login form and (3) a Guard authenticator class that processes the login submit.

The /login route & controller:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Controller/SecurityController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login", name="app_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // 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', [
            'last_username' => $lastUsername,
            'error' => $error
        ]);
    }
}

The template has very little to do with security: it just generates a traditional HTML form that submits to /login:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{% extends 'base.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
<form method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail" class="sr-only">Email</label>
    <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email" required autofocus>
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>

    <input type="hidden" name="_csrf_token"
           value="{{ csrf_token('authenticate') }}"
    >

    {#
        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
        See https://symfony.com/doc/current/security/remember_me.html

        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" name="_remember_me"> Remember me
            </label>
        </div>
    #}

    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}

The Guard authenticator processes the form submit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// src/Security/LoginFormAuthenticator.php
namespace App\Security;

use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
use Symfony\Component\Security\Http\Util\TargetPathTrait;

class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
    use TargetPathTrait;

    private $entityManager;
    private $router;
    private $csrfTokenManager;
    private $passwordEncoder;

    public function __construct(EntityManagerInterface $entityManager, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
    {
        $this->entityManager = $entityManager;
        $this->router = $router;
        $this->csrfTokenManager = $csrfTokenManager;
        $this->passwordEncoder = $passwordEncoder;
    }

    public function supports(Request $request)
    {
        return 'app_login' === $request->attributes->get('_route')
            && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
        $credentials = [
            'email' => $request->request->get('email'),
            'password' => $request->request->get('password'),
            'csrf_token' => $request->request->get('_csrf_token'),
        ];
        $request->getSession()->set(
            Security::LAST_USERNAME,
            $credentials['email']
        );

        return $credentials;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        $token = new CsrfToken('authenticate', $credentials['csrf_token']);
        if (!$this->csrfTokenManager->isTokenValid($token)) {
            throw new InvalidCsrfTokenException();
        }

        $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);

        if (!$user) {
            // fail authentication with a custom error
            throw new CustomUserMessageAuthenticationException('Email could not be found.');
        }

        return $user;
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
            return new RedirectResponse($targetPath);
        }

        // For example : return new RedirectResponse($this->router->generate('some_route'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

    protected function getLoginUrl()
    {
        return $this->router->generate('app_login');
    }
}

Finishing the Login Form

Woh. The make:auth command just did a lot of work for you. But, you're not done yet. First, go to /login to see the new login form. Feel free to customize this however you want.

When you submit the form, the LoginFormAuthenticator will intercept the request, read the email (or whatever field you're using) & password from the form, find the User object, validate the CSRF token and check the password.

But, depending on your setup, you'll need to finish one or more TODOs before the whole process works. You will at least need to fill in where you want your user to be redirected after success:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// src/Security/LoginFormAuthenticator.php

// ...
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
    // ...

-     throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
+     // redirect to some "app_homepage" route - of wherever you want
+     return new RedirectResponse($this->router->generate('app_homepage'));
}

Unless you have any other TODOs in that file, that's it! If you're loading users from the database, make sure you've loaded some dummy users. Then, try to login.

If you're successful, the web debug toolbar will tell you who you are and what roles you have:

../_images/symfony_loggedin_wdt.png

The Guard authentication system is powerful, and you can customize your authenticator class to do whatever you need. To learn more about what the individual methods do, see Custom Authentication System with Guard (API Token Example).

Controlling Error Messages

You can cause authentication to fail with a custom message at any step by throwing a custom CustomUserMessageAuthenticationException. This is an easy way to control the error message.

But in some cases, like if you return false from checkCredentials(), you may see an error that comes from the core of Symfony - like Invalid credentials..

To customize this message, you could throw a CustomUserMessageAuthenticationException instead. Or, you can translate the message through the security domain:

  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <!-- translations/security.en.xlf -->
    <?xml version="1.0"?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
        <file source-language="en" datatype="plaintext" original="file.ext">
            <body>
                <trans-unit id="Invalid credentials.">
                    <source>Invalid credentials.</source>
                    <target>The password you entered was invalid!</target>
                </trans-unit>
            </body>
        </file>
    </xliff>
    
  • YAML
    1
    2
    # translations/security.en.yaml
    'Invalid credentials.': 'The password you entered was invalid!'
    
  • PHP
    1
    2
    3
    4
    // translations/security.en.php
    return array(
        'Invalid credentials.' => 'The password you entered was invalid!',
    );
    

If the message isn't translated, make sure you've installed the translator and try clearing your cache:

1
$ php bin/console cache:clear

Redirecting to the Last Accessed Page with TargetPathTrait

The last request URI is stored in a session variable named _security.<your providerKey>.target_path (e.g. _security.main.target_path if the name of your firewall is main). Most of the times you don't have to deal with this low level session variable. However, the TargetPathTrait utility can be used to read (like in the example above) or set this value manually.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.