Buy
Buy

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

Login Subscribe

Now that we've added our authenticator under the authenticators key:

security:
... lines 2 - 8
firewalls:
... lines 10 - 12
main:
... lines 14 - 15
guard:
authenticators:
- App\Security\LoginFormAuthenticator
... lines 19 - 33

Symfony calls its supports() method at the beginning of every request, which is why we see this little die statement:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
die('Our authenticator is alive!');
}
... lines 17 - 41
}

These authenticator classes are really cool because each method controls just one small part of the authentication process.

The supports() Method

The first method - supports() - is called on every request. Our job is simple: to return true if this request contains authentication info that this authenticator knows how to process. And if not, to return false.

In this case, when we submit the login form, it POSTs to /login. So, our authenticator should only try to authenticate the user in that exact situation. Return $request->attributes->get('_route') === 'app_login':

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
// do your work when we're POSTing to the login page
return $request->attributes->get('_route') === 'app_login'
... line 17
}
... lines 19 - 43
}

Let me... explain this. If you look in SecurityController, the name of our login route is app_login:

... lines 1 - 8
class SecurityController extends AbstractController
{
/**
* @Route("/login", name="app_login")
*/
public function login(AuthenticationUtils $authenticationUtils)
{
... lines 16 - 25
}
}

And, though you don't need to do it very often, if you want to find out the name of the currently-matched route, you can do that by reading this special _route key from the request attributes. In other words, this is checking to see if the URL is /login. We also only want our authenticator to try to login the user if this is a POST request. So, add && $request->isMethod('POST'):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
public function supports(Request $request)
{
// do your work when we're POSTing to the login page
return $request->attributes->get('_route') === 'app_login'
&& $request->isMethod('POST');
}
... lines 19 - 43
}

Here's how this works: if we return false from supports(), nothing else happens. Symfony doesn't call any other methods on our authenticator, and the request continues on like normal to our controller, like nothing happened. It's not an authentication failure - it's just that nothing happens at all.

If we return true from supports(), well, that's when the fun starts. If we return true, Symfony will immediately call getCredentials():

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
... line 22
}
... lines 24 - 43
}

To see if things are working, let's just dump($request->request->all()), then die():

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
dump($request->request->all());die;
}
... lines 24 - 43
}

I know, that looks funny. Unrelated to security, if you want to read POST data off of the request, you use the $request->request property.

Anyways, let's try it! Go back to your browser and hit enter on the URL so that it makes a GET request to /login. Hello login page! Our supports() method just returned false. And so, the request continued anonymously, like normal.

Log in with one of our dummy users: spacebar1@example.com. The password doesn't matter. And... enter! Yes! This time, because this is a POST request to /login, supports() returns true! So, Symfony calls getCredentials() and our dump fires! As expected, we can see the email and password POST parameters, because the login form uses these names:

... lines 1 - 10
{% block body %}
<form class="form-signin" method="post">
... lines 13 - 18
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus>
... line 20
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
... lines 22 - 29
</form>
{% endblock %}

The Brand-New dd() Function

Oh, and I want to show you a quick new Easter egg in Symfony 4.1, unrelated to security. Instead of dump() and die, use dd() and then remove the die:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
dd($request->request->all());
}
... lines 24 - 43
}

Refresh! Same result. This is just a nice, silly shortcut: dd() is dump() and die. We'll use it... because... why not?

The getCredentials() Method

Back to work! Our job in getCredentials() is simple: to read our authentication credentials off of the request and return them. In this case, we'll return the email and password. But, if this were an API token authenticator, we would return that token. We'll see that later.

Return an array with an email key set to $request->request->get('email') and password set to $request->request->get('password'):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 19
public function getCredentials(Request $request)
{
return [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
];
}
... lines 27 - 46
}

I'm just inventing these email and password keys for the new array: we can really return whatever we want from this method. Because, after we return from getCredentials(), Symfony will immediately call getUser() and pass this array back to us as the first $credentials argument:

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 27
public function getUser($credentials, UserProviderInterface $userProvider)
{
... line 30
}
... lines 32 - 46
}

Let's see that in action: dd($credentials):

... lines 1 - 10
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 13 - 27
public function getUser($credentials, UserProviderInterface $userProvider)
{
dd($credentials);
}
... lines 32 - 46
}

Move back to your browser and, refresh! Coincidentally, it dumps the exact same thing as before. But, this time, it's coming from line 30 - our line in getUser().

The getUser() Method

Let's keep going! Our job in getUser() is to use these $credentials to return a User object, or null if the user isn't found. Because we're storing our users in the database, we need to query for the user via their email. And to do that, we need the UserRepository that was generated with our entity.

