The LoginFormAuthenticator
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 SubscribeTo use Guard - no matter what crazy authentication system you have - the first step is always to create an authenticator class. Create a new directory called Security
and inside, a new class: how about LoginFormAuthenticator
:
// ... lines 1 - 2 | |
namespace AppBundle\Security; | |
// ... lines 4 - 7 | |
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 12 - 30 | |
} |
The only rule about an authenticator is that it needs to extend AbstractGuardAuthenticator
. Well, not totally true - if you're building some sort of login form, you can extend a different class instead: AbstractFormLoginAuthenticator
- it extends that other class, but fills in some details for us.
Hit Command
+N
- or go to the "Code"->"Generate" menu - choose "Implement Methods" and select the first three:
// ... lines 1 - 4 | |
use Symfony\Component\HttpFoundation\Request; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
use Symfony\Component\Security\Core\User\UserProviderInterface; | |
// ... lines 8 - 9 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
public function getCredentials(Request $request) | |
{ | |
} | |
public function getUser($credentials, UserProviderInterface $userProvider) | |
{ | |
} | |
public function checkCredentials($credentials, UserInterface $user) | |
{ | |
} | |
// ... lines 23 - 30 | |
} |
Then, do it again, and choose the other two:
// ... lines 1 - 9 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 12 - 23 | |
protected function getLoginUrl() | |
{ | |
} | |
protected function getDefaultSuccessRedirectUrl() | |
{ | |
} | |
} |
Tip
Starting in Symfony 3.1, you won't see getDefaultSuccessRedirectUrl()
in this list anymore.
Don't worry! We'll tell you how to handle this later.
That was just my way to get these methods in the order I want, but it doesn't matter.
How Authenticators Work
When we're finished, Symfony will call our authenticator on every single request. Our job is to:
- See if the user is submitting the login form, or if this is just some random request for some random page.
- Read the username and password from the request.
- Load the User object from the database.
getCredentials()
That all starts in getCredentials()
. Since this method is called on every request, we first need to see if the request is a login form submit. We setup our form so that it POSTs right back to /login
. So if the URL is /login
and the HTTP method is POST
, our authenticator should spring into action. Otherwise, it should do nothing: this is just a normal page.
Create a new variable called $isLoginSubmit
Set that to $request->getPathInfo()
- that's the URL - == '/login' && $request->isMethod('POST')
:
// ... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 14 - 20 | |
public function getCredentials(Request $request) | |
{ | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
// ... lines 24 - 34 | |
} | |
// ... lines 36 - 51 | |
} |
Tip
Instead of hardcoding the /login
URL, you could instead check for the current page's route name:
if ($request->attributes->get('_route') === 'security_login' && $request->isMethod('POST'))
If both of those are true, the user has just submitted the login form.
So, if (!$isLoginSubmit)
, just return null
:
// ... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
// ... lines 28 - 53 |
If you return null
from getCredentials()
, Symfony skips trying to authenticate the user and the request continues on like normal.
getCredentials(): Build the Form
If the user is trying to login, our new task is to fetch the username & password and return them.
Since we built a form, let's let the form do the work for us.
Normally in a controller, we call $this->createForm()
to build the form:
// ... lines 1 - 38 | |
abstract class Controller implements ContainerAwareInterface | |
{ | |
// ... lines 41 - 274 | |
/** | |
* Creates and returns a Form instance from the type of the form. | |
* | |
* @param string|FormTypeInterface $type The built type of the form | |
* @param mixed $data The initial data for the form | |
* @param array $options Options for the form | |
* | |
* @return Form | |
*/ | |
protected function createForm($type, $data = null, array $options = array()) | |
{ | |
return $this->container->get('form.factory')->create($type, $data, $options); | |
} | |
// ... lines 288 - 396 | |
} |
In reality, this grabs the form.factory
service and calls create()
on it.
Dependency Inject form.factory (FormFactory)
So how can we create a form in the authenticator? Use dependency injection to inject the form.factory
service.
Add a __construct()
method with a $formFactory
argument:
// ... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 14 - 15 | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
// ... line 18 | |
} | |
// ... lines 20 - 51 | |
} |
Now, I like to type-hint my arguments, so let's just guess at the service's class name and see if there's one called FormFactory
. Yep, there's even a FormFactoryInterface
!
// ... lines 1 - 5 | |
use Symfony\Component\Form\FormFactoryInterface; | |
// ... lines 7 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 14 - 15 | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
// ... line 18 | |
} | |
// ... lines 20 - 51 | |
} |
That's probably what we want. I'll press Option
+Enter
and select "Initialize Fields" to set that property for me:
// ... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
private $formFactory; | |
public function __construct(FormFactoryInterface $formFactory) | |
{ | |
$this->formFactory = $formFactory; | |
} | |
// ... lines 20 - 51 | |
} |
If you're still getting used to dependency injection and that all happened too fast, don't worry. We know we want to inject the form.factory
service, so I guessed its class for the type-hint, which is optional. You can always find your terminal and run:
./bin/console debug:container form.factory
to find out the exact class to use for the type-hint. We will also register this as a service in services.yml
in a minute.
Return the Credentials
Back in getCredentials()
, add $form = $this->formFactory->create()
and pass it LoginForm::class
:
// ... lines 1 - 4 | |
use AppBundle\Form\LoginForm; | |
// ... lines 6 - 11 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
// ... lines 14 - 20 | |
public function getCredentials(Request $request) | |
{ | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
// ... lines 30 - 34 | |
} | |
// ... lines 36 - 51 | |
} |
Then - just like always - use $form->handleRequest($request)
:
// ... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
$form->handleRequest($request); | |
// ... lines 31 - 53 |
Normally, we would check if $form->isValid()
, but we'll do any password checking or other validation manually in a moment. Instead, just skip to $data = $form->getData()
and return $data
:
// ... lines 1 - 22 | |
$isLoginSubmit = $request->getPathInfo() == '/login' && $request->isMethod('POST'); | |
if (!$isLoginSubmit) { | |
// skip authentication | |
return; | |
} | |
$form = $this->formFactory->create(LoginForm::class); | |
$form->handleRequest($request); | |
$data = $form->getData(); | |
return $data; | |
// ... lines 35 - 53 |
Since our form is not bound to a class, this returns an associative array with _username
and _password
. And that's it for getCredentials()
. If you return any non-null value, authentication continues to the next step.
At about the 2 minute mark, you hard code "/login". It would be better to somehow call the path "security_login" created in the routing of the controller, correct? In case "/login" later changes to something else.