AbstractLoginFormAuthenticator y redireccionamiento a la URL anterior
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeTengo que confesar algo: en nuestro autentificador, ¡hemos hecho demasiado trabajo! Sí, cuando construyes un autenticador personalizado para un "formulario de inicio de sesión", Symfony proporciona una clase base que puede hacer la vida mucho más fácil. En lugar de extender AbstractAuthenticator extiendeAbstractLoginFormAuthenticator:
| // ... lines 1 - 15 | |
| use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; | |
| // ... lines 17 - 25 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 28 - 95 | |
| } |
Mantén Command o Ctrl para abrir esa clase. Sí, extiende AbstractAuthenticatory también implementa AuthenticationEntryPointInterface. ¡Genial! Eso significa que podemos eliminar nuestro redundante AuthenticationEntryPointInterface:
| // ... lines 1 - 23 | |
| use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 28 - 95 | |
| } |
La clase abstracta requiere que añadamos un nuevo método llamado getLoginUrl(). Dirígete a la parte inferior de esta clase y ve a "Código"->"Generar" -o Command+N en un Mac- y luego a "Implementar métodos" para generar getLoginUrl(). Para el interior, roba el código de arriba... y devuelve $this->router->generate('app_login'):
| // ... lines 1 - 25 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 28 - 91 | |
| protected function getLoginUrl(Request $request): string | |
| { | |
| return $this->router->generate('app_login'); | |
| } | |
| } |
La utilidad de esta clase base es bastante fácil de ver: ¡implementa tres de los métodos por nosotros! Por ejemplo, implementa supports() comprobando si el método es POST y si la cadena getLoginUrl() coincide con la URL actual. En otras palabras, hace exactamente lo mismo que nuestro método supports(). También gestionaonAuthenticationFailure() -almacenando el error en la sesión y redirigiendo de nuevo a la página de inicio de sesión- y también el punto de entrada - start() - redirigiendo, una vez más, a /login.
Esto significa que... oh sí... ¡podemos eliminar código! Veamos: eliminar supports(),onAuthenticationFailure() y también start():
| // ... lines 1 - 25 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 28 - 36 | |
| public function supports(Request $request): ?bool | |
| { | |
| return ($request->getPathInfo() === '/login' && $request->isMethod('POST')); | |
| } | |
| // ... lines 41 - 75 | |
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |
| { | |
| $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); | |
| return new RedirectResponse( | |
| $this->router->generate('app_login') | |
| ); | |
| } | |
| public function start(Request $request, AuthenticationException $authException = null): Response | |
| { | |
| return new RedirectResponse( | |
| $this->router->generate('app_login') | |
| ); | |
| } | |
| // ... lines 91 - 95 | |
| } |
Mucho más bonito:
| // ... lines 1 - 25 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| private UserRepository $userRepository; | |
| private RouterInterface $router; | |
| public function __construct(UserRepository $userRepository, RouterInterface $router) | |
| { | |
| // ... lines 33 - 34 | |
| } | |
| public function authenticate(Request $request): PassportInterface | |
| { | |
| // ... lines 39 - 61 | |
| } | |
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| { | |
| // ... lines 66 - 68 | |
| } | |
| protected function getLoginUrl(Request $request): string | |
| { | |
| // ... line 73 | |
| } | |
| } |
Asegurémonos de que no rompemos nada: vamos a /admin y... ¡perfecto! El método start() nos sigue redirigiendo a /login. Entremos conabraca_admin@example.com, contraseña tada y... ¡sí! Eso también sigue funcionando: nos redirige a la página de inicio... porque eso es lo que estamos haciendo dentro deonAuthenticationSuccess:
| // ... lines 1 - 25 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 28 - 63 | |
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| { | |
| return new RedirectResponse( | |
| $this->router->generate('app_homepage') | |
| ); | |
| } | |
| // ... lines 70 - 74 | |
| } |
TargetPathTrait: Redirección inteligente
Pero... si lo piensas... eso no es lo ideal. Ya que en un principio intentaba ir a /admin... ¿no debería el sistema ser lo suficientemente inteligente como para redirigirnos de nuevo allí después de que hayamos entrado con éxito? Sí Y eso es totalmente posible.
Vuelve a cerrar la sesión. Cuando un usuario anónimo intenta acceder a una página protegida como /admin, justo antes de llamar a la función del punto de entrada, Symfony almacena la URL actual en algún lugar de la sesión. Gracias a esto, en onAuthenticationSuccess(), podemos leer esa URL -que se denomina "ruta de destino"- y redirigirla allí.
Para ayudarnos a hacer esto, ¡podemos aprovechar un trait! En la parte superior de la claseuse TargetPathTrait:
| // ... lines 1 - 24 | |
| use Symfony\Component\Security\Http\Util\TargetPathTrait; | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| use TargetPathTrait; | |
| // ... lines 30 - 81 | |
| } |
Luego, abajo, en onAuthenticationSuccess(), podemos comprobar si se ha almacenado una "ruta de destino" en la sesión. Lo hacemos diciendo si$target = $this->getTargetPath() - pasando la sesión -$request->getSession() - y luego el nombre del cortafuegos, que en realidad tenemos como argumento. Esa es la clave main:
| // ... lines 1 - 26 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 29 - 66 | |
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| { | |
| if ($target = $this->getTargetPath($request->getSession(), $firewallName)) { | |
| // ... line 70 | |
| } | |
| return new RedirectResponse( | |
| $this->router->generate('app_homepage') | |
| ); | |
| } | |
| // ... lines 77 - 81 | |
| } |
Esta línea hace dos cosas a la vez: establece una variable $target a la ruta de destino y, en la sentencia if, comprueba si ésta tiene algo. Porque, si el usuario va directamente a la página de inicio de sesión, entonces no tendrá una ruta de destino en la sesión.
Así que, si tenemos una ruta de destino, redirige a ella: return new RedirectResponse($target):
| // ... lines 1 - 26 | |
| class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
| { | |
| // ... lines 29 - 66 | |
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
| { | |
| if ($target = $this->getTargetPath($request->getSession(), $firewallName)) { | |
| return new RedirectResponse($target); | |
| } | |
| return new RedirectResponse( | |
| $this->router->generate('app_homepage') | |
| ); | |
| } | |
| // ... lines 77 - 81 | |
| } |
¡Hecho y listo! Si mantienes Command o Ctrl y haces clic en getTargetPath() para saltar a ese método central, puedes ver que es súper sencillo: sólo lee una clave muy específica de la sesión. Esta es la clave que el sistema de seguridad establece cuando un usuario anónimo intenta acceder a una página protegida.
¡Vamos a probar esto! Ya hemos cerrado la sesión. Dirígete a /admin. Nuestro punto de entrada nos redirige a /login. Pero además, entre bastidores, Symfony acaba de fijar la URL/admin en esa clave de la sesión. Así que cuando nos conectamos ahora con nuestro correo electrónico y contraseña habituales... ¡impresionante! ¡Se nos redirige de nuevo a /admin!
Siguiente: um... seguimos haciendo demasiado trabajo en LoginFormAuthenticator. ¡Maldita sea! Resulta que, a menos que necesitemos algunas cosas especialmente personalizadas, si estás construyendo un formulario de inicio de sesión, puedes omitir por completo la clase del autentificador personalizado y confiar en un autentificador central de Symfony.
12 Comments
Hello,
I kindly request a help about one error: I have a controller as below:
My purpose is, when user logged in, url should redirect his page. I coded as below to LoginFormAuthenticator.
However, I am getting below error. Could you please help me to why ı am getting this?
Hey Mahmut,
Are you sure the error you see is related to the code you showed above? :) Because that code looks good to me, probably you have another spot in your app where you generate
app_main_homepageroute without parameters? Please, double-check it. You can easily debug it withdd()before and after that route generation:Make sure you hit "before" spot first, then comment it out and make sure you hit the "after". Also, probably you generate route that shows the error on the redirected page? I would recommend you to search for
app_main_homepageglobally in your project to see if there are any spots where you forgot that mandatory parameter. If everything looks good - hm, maybe try to clear the cache just in case, sometimes it helps!Also, make sure that the
$usernameyou're passing togenerate()is not null or empty string :)Cheers!
hello Victor,
I found the problem, ı was missing the parameter to path ( app_main_homepage). I added and fixed.
Hey Mahmut,
Great! Easy fix :)
Very happy with all these walkthroughs as always. One question - I have the following authenticate() method with a flag on the user to block login via the form for certain users.
As it currently behaves, it will check whether this block flag is on or not BEFORE the password is checked. This has the side effect that you can find out whether a user exists or not even if you don't pass the correct password.
Where would I put this deny-flag-check in order to do it only once the password is successfully checked (but before the user is authenticated... I understand that I could do it in the onAuthenticationSuccess method, but I want it before the session is approved)
Hey MattWelander!
Happy new year! This is an excellent question. Take this logic out of your authenticator and instead, add it to an event subscriber on the
CheckPassportEvent::classevent. ThePasswordCredentialsthemselves are actually checked via a listener on this event: https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Security/Http/EventListener/CheckCredentialsListener.php - you can use it as a guide. To be sure that you're running AFTER the password check, you could set your priority to-1for this subscriber - so like this:Let me know if that works out! Btw, a side effect of this (which is probably good) is that if you have any other ways for a user to authenticate (including, iirc, via a "remember me cookie"), those will also be "subject" to this check. In other words, no matter how your user tries to log in, if they fail this check, they will fail authentication. If this is NOT what you want (and you only want this check to happen for this ONE authenticator), that's no problem. Do this:
A) Create a new badge class - e.g.
CheckDenyFormLoginBadge. It can basically be emptyB) Add this to your
PassportC) In your subscriber, only run the check if this badge is present.
Cheers!
Hello.
Great guide. it's really helping me a lot.
I am writing this comment because while I was trying this step "AbstractLoginFormAuthenticator & Redirecting to Previous URL", after all the changes the authentication process did not seem to take place.
the solution I found was to change the LoginFormAuthenticator class
adding the motodo
this
it returned false and did not make the magic work.
Hey Andrea Gelmini !
It look like you have a tiny error in your first code example. This is how we do the
supports()checkYou can notice we hardcode the "/login" string
Cheers!
I'm confused =)
At one stage in this script you say:
"This means that... oh yea... we can remove code! Let's see: delete supports(), onAuthenticationFailure() and also start():"
In the resulting example, the method supports() is completely erased. Like Andrea Gelmini says above, this causes the authenticator to go completely disconnect. the login form is no longer attached to an authenticator, submitting the form will only render the login form anew.
By your answer to Andrea above, it seems that you confirm that the supports() method should still be in the LoginFormAuthenticator? Was it a mistake to tell us to remove it alltogether and the script needs amending? Or is there something in mine and Andreas environment that is configured differently than in your environment, causing the supports() method from the AbstractLoginFormAuthenticator to go disconnect?
Hmm, I see what you mean. Let me clarify what's going on. I think you likely already know some of this, but just to clear everything up:
A) Every authenticator DOES need a
supports()method.B) But, when we extend
AbstractLoginFormAuthenticator, you can remove thesupports()method in your class, simply because it already exists in the parent class.So yes, I think Diego's comment was not quite right.
Now, to the real question:
Yes, possibly :). For some reason, the
supports()method that you're inheriting is returningfalsewhen it should returntrue. You could add some debugging code to that method to find out why, though I might have an idea: are you running your site at the root of a domain - e.g.http://127.0.0.1:8000is your homepage? Or is it under a subdirectory - e.g.http://127.0.0.1:8000/siteis the homepage? If it is the latter, there is a known bug in that inherited method that makes it not match correctly. In that case, you should keep yoursupports()method so it works. Wow, and apparently I opened that issue - lol - https://github.com/symfony/symfony/issues/44893 - and it's fixed in Symfony 5.4.13, 6.0.14, 6.1.6 and 6.2.0 and higher.Anyways, that was just a guess at the problem. If I'm wrong, I'd love to know what you find going wrong in that parent
supports()method!Cheers!
That was in fact a pretty good guess - I'm testing it off of http://localhost:8888/mysite/ 😂 thanks
Yay! Well then I'm double glad that this will, at least, be fixed in later versions!
Have fun!
"Houston: no signs of life"
Start the conversation!