At the top of the class, add public function __construct() with a UserRepository $userRepository argument:

... lines 1 - 4
use App\Repository\UserRepository;
... lines 6 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 15
public function __construct(UserRepository $userRepository)
{
... line 18
}
... lines 20 - 54
}

I'll hit Alt+Enter and select "Initialize Fields" to add that property and set it:

... lines 1 - 4
use App\Repository\UserRepository;
... lines 6 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
... lines 20 - 54
}

Back down in getUser(), just return $this->userRepository->findOneBy() to query by email, set to $credentials['email']:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 35
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $this->userRepository->findOneBy(['email' => $credentials['email']]);
}
... lines 40 - 54
}

This will return our User object, or null. The cool thing is that if this returns null, the whole authentication process will stop, and the user will see an error. But if we return a User object, then Symfony immediately calls checkCredentials(), and passes it the same $credentials and the User object we just returned:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
... line 43
}
... lines 45 - 54
}

Inside, dd($user) so we can see if things are working:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
dd($user);
}
... lines 45 - 54
}

Refresh and... got it! That's our User object!

The checkCredentials() Method

Ok, final step: checkCredentials(). This is your opportunity to check to see if the user's password is correct, or any other last, security checks. Right now... well... we don't have a password, so, let's return true:

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 40
public function checkCredentials($credentials, UserInterface $user)
{
// only needed if we need to check a password - we'll do that later!
return true;
}
... lines 46 - 55
}

And actually, in many systems, simply returning true is perfect! For example, if you have an API token system, there's no password.

If you did return false, authentication would fail and the user would see an "Invalid Credentials" message. We'll see that soon.

But, when you return true... authentication is successful! Woo! To figure out what to do, now that the user is authenticated, Symfony calls onAuthenticationSuccess():

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 46
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
... line 49
}
... lines 51 - 55
}

Put a dd() here that says "Success":

... lines 1 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 46
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
dd('success!');
}
... lines 51 - 55
}

Move over and... refresh the POST! Yes! We hit it! At this point, we have fully filled in all the authentication logic. We used supports() to tell Symfony whether or not our authenticator should be used in this request, fetched credentials off of the request, used those to find the user, and returned true in checkCredentials() because we don't have a password.

Next, let's fill in these last two methods and finally see - for real - that our user is logged in. We'll also learn a bit more about what happens when authentication fails and how the error message is rendered.

