Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Tengo 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.

Leave a comment!

8
Login or Register to join the conversation
MattWelander Avatar
MattWelander Avatar MattWelander | posted hace 1 mes

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)

public function authenticate(Request $request): PassportInterface
{

    $email = $request->request->get('email');
    $password = $request->request->get('password');

    return new Passport(
        new UserBadge($email, function($userIdentifier) {
            // optionally pass a callback to load the User manually
            $user = $this->userRepository->findOneBy(['email' => $userIdentifier]);

            if (!$user) {
                throw new UserNotFoundException();
            } elseif ($user->isDenyFormLogin() == true) { //checking for denyFlag on user
                throw new CustomUserMessageAuthenticationException('Form login denied for this user.');
            }

            return $user;
        }),
        new PasswordCredentials($password),
        [
            new CsrfTokenBadge(
                'authenticate',
                $request->request->get('_csrf_token')
            ),
            (new RememberMeBadge())->enable(),
        ]
    );
}
Reply

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::class event. The PasswordCredentials themselves 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 -1 for this subscriber - so like this:

public static function getSubscribedEvents(): array
{
    return [CheckPassportEvent::class => ['checkPassport', -1]];
}

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 empty
B) Add this to your Passport
C) In your subscriber, only run the check if this badge is present.

Cheers!

Reply
Default user avatar
Default user avatar Andrea Gelmini | posted hace 1 año | edited

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

    public function supports(Request $request): bool
    {
        return $request->isMethod('POST') && $this->getLoginUrl($request) === $request->getRequestUri();
    }

this

$this->getLoginUrl($request) === $request->getPathInfo()

it returned false and did not make the magic work.

Reply

Hey Andrea Gelmini !

It look like you have a tiny error in your first code example. This is how we do the `supports()` check


public function supports(Request $request): ?bool
{
return ($request->getPathInfo() === '/login' && $request->isMethod('POST'));
}

You can notice we hardcode the "/login" string

Cheers!

Reply
MattWelander Avatar
MattWelander Avatar MattWelander | MolloKhan | posted hace 1 mes

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?

Reply

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 the supports() 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:

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?

Yes, possibly :). For some reason, the supports() method that you're inheriting is returning false when it should return true. 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:8000 is your homepage? Or is it under a subdirectory - e.g. http://127.0.0.1:8000/site is 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 your supports() 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!

Reply
MattWelander Avatar

That was in fact a pretty good guess - I'm testing it off of http://localhost:8888/mysite/ 😂 thanks

Reply

Yay! Well then I'm double glad that this will, at least, be fixed in later versions!

Have fun!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}