New Security's component
Symfony 6 will get a new or maybe I should say an update of the Security Component. New feature will come with it and good news we can already use it 😊
Login Link
As says the doc, "the login link also called “magic link”, is a passwordless authentication mechanism. Whenever a user wants to login, a new link is generated and sent to them (e.g. using an email). The link fully authenticates the user in the application when clicking on it."
Let's start
I have a new symfony's project and I've already configurated the authentication. Now to use the login link feature, I have to enable the authenticator manager in my security.yaml and disable the anonymous in my firewall
security:
enable_authenticator_manager: true
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
#anonymous: true
Now I can tell to Symfony I want to use the login link. In my firewall so I add it :
login_link:
check_route: app_login_check
signature_properties: [id]
lifetime: 300
- Check_route is the name of the route that Symfony need to generate the login link to authenticate the user.
- 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
- Lifetime is the link's lifetime in seconds. Here I said 5 minutes
Then in the SecurityController, I write a new function called "check" with the route "/login-check". This function can be empty
/**
* @Route("/login-check", name="app_login_check")
*/
public function check()
{
throw new \LogicException('This code should never be reached');
}
Now I create the controller which will send the login link to the user
/**
* @Route("/forgotten-password", name="app_login_link")
*/
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]);
if ($user === null){
$this->addFlash('danger', 'This email does not exist ');
return $this->redirectToRoute('app_login_link');;
}
$loginLinkDetails = $loginLinkHandler->createLoginLink($user);
// create a notification based on the login link details
$notification = new CustomLoginLinkNotification(
$loginLinkDetails,
'Link to Connect' // email subject
);
// create a recipient for this user
$recipient = new Recipient($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', [
'user_email' => $user->getEmail()
]);
}
return $this->render('security/login_link_form.html.twig');
}
First I check if the method is "POST" then I get the email. If the user is not in my database then I redirect him in the same page with error message, here it's a flashbag
if ($user === null){
$this->addFlash('danger', 'This mail does not exist ');
return $this->redirectToRoute('app_login_link');;
}
I create the login link $loginLinkDetails = $loginLinkHandler->createLoginLink($user);
and the notification. In my case I use a CustomLoginLinkNotification
but if you don't want you can use the LoginLinkNotification
.
Then I create the recipient for the user and I send the notification
// create a recipient for this user
$recipient = new Recipient($user->getEmail());
// send the notification to the user
$notifier->send($notification, $recipient);
When the link is send, a new template appear with a custom message. You can write what you want, here I wrote "The link is send to your email address : {{ user_email }}
return $this->render('security/login_link_sent.html.twig', [
'user_email' => $user->getEmail()
]);
For the email to be correctly send , I need to install :
- Mailer
- twig/cssinliner-extra
- twig/inky-extra
I use a full project symfony and if you don't, you will need to instal notifier and twig
Now I create a new Notifier's folder in 'App' and I create my new class : CustomLoginLinkNotification
<?php
namespace App\Notifier;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
use Symfony\Component\Security\Http\LoginLink\LoginLinkDetails;
use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification;
class CustomLoginLinkNotification extends LoginLinkNotification
{
private LoginLinkDetails $loginLinkDetails;
public function __construct(LoginLinkDetails $loginLinkDetails, string $subject, array $channels = [])
{
parent::__construct($loginLinkDetails, $subject, $channels);
$this->loginLinkDetails = $loginLinkDetails;
}
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->from('admin@example.com');
$email->content($this->getLeftTime());
$email->htmlTemplate('email/_custom_login_link_email.html.twig');
return $emailMessage;
}
public function getLeftTime(): string
{
$duration = $this->loginLinkDetails->getExpiresAt()->getTimestamp() - time();
$durationString = floor($duration / 60).' minute'.($duration > 60 ? 's' : '');
if (($hours = $duration / 3600) >= 1) {
$durationString = floor($hours).' hour'.($hours >= 2 ?'s' : '');
}
return $durationString;
}
}
Here I extends the LoginLinkNotification
and I override the function asEmailMessage
to send a custom mail. The function getLeftTime
is the code from the doc to get the lifetime of the link and to be able to send it in the email. My email look like this :
<p>Click on the link below to connect, it expires in {{ content }} </p>
<a href="{{ action_url }}">Connect</a>
When the link is send, the user can click on the button and he will be connect. In my case when an user is connect (after login) he is redirect to the page /{user} and it's the same thing when he clicks on the link. I like this so I will not change this behaviour but if you want to do something else like a new route or maybe persist some information in your database, you can create a success_handler. You could find how to do it in the doc.
If the link is expires and the user click on it, by default he will be redirect to "/login". I don't want that because my route to the login is '/'. I will create then a failure_handler.
First in the security.yaml I need to tell to Symfony I want to handle the failure myself :
login_link:
check_route: app_login_check
signature_properties: [id]
lifetime: 300
failure_handler: App\Security\Authentication\AuthenticationFailureHandler
Next I create my failure's handler :
namespace App\Security\Authentication;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
private UrlGeneratorInterface $urlGenerator;
private FlashBagInterface $flash;
public function __construct(UrlGeneratorInterface $urlGenerator, FlashBagInterface $flash)
{
$this->urlGenerator = $urlGenerator;
$this->flash = $flash;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$this->flash->add('danger','this link is expires 😮😮😮');
return new RedirectResponse($this->urlGenerator->generate('app_login'));
}
}
The goal of this handler is to redirect the user to the route I want in case the link is expires with a flash message error.
That's it guys !
Of course I didn't show you everything, there are differents custom you can do. For me this feature is a better way for the users who has forgotten their password
There are new features to explore with the "new" Security component like the Login Throttling for example but it will be for an other time...
Top comments (1)
Thanks for nice article! How can I add "rememberme" functionality for login by link logic? Thanks!