Leave a comment!

  • 2018-10-12 Oussama EZZIOURI

    Everything is ok regarding the logging and redirection to homepage.

    I will work on translation and error handling this evening and I'll give you a feedback.

    Thank's Victor.

  • 2018-10-12 Victor Bocharsky

    Hey Oussama,

    About 1st: Where does this message is set? Do you translate this message in the template with "|trans()" Twig filter? Did you enable translator in your system? If so, you need to clear the cache (even for dev environment) when you create a new translation file, otherwise Symfony won't see it. So, first of all, try to clear the cache. Here's the link for the reference: https://symfonycasts.com/sc...

    About 2nd: Oh, you do it wrong :) You pass to response just an error message, and it's sent to the user, i.e. you sent to users just an empty page with only error message in it. Looks like you're trying to do it in advance of this screencast. Please, watch a few more videos in this course, where we're talking about handling errors, e.g.:
    https://symfonycasts.com/sc...

    So in short, please, try to watch this course completely. We have covered this questions in it, but as I see you just missed some parts, probably you watched this course selectively?

    Cheers!

  • 2018-10-12 Victor Bocharsky

    Hey Oussama,

    Nope, you need to return it instead of sending, that's the correct way how it should work. If it's not redirect you, then you have some other problems you should fix first.

    Cheers!

  • 2018-10-11 Oussama EZZIOURI

    Hi Victor,

    Thank you for your replay. I've comment the Serializable interface and 2 methods: serialize() and unserialize(), now everything work like a charm! I got the username in the profiler bar.

    Now I've 2 issues :

    The 1st issue is with translation, I've created the translations/security.en.yaml file with the following content

    "Username could not be found.": "Incorrect login and password combination !"

    But when I login with bad credentials, I still get the standard message "Username could not be found."

    The 2nd issue is when I get The "Username could not be found." message, the browser stopped in a blank page with this error message, which is normal because I return a Response in onAuthenticationFailure() method :


    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
    return new Response($exception->getMessageKey());
    }

    My question now, is how can I dispaly this error in the login form ?

    Best regards.
    Symfony Lover.

  • 2018-10-11 Diego Aguiar

    Hey @Dan

    It's there a chance that you have something wrong in your supports() method? because if it doesn't return "true", then, the authentication flow stops and the request continues its lifecycle.

    Cheers!

  • 2018-10-11 Oussama EZZIOURI

    Hello my friend,

    Thank you for taking care of my comment, I will take this in considration in my future comments.

    For the response I've tried to return the response but it doesn't redirect me, so I've add send() method.

    But i still have the same problem with anonymous session. Could you please help me to solve it.

    Le jeu. 11 oct. 2018 10:24, Disqus <notifications@disqus.net> a écrit :

  • 2018-10-11 Dan

    Hey there I seem to have hit a snag - everything works until the supports function and then at getCredentials dump doesn't work and I get a huge red error "Cannot redirect to an empty URL." Do you have any idea what that might be caused by? Thanks a bunch.

  • 2018-10-11 Victor Bocharsky

    Hey Oussama,

    Looks like your comment was marked as spam by Disqus because it's long and contain HTML tags, I just recovered it from spam folder and modified slightly - wrap code with pre/code tags - it highlights syntax and give you more chances to bypass Discus spam system, just wanted to let you know.

    What about your question, hm, in onAuthenticationSuccess() just return the response, don't need to send it manually. It will be sent later automatically in another part of the system, i.e:


    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    return new RedirectResponse($this->router->generate('app_homepage'));
    }

    I see you use serialize()/unserialize() in your User class, that may be a problem with serialization/unserialization. Could you temporary comment out implementing \Serializable interface and those 2 methods: serialize() and unserialize()? Does it work without it? Now are you logged in in the Symfony web profiler?

    Cheers!

  • 2018-10-10 Oussama EZZIOURI

    Hi, Thank you for this great tutorial.

    I've succeed to login and redirect to my homepage, but the User still anonymous in the symfony profiler bar. This my code :


    namespace App\Security;

    use App\Repository\UserRepository;
    use Symfony\Component\HttpFoundation\RedirectResponse;
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\Routing\RouterInterface;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\User\UserInterface;
    use Symfony\Component\Security\Core\User\UserProviderInterface;
    use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator;

    class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
    {
    private $userRepository;
    private $router;

    public function __construct(UserRepository $userRepository, RouterInterface $router)
    {
    $this->userRepository = $userRepository;
    $this->router = $router;
    }
    public function supports(Request $request)
    {
    return $request->attributes->get('_route') === 'app_login'
    && $request->isMethod('POST');
    }

    public function getCredentials(Request $request)
    {
    return [
    'email' => $request->request->get('email'),
    'password' => $request->request->get('password'),
    ];
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
    return $this->userRepository->findOneBy(['email' => $credentials['email']]);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
    return true;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
    dd('failure!');
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    $response = new RedirectResponse($this->router->generate('app_homepage'));
    $response->send();
    }

    public function start(Request $request, AuthenticationException $authException = null)
    {
    // todo
    }

    public function supportsRememberMe()
    {
    // todo
    }

    /**
    * Return the URL to the login page.
    *
    * @return string
    */
    protected function getLoginUrl()
    {
    // TODO: Implement getLoginUrl() method.
    }
    }

    And for the user entity code :


    namespace App\Entity;

    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Component\Security\Core\User\UserInterface;

    /**
    * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
    */
    class User implements UserInterface, \Serializable
    {
    /**
    * @ORM\Id()
    * @ORM\GeneratedValue()
    * @ORM\Column(type="integer")
    */
    private $id;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $fname;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $lname;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $password;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $agency;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $email;

    /**
    * @ORM\Column(type="json", nullable=true)
    */
    private $roles = [];

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $username;

    public function getId(): ?int
    {
    return $this->id;
    }

    public function getFirstName(): ?string
    {
    return $this->fname;
    }

    public function setFirstName(string $firstName): self
    {
    $this->fname = $firstName;

    return $this;
    }

    public function getLastName(): ?string
    {
    return $this->lname;
    }

    public function setLastName(string $lastName): self
    {
    $this->lname = $lastName;

    return $this;
    }

    public function getPassword(): ?string
    {
    return $this->password;
    }

    public function setPassword(string $password): self
    {
    $this->password = $password;

    return $this;
    }

    public function getAgency(): ?string
    {
    return $this->agency;
    }

    public function setAgency(string $agency): self
    {
    $this->agency = $agency;

    return $this;
    }

    public function getEmail(): ?string
    {
    return $this->email;
    }

    public function setEmail(string $email): self
    {
    $this->email = $email;

    return $this;
    }

    /**
    * Returns the roles granted to the user.
    *
    *
    * public function getRoles()
    * {
    * return array('ROLE_USER');
    * }
    *

    *
    * Alternatively, the roles might be stored on a ``roles`` property,
    * and populated in any number of different ways when the user object
    * is created.
    *
    * @return (Role|string)[] The user roles
    */
    public function getRoles()
    {
    return array('ROLE_USER');
    }

    /**
    * Returns the salt that was originally used to encode the password.
    *
    * This can return null if the password was not encoded using a salt.
    *
    * @return string|null The salt
    */
    public function getSalt()
    {
    // TODO: Implement getSalt() method.
    }

    /**
    * Returns the username used to authenticate the user.
    *
    * @return string The username
    */
    public function getUsername()
    {
    return $this->username;
    }

    /**
    * Removes sensitive data from the user.
    *
    * This is important if, at any given point, sensitive information like
    * the plain-text password is stored on this object.
    */
    public function eraseCredentials()
    {
    // TODO: Implement eraseCredentials() method.
    }

    public function serialize()
    {
    return $this->serialize([
    $this->id,
    $this->fname,
    $this->lname,
    $this->email,
    $this->password,
    $this->username,
    $this->agency
    ]);
    }

    public function unserialize ($string)
    {
    list($this->id,
    $this->fname,
    $this->lname,
    $this->email,
    $this->password,
    $this->username,
    $this->agency) = unserialize($string, ['allowed_classes' => false]);
    }

    public function setRoles(?array $roles): self
    {
    $this->roles = $roles;

    return $this;
    }

    public function setUsername(string $username): self
    {
    $this->username = $username;

    return $this;
    }
    }
  • 2018-09-17 weaverryan

    Hey Peter Kosak!

    Ah, thanks dude! We're going to stay VERY busy writing more tutorials, don't worry ;).

    Great question! And... you already answered it ;). The only difference would be the error message - the user would always get a "Username not found" error message. However, even *that* can be controlled. At any time in your authenticator, you can throw a CustomUserMessageAuthenticationException (https://github.com/symfony/... and that message will be shown to the user. We separated getUser() from checkCredentials() only to make it a bit easier to get the right error message.

    > Why it is set up this way that firstly check user and then pw?

    We *need* to fetch the user first because we need it in *order* to check the password. That's the only reason :). This *does* mean that a user will be able to "discover" that an email address is valid or invalid by trying different email addresses. Some people don't like this from a security standpoint. If you care, you can change the error messages (via the CustomUserMessageAuthenticationException or via translations - we'll show that next) so that there is always the same message, no matter if the email or password failed.

    Cheers!

  • 2018-09-17 Peter Kosak

    Hi weaverryan,

    first of all congratulation You and whole KNP team (Victor & Diego I think sorry if I missed others) for working closer with Symfony and change of the name to SymfonyCasts great step. (I hope you wont be that busy with rewriting whole symfony doc as you said in emails, we need more videos :))

    My question is: What would be the difference if I would call in getUser method ->findOneBy and pass email & password?

    It would only return user when pw & email match so I wouldnt have to use checkCredential method.

    Is it because we will return different messages?
    One for user exists but PW is wrong and another for User does not exist please register?
    Why it is set up this way that firstly check user and then pw?

    Thanks & again big thumbs up!

  • 2018-09-14 weaverryan

    Hey Stéphane!

    Great question! I get this often. In short, I don't like the user provider :). It's actually not *really* meant to be used in this way. The main purpose of the user provider is 2 things:

    1) To help refresh the user at the beginning of every request
    2) Some core parts of security - like remember_me and switch_user - call its loadUserByUsername() when you use those

    Related to (2), if you use the form_login mechanism on your firewall, it also uses this method.

    Why don't I like it? Well, when you're writing your own code, you *already* know very well how to query for your User based on whatever condition you want. Telling people to use the user provider - an object they're not familiar with - can cause confusion. Of course, technically speaking, there's absolutely nothing wrong with using the user provider :).

    But, let me give you one more reason. IF we told people to use the user provider like you've done (which is a fine solution), it would work. But, for people using email, it looks confusing! "Hey! I don't have a username". Also, if your query logic becomes more complex - e.g. you have a third field on your login form called "companyId" and you need to query by "email & companyId", it's not possible with loadUserByUsername(). And, it's not clear *how* you would fix this. But, by making a query, it becomes very obvious later if you need to change that query with some extra logic.

    Phew! Does that make sense? I'm sure other people will have this question - so I wanted to give a clear answer. The same question was asked for our Symfony 3 security tutorial. Like I said, it's a good question :).

    Cheers!

  • 2018-09-14 Stéphane

    Hey @weaverryan,

    Why in getUser() method, you do not use $userProvider to retrieve user object like this :

    $user = $userProvider->loadUserByUsername($credentials['email']);

    There is no difference about dd($user);

    Thank for your explanation.