AbstractLoginFormAuthenticator & Redirecting to Previous URL
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 SubscribeI have a confession to make: in our authenticator, we did too much work! Yep, when you build a custom authenticator for a "login form", Symfony provides a base class that can make life much easier. Instead of extending AbstractAuthenticator
extend AbstractLoginFormAuthenticator
:
// ... lines 1 - 15 | |
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator; | |
// ... lines 17 - 25 | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
// ... lines 28 - 95 | |
} |
Hold Command
or Ctrl
to open that class. Yup, it extends AbstractAuthenticator
and also implements AuthenticationEntryPointInterface
. Cool! That means that we can remove our redundant AuthenticationEntryPointInterface
:
// ... lines 1 - 23 | |
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
// ... lines 28 - 95 | |
} |
The abstract class requires us to add one new method called getLoginUrl()
. Head to the bottom of this class and go to "Code"->"Generate" - or Command
+N
on a Mac - and then "Implement Methods" to generate getLoginUrl()
. For the inside, steal the code from above... and return $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'); | |
} | |
} |
The usefulness of this base class is pretty easy to see: it implements three of the methods for us! For example, it implements supports()
by checking to see if the method is POST and if the getLoginUrl()
string matches the current URL. In other words, it does exactly what our supports()
method does. It also handles onAuthenticationFailure()
- storing the error in the session and redirecting back to the login page - and also the entry point - start()
- by, yet again, redirecting to /login
.
This means that... oh yea... we can remove code! Let's see: delete supports()
, onAuthenticationFailure()
and also 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 | |
} |
Much nicer:
// ... 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 | |
} | |
} |
Let's make sure we didn't break anything: go to /admin
and... perfect! The start()
method still redirects us to /login
. Let's log in with abraca_admin@example.com
, password tada
and... yes! That still works too: it redirects us to the homepage... because that's what we're doing inside of onAuthenticationSuccess
:
// ... 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: Smart Redirecting
But... if you think about it... that's not ideal. Since I was originally trying to go to /admin
... shouldn't the system be smart enough to redirect us back there after we successfully log in? Yep! And that's totally possible.
Log back out. When an anonymous user tries to access a protected page like /admin
, right before calling the entry point function, Symfony stores the current URL somewhere in the session. Thanks to this, in onAuthenticationSuccess()
, we can read that URL - which is called the "target path" - and redirect there.
To help us do this, we can leverage a trait! At the top of the class use TargetPathTrait
:
// ... lines 1 - 24 | |
use Symfony\Component\Security\Http\Util\TargetPathTrait; | |
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator | |
{ | |
use TargetPathTrait; | |
// ... lines 30 - 81 | |
} |
Then, down in onAuthenticationSuccess()
, we can check to see if a "target path" was stored in the session. We do that by saying if $target = $this->getTargetPath()
- passing the session - $request->getSession()
- and then the name of the firewall, which we actually have as an argument. That's that key 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 | |
} |
This line does two things at once: it sets a $target
variable to the target path and, in the if statement, checks to see if this has something in it. Because, if the user goes directly to the login page, then they won't have a target path in the session.
So, if we have a target path, redirect to it: 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 | |
} |
Done and done! If you hold Command
or Ctrl
and click getTargetPath()
to jump into that core method, you can see that it's super simple: it just reads a very specific key from the session. This is the key that the security system sets when an anonymous user tries to access a protected page.
Let's try this thing! We're already logged out. Head to /admin
. Our entry point redirects us to /login
. But also, behind the scenes, Symfony just set the URL /admin
onto that key in the session. So when we log in now with our usual email and password... awesome! We get redirected back to /admin
!
Next: um... we're still doing too much work in LoginFormAuthenticator
. Dang! It turns out that, unless we need some especially custom stuff, if you're building a login form, you can skip the custom authenticator class entirely and rely on a core authenticator from Symfony.
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)