How to Build a Login Form

How to Build a Login Form

See also

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 can be bootstrapped with 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. $ php bin/console make:auth
  2. What style of authentication do you want? [Empty authenticator]:
  3. [0] Empty authenticator
  4. [1] Login form authenticator
  5. > 1
  6. The class name of the authenticator to create (e.g. AppCustomAuthenticator):
  7. > LoginFormAuthenticator
  8. Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
  9. > SecurityController
  10. Do you want to generate a '/logout' URL? (yes/no) [yes]:
  11. > yes
  12. created: src/Security/LoginFormAuthenticator.php
  13. updated: config/packages/security.yaml
  14. created: src/Controller/SecurityController.php
  15. 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 the following: 1) login/logout routes & controller, 2) a template that renders the login form, 3) a Guard authenticator class that processes the login submit and 4) updates the main security config file.

Step 1. The /login//logout routes & controller:

  1. // src/Controller/SecurityController.php
  2. namespace App\Controller;
  3. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  4. use Symfony\Component\HttpFoundation\Response;
  5. use Symfony\Component\Routing\Annotation\Route;
  6. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  7. class SecurityController extends AbstractController
  8. {
  9. /**
  10. * @Route("/login", name="app_login")
  11. */
  12. public function login(AuthenticationUtils $authenticationUtils): Response
  13. {
  14. // if ($this->getUser()) {
  15. // return $this->redirectToRoute('target_path');
  16. // }
  17. // get the login error if there is one
  18. $error = $authenticationUtils->getLastAuthenticationError();
  19. // last username entered by the user
  20. $lastUsername = $authenticationUtils->getLastUsername();
  21. return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
  22. }
  23. /**
  24. * @Route("/logout", name="app_logout")
  25. */
  26. public function logout(): void
  27. {
  28. throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
  29. }
  30. }

Edit the security.yaml file in order to declare the /logout path:

  • YAML

    1. # config/packages/security.yaml
    2. security:
    3. # ...
    4. firewalls:
    5. main:
    6. # ...
    7. logout:
    8. path: app_logout
    9. # where to redirect after logout
    10. # target: app_any_route
  • XML

    1. <!-- config/packages/security.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <srv:container xmlns="http://symfony.com/schema/dic/security"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:srv="http://symfony.com/schema/dic/services"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd">
    8. <config>
    9. <!-- ... -->
    10. <firewall name="main">
    11. <!-- ... -->
    12. <logout path="app_logout"/>
    13. </firewall>
    14. </config>
    15. </srv:container>
  • PHP

    1. // config/packages/security.php
    2. $container->loadFromExtension('security', [
    3. // ...
    4. 'firewalls' => [
    5. 'main' => [
    6. // ...
    7. 'logout' => [
    8. 'path' => 'app_logout',
    9. // where to redirect after logout
    10. 'target' => 'app_any_route'
    11. ],
    12. ],
    13. ],
    14. ]);

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

  1. {% extends 'base.html.twig' %}
  2. {% block title %}Log in!{% endblock %}
  3. {% block body %}
  4. <form method="post">
  5. {% if error %}
  6. <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
  7. {% endif %}
  8. {% if app.user %}
  9. <div class="mb-3">
  10. You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
  11. </div>
  12. {% endif %}
  13. <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
  14. <label for="inputEmail">Email</label>
  15. <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus>
  16. <label for="inputPassword">Password</label>
  17. <input type="password" name="password" id="inputPassword" class="form-control" required>
  18. <input type="hidden" name="_csrf_token"
  19. value="{{ csrf_token('authenticate') }}"
  20. >
  21. {#
  22. Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
  23. See https://symfony.com/doc/current/security/remember_me.html
  24. <div class="checkbox mb-3">
  25. <label>
  26. <input type="checkbox" name="_remember_me"> Remember me
  27. </label>
  28. </div>
  29. #}
  30. <button class="btn btn-lg btn-primary" type="submit">
  31. Sign in
  32. </button>
  33. </form>
  34. {% endblock %}

Step 3. The Guard authenticator processes the form submit:

  1. // src/Security/LoginFormAuthenticator.php
  2. namespace App\Security;
  3. use App\Entity\User;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Symfony\Component\HttpFoundation\RedirectResponse;
  6. use Symfony\Component\HttpFoundation\Request;
  7. use Symfony\Component\HttpFoundation\Response;
  8. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  9. use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
  10. use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
  11. use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
  12. use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException;
  13. use Symfony\Component\Security\Core\Security;
  14. use Symfony\Component\Security\Core\User\UserInterface;
  15. use Symfony\Component\Security\Core\User\UserProviderInterface;
  16. use Symfony\Component\Security\Csrf\CsrfToken;
  17. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  18. use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;
  19. use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface;
  20. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  21. class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface
  22. {
  23. use TargetPathTrait;
  24. public const LOGIN_ROUTE = 'app_login';
  25. private $entityManager;
  26. private $urlGenerator;
  27. private $csrfTokenManager;
  28. private $passwordEncoder;
  29. public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder)
  30. {
  31. $this->entityManager = $entityManager;
  32. $this->urlGenerator = $urlGenerator;
  33. $this->csrfTokenManager = $csrfTokenManager;
  34. $this->passwordEncoder = $passwordEncoder;
  35. }
  36. public function supports(Request $request): bool
  37. {
  38. return self::LOGIN_ROUTE === $request->attributes->get('_route')
  39. && $request->isMethod('POST');
  40. }
  41. public function getCredentials(Request $request)
  42. {
  43. $credentials = [
  44. 'email' => $request->request->get('email'),
  45. 'password' => $request->request->get('password'),
  46. 'csrf_token' => $request->request->get('_csrf_token'),
  47. ];
  48. $request->getSession()->set(
  49. Security::LAST_USERNAME,
  50. $credentials['email']
  51. );
  52. return $credentials;
  53. }
  54. public function getUser($credentials, UserProviderInterface $userProvider): ?User
  55. {
  56. $token = new CsrfToken('authenticate', $credentials['csrf_token']);
  57. if (!$this->csrfTokenManager->isTokenValid($token)) {
  58. throw new InvalidCsrfTokenException();
  59. }
  60. $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]);
  61. if (!$user) {
  62. // fail authentication with a custom error
  63. throw new CustomUserMessageAuthenticationException('Email could not be found.');
  64. }
  65. return $user;
  66. }
  67. public function checkCredentials($credentials, UserInterface $user): bool
  68. {
  69. return $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
  70. }
  71. /**
  72. * Used to upgrade (rehash) the user's password automatically over time.
  73. */
  74. public function getPassword($credentials): ?string
  75. {
  76. return $credentials['password'];
  77. }
  78. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
  79. {
  80. if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
  81. return new RedirectResponse($targetPath);
  82. }
  83. // For example : return new RedirectResponse($this->urlGenerator->generate('some_route'));
  84. throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
  85. }
  86. protected function getLoginUrl(): string
  87. {
  88. return $this->urlGenerator->generate(self::LOGIN_ROUTE);
  89. }
  90. }

