Buy Access to Course
05.

The LoginFormAuthenticator

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

To 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:

  1. See if the user is submitting the login form, or if this is just some random request for some random page.
  2. Read the username and password from the request.
  3. 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.