From 8ed09166a92302078a684c4c6d9e2d0dd06e3ef7 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sun, 11 Oct 2020 13:47:27 +0200 Subject: [PATCH] Added login link documentation --- _images/security/login_link_email.png | Bin 0 -> 4301 bytes security/experimental_authenticators.rst | 2 + security/login_link.rst | 652 +++++++++++++++++++++++ 3 files changed, 654 insertions(+) create mode 100644 _images/security/login_link_email.png create mode 100644 security/login_link.rst diff --git a/_images/security/login_link_email.png b/_images/security/login_link_email.png new file mode 100644 index 0000000000000000000000000000000000000000..8331b878f68376279fc5f672eb3e3826761b5202 GIT binary patch literal 4301 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 +in your configuration to use this feature. + +Using the Login Link Authenticator +---------------------------------- + +This guide assumes you have setup security and have created a user object +in your application. Follow :doc:`the main security guide ` if +this is not yet the case. + +1) Configure the Login Link Authenticator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The login link authenticator is configured using the ``login_link`` option +under the firewall. You must configure a ``check_route`` and +``signature_properties`` when enabling this authenticator: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + signature_properties: ['id'] + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + ], + ], + ], + ]); + +The ``signature_properties`` are used to create a signed URL. This must +contain at least one property of your ``User`` object that uniquely +identifies this user (e.g. the user ID). Read more about this setting +:ref:`further down below `. + +The ``check_route`` must be an existing route and it will be used to +generate the login link that will authenticate the user. You don't need a +controller (or it can be empty) because the login link authenticator will +intercept requests to this route: + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login_check", name="login_check") + */ + public function check() + { + throw new \LogicException('This code should never be reached'); + } + } + + .. code-block:: yaml + + # config/routes.yaml + + # ... + login_check: + path: /login_check + + .. code-block:: xml + + + + + + + + + + .. code-block:: php + + // config/routes.php + use App\Controller\DefaultController; + use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; + + return function (RoutingConfigurator $routes) { + // ... + $routes->add('login_check', '/login_check'); + }; + +2) Generate the Login Link +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that the authenticator is able to check the login links, you must +create a page where a user can request a login link and log in to your +website. + +The login link can be generated using the +:class:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandlerInterface`. +The correct login link handler is autowired for you when type-hinting for +this interface:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use App\Repository\UserRepository; + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login") + */ + public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + { + // check if login form is submitted + if ($request->isMethod('POST')) { + // load the user in some way (e.g. using the form input) + $email = $request->request->get('email'); + $user = $userRepository->findOneBy(['email' => $email]); + + // create a login link for $user this returns an instance + // of LoginLinkDetails + $loginLinkDetails = $loginLinkHandler->createLoginLink($user); + $loginLink = $loginLinkDetails->getUrl(); + + // ... send the link and return a response (see next section) + } + + // if it's not submitted, render the "login" form + return $this->render('security/login.html.twig'); + } + + // ... + } + +.. code-block:: html+twig + + {# templates/security/login.html.twig #} + {% extends 'base.html.twig' %} + + {% block body %} +
+ + +
+ {% endblock %} + +In this controller, the user is submitting their e-mail address to the +controller. Based on this property, the correct user is loaded and a login +link is created using +:method:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkHandlerInterface::createLoginLink`. + +.. caution:: + + It is important to send this link to the user and **not show it directly**, + as that would allow anyone to login. For instance, use the + :doc:`mailer ` component to mail the login link to the user. + Or use the component to send an SMS to the + user's device. + +3) Send the Login Link to the User +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now the link is created, it needs to be send to the user. Anyone with the +link is able to login as this user, so you need to make sure to send it to +a known device of them (e.g. using e-mail or SMS). + +You can send the link using any library or method. However the login link +authenticator provides integration with the :doc:`Notifier component `. +Use the special :class:`Symfony\\Component\\Security\\Http\\LoginLink\\LoginLinkNotification` +to create a notification and send it to the user's email address or phone +number:: + + // src/Controller/SecurityController.php + + // ... + use Symfony\Component\Notifier\NotifierInterface; + use Symfony\Component\Notifier\Recipient\Recipient; + use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login") + */ + public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request) + { + if ($request->isMethod('POST')) { + $email = $request->request->get('email'); + $user = $userRepository->findOneBy(['email' => $email]); + + $loginLinkDetails = $loginLinkHandler->createLoginLink($user); + + // create a notification based on the login link details + $notification = new LoginLinkNotification( + $loginLinkDetails, + 'Welcome to MY WEBSITE!' // email subject + ); + // create a recipient for this user + $recipient = (new Recipient())->email($user->getEmail()); + + // send the notification to the user + $notifier->send($notification, $recipient); + + // render a "Login link is sent!" page + return $this->render('security/login_link_sent.html.twig'); + } + + return $this->render('security/login.html.twig'); + } + + // ... + } + +.. note:: + + This integration requires the :doc:`Notifier ` and + :doc:`Mailer ` components to be installed and configured. + Install all required packages using: + + .. code-block:: terminal + + $ composer require symfony/mailer symfony/notifier \ + symfony/twig-bundle twig/extra-bundle \ + twig/cssinliner-extra twig/inky-extra + +This will send an email like this to the user: + +.. image:: /_images/security/login_link_email.png + :align: center + +.. tip:: + + You can customize this e-mail template by extending the + ``LoginLinkNotification`` and configuring another ``htmlTemplate``:: + + // src/Notifier/CustomLoginLinkNotification + namespace App\Notifier; + + use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification; + + class CustomLoginLinkNotification extends LoginLinkNotification + { + public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage + { + $emailMessage = parent::asEmailMessage($recipient, $transport); + + // get the NotificationEmail object and override the template + $email = $emailMessage->getMessage(); + $email->htmlTemplate('emails/custom_login_link_email.html.twig'); + + return $emailMessage; + } + } + + Then, use this new ``CustomLoginLinkNotification`` in the controller + instead. + +Important Considerations +------------------------ + +Login links are a convenient way of authenticating users, but it is also +considered less secure than a traditional username and password form. It is +not recommended to use login links in security critical applications. + +However, the implementation in Symfony does have a couple extension points +to make the login links more secure. In this section, the most important +configuration decisions are discussed: + +* `Limit Login Link Lifetime`_ +* `Invalidate Login Links`_ +* `Only allow a Link to be used Once`_ + +Limit Login Link Lifetime +~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is important for login links to have a limited lifetime. This reduces +the risk that someone can intercept the link and use it to login as +somebody else. By default, Symfony defines a lifetime of 10 minutes (600 +seconds). You can customize this using the ``lifetime`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + # lifetime in seconds + lifetime: 300 + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + // lifetime in seconds + 'lifetime' => 300, + ], + ], + ], + ]); + +.. _security-login-link-signature: + +Invalidate Login Links +~~~~~~~~~~~~~~~~~~~~~~ + +Symfony uses signed URLs to implement login links. The advantage of this is +that valid links do not have to be stored in a database. The signed URLs +allow Symfony to still invalidate already sent login links when important +information changes (e.g. a user's email address). + +The signed URL contains 3 parameters: + +``expires`` + The UNIX timestamp when the link expires. + +``user`` + The value returned from ``$user->getUsername()`` for this user. + +``hash`` + A hash of ``expires``, ``user`` and any configured signature + properties. Whenever these change, the hash changes and previous login + links are invalidated. + +You can add more properties to the ``hash`` by using the +``signature_properties`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + signature_properties: [id, email] + + .. code-block:: xml + + + + + + + + + id + email + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + 'signature_properties' => ['id', 'email'], + ], + ], + ], + ]); + +The properties are fetched from the user object using the +:doc:`PropertyAccess component ` (e.g. using +``getEmail()`` or a public ``$email`` property in this example). + +.. tip:: + + You can also use the signature properties to add very advanced + invalidating logic to your login links. For instance, if you store a + ``$lastLinkRequestedAt`` property on your users that you update in the + ``requestLoginLink()`` controller, you can invalidate all login links + whenever a user requests a new link. + +Configure a Maximum Use of a Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It is a common characteristic of login links to limit the number of times +it can be used. Symfony can support this by storing used login links in the +cache. Enable this support by setting the ``max_uses`` option: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + # only allow the link to be used 3 times + max_uses: 3 + + # optionally, configure the cache pool + #used_link_cache: 'cache.redis' + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + // only allow the link to be used 3 times + 'max_uses' => 3, + + // optionally, configure the cache pool + //'used_link_cache' => 'cache.redis', + ], + ], + ], + ]); + +Make sure there is enough space left in the cache, otherwise invalid links +can no longer be stored (and thus become valid again). Expired invalid +links are automatically removed from the cache. + +The cache pools are not cleared by the ``cache:clear`` command, but +removing ``var/cache/`` manually may remove the cache if the cache +component is configured to store its cache in that location. Read the +:doc:`/cache` guide for more information. + +Allow a Link to only be Used Once +................................. + +When setting ``max_uses`` to ``1``, you must take extra precautions to +make it work as expected. Email providers and browsers often load a +preview of the links, meaning that the link is already invalidated by +the preview loader. + +In order to solve this issue, first set the ``check_post_only`` option let +the authenticator only handle HTTP POST methods: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + firewalls: + main: + login_link: + check_route: login_check + check_post_only: true + max_uses: 1 + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + 'login_link' => [ + 'check_route' => 'login_check', + 'check_post_only' => true, + 'max_uses' => 1, + ], + ], + ], + ]); + +Then, use the ``check_route`` controller to render a page that lets the +user create this POST request (e.g. by clicking a button):: + + // src/Controller/SecurityController.php + namespace App\Controller; + + // ... + use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + + class SecurityController extends AbstractController + { + /** + * @Route("/login_check", name="login_check") + */ + public function check() + { + // get the login link query parameters + $expires = $request->query->get('expires'); + $username = $request->query->get('user'); + $hash = $request->query->get('hash'); + + // and render a template with the button + return $this->render('security/process_login_link.html.twig', [ + 'expires' => $expires, + 'user' => $username, + 'hash' => $hash, + ]); + } + } + +.. code-block:: html+twig + + {# templates/security/process_login_link.html.twig #} + {% extends 'base.html.twig' %} + + {% block body %} +

Hi! You are about to login to ...

+ + +
+ + + + + +
+ {% endblock %}