Step 4. Updates the main security config file to enable the Guard authenticator and configure logout route:

  • YAML

    1. # config/packages/security.yaml
    2. security:
    3. # ...
    4. firewalls:
    5. main:
    6. # ...
    7. guard:
    8. authenticators:
    9. - App\Security\LoginFormAuthenticator
    10. logout:
    11. path: app_logout
  • XML

    1. <!-- config/packages/security.xml -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <srv:container xmlns="http://symfony.com/schema/dic/security"
    4. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    5. xmlns:srv="http://symfony.com/schema/dic/services"
    6. xsi:schemaLocation="http://symfony.com/schema/dic/services
    7. https://symfony.com/schema/dic/services/services-1.0.xsd">
    8. <config>
    9. <!-- ... -->
    10. <firewall name="main">
    11. <!-- ... -->
    12. <guard>
    13. <authenticator class="App\Security\LoginFormAuthenticator"/>
    14. </guard>
    15. <logout path="app_logout"/>
    16. </firewall>
    17. </config>
    18. </srv:container>
  • PHP

    1. // config/packages/security.php
    2. use App\Security\LoginFormAuthenticator;
    3. $container->loadFromExtension('security', [
    4. // ...
    5. 'firewalls' => [
    6. 'main' => [
    7. // ...,
    8. 'guard' => [
    9. 'authenticators' => [
    10. LoginFormAuthenticator::class,
    11. ]
    12. ],
    13. 'logout' => [
    14. 'path' => 'app_logout',
    15. ],
    16. ],
    17. ],
    18. ]);

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. // src/Security/LoginFormAuthenticator.php
  2. // ...
  3. public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): Response
  4. {
  5. // ...
  6. - throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
  7. + // redirect to some "app_homepage" route - of wherever you want
  8. + return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
  9. }

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 Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException. 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. <!-- translations/security.en.xlf -->
    2. <?xml version="1.0" encoding="UTF-8" ?>
    3. <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
    4. <file source-language="en" datatype="plaintext" original="file.ext">
    5. <body>
    6. <trans-unit id="Invalid credentials.">
    7. <source>Invalid credentials.</source>
    8. <target>The password you entered was invalid!</target>
    9. </trans-unit>
    10. </body>
    11. </file>
    12. </xliff>
  • YAML

    1. # translations/security.en.yaml
    2. 'Invalid credentials.': 'The password you entered was invalid!'
  • PHP

    1. // translations/security.en.php
    2. return [
    3. 'Invalid credentials.' => 'The password you entered was invalid!',
    4. ];

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 Symfony\Component\Security\Http\Util\TargetPathTrait utility can be used to read (like in the example above) or set this value manually.

When the user tries to access a restricted page, they are being redirected to the login page. At that point target path will be set. After a successful login, the user will be redirected to this previously set target path.

If you also want to apply this behavior to public pages, you can create an event subscriber to set the target path manually whenever the user browses a page:

  1. // src/EventSubscriber/RequestSubscriber.php
  2. namespace App\EventSubscriber;
  3. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  4. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  5. use Symfony\Component\HttpKernel\Event\RequestEvent;
  6. use Symfony\Component\HttpKernel\KernelEvents;
  7. use Symfony\Component\Security\Http\Util\TargetPathTrait;
  8. class RequestSubscriber implements EventSubscriberInterface
  9. {
  10. use TargetPathTrait;
  11. private $session;
  12. public function __construct(SessionInterface $session)
  13. {
  14. $this->session = $session;
  15. }
  16. public function onKernelRequest(RequestEvent $event): void
  17. {
  18. $request = $event->getRequest();
  19. if (
  20. !$event->isMasterRequest()
  21. || $request->isXmlHttpRequest()
  22. || 'app_login' === $request->attributes->get('_route')
  23. ) {
  24. return;
  25. }
  26. $this->saveTargetPath($this->session, 'main', $request->getUri());
  27. }
  28. public static function getSubscribedEvents(): array
  29. {
  30. return [
  31. KernelEvents::REQUEST => ['onKernelRequest']
  32. ];
  33. }
  34. }

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