Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Authenticator: getUser, checkCredentials & Success/Failure

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

Here's the deal: if you return null from getCredentials(), authentication is skipped. But if you return anything else, Symfony calls getUser():

... lines 1 - 8
use Symfony\Component\Security\Core\User\UserProviderInterface;
... lines 10 - 11
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 14 - 36
public function getUser($credentials, UserProviderInterface $userProvider)
{
}
... lines 40 - 51
}

And see that $credentials argument? That's equal to what we return in getCredentials(). In other words, add $username = $credentials['_username']:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 39
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['_username'];
... lines 43 - 45
}
... lines 47 - 58
}

I do continue to call this username, but in our case, it's an email address. And for you, it could be anything - don't let that throw you off.

Hello getUser()

Our job in getUser() is... surprise! To get the user! What I mean is - to somehow return a User object. Since our Users are stored in the database, we'll query for them via the entity manager. To get that, add a second constructor argument: EntityManager $em:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 17
public function __construct(FormFactoryInterface $formFactory, EntityManager $em)
{
... lines 20 - 21
}
... lines 23 - 58
}

And once again, I'll use my Option+Enter shortcut to create and set that property:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... line 15
private $em;
public function __construct(FormFactoryInterface $formFactory, EntityManager $em)
{
... line 20
$this->em = $em;
}
... lines 23 - 58
}

Now, it's real simple: return $this->em->getRepository('AppBundle:User')->findOneBy() with email => $email:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 39
public function getUser($credentials, UserProviderInterface $userProvider)
{
$username = $credentials['_username'];
return $this->em->getRepository('AppBundle:User')
->findOneBy(['email' => $username]);
}
... lines 47 - 58
}

Easy. If this returns null, guard authentication will fail and the user will see an error. But if we do return a User object, then on we march! Guard calls checkCredentials():

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 47
public function checkCredentials($credentials, UserInterface $user)
{
}
... lines 51 - 58
}

Enter checkCredentials()

This is our chance to verify the user's password if they have one or do any other last-second validation. Return true if you're happy and the user should be logged in.

For us, add $password = $credentials['_password']:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 47
public function checkCredentials($credentials, UserInterface $user)
{
$password = $credentials['_password'];
... lines 51 - 56
}
... lines 58 - 65
}

Our users don't have a password yet, but let's add something simple: pretend every user shares a global password. So, if ($password == 'iliketurtles'), then return true:

... lines 1 - 49
$password = $credentials['_password'];
if ($password == 'iliketurtles') {
return true;
}
return false;
... lines 57 - 67

Otherwise, return false: authentication will fail.

When Authentication Fails? getLoginUrl()

That's it! Authenticators are always these three methods.

But, what happens if authentication fails? Where should we send the user? And what about when the login is successful?

When authentication fails, we need to redirect the user back to the login form. That will happen automatically - we just need to fill in getLoginUrl() so the system knows where that is:

... lines 1 - 12
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 15 - 58
protected function getLoginUrl()
{
}
... lines 62 - 65
}

But to do that, we'll need the router service. Once again, go back to the top and add another constructor argument for the router. To be super cool, you can type-hint with the RouterInterface:

... lines 1 - 8
use Symfony\Component\Routing\RouterInterface;
... lines 10 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 19
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router)
{
... lines 22 - 24
}
... lines 26 - 70
}

Use the Option+Enter shortcut again to set up that property:

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 17
private $router;
public function __construct(FormFactoryInterface $formFactory, EntityManager $em, RouterInterface $router)
{
... lines 22 - 23
$this->router = $router;
}
... lines 26 - 70
}

Down in getLoginUrl(), return $this->router->generate('security_login'):

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 61
protected function getLoginUrl()
{
return $this->router->generate('security_login');
}
... lines 66 - 70
}

When Authentication is Successful?

Tip

Due to a change in Symfony 3.1, you can still fill in getDefaultSuccessRedirectUrl() like we do here, but it's deprecated. Instead, you'll add a different method - onAuthenticationSuccess() - we have the code in a comment: http://bit.ly/guard-success-change

So what happens when authentication is successful? It's awesome: the user is automatically redirected back to the last page they tried to visit before being forced to login. In other words, if the user tried to go to /checkout and was redirected to /login, then they'll automatically be sent back to /checkout so they can continue buying your awesome stuff.

But, in case they go directly to /login and there is no previous URL to send them to, we need a backup plan. That's the purpose of getDefaultSuccessRedirectUrl():

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 66
protected function getDefaultSuccessRedirectUrl()
{
... line 69
}
}

Send them to the homepage: return $this->router->generate('homepage'):

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 66
protected function getDefaultSuccessRedirectUrl()
{
return $this->router->generate('homepage');
}
}

The authenticator is done. If you need even more control over what happens on error or success, there are a few other methods you can override. Or check out our Guard tutorial. Let's finally hook this thing up.

Registering the Service

To do that, open up app/config/services.yml and register the authenticator as a service.

Tip

If you're using Symfony 3.3, your app/config/services.yml contains some extra code that may break things when following this tutorial! To keep things working - and learn about what this code does - see https://knpuniversity.com/symfony-3.3-changes

Let's call it app.security.login_form_authenticator. Set the class to LoginFormAuthenticator and because I'm feeling super lazy, autowire the arguments:

... lines 1 - 5
services:
... lines 7 - 17
app.security.login_form_authenticator:
class: AppBundle\Security\LoginFormAuthenticator
autowire: true

We can do that because we type-hinted all the constructor arguments.

Configuring in security.yml

Finally, copy the service name and open security.yml. To activate the authenticator, add a new key under your firewall called guard. Add authenticators below that, new line, dash and paste the service name:

... lines 1 - 2
security:
... lines 4 - 9
firewalls:
... lines 11 - 15
main:
... line 17
guard:
authenticators:
- app.security.login_form_authenticator
# activate different ways to authenticate
... lines 22 - 28

As soon as we do that, getCredentials() will be called on every request and our whole system should start singing.

Let's try it! Try logging in with weaverryan+1@gmail.com, but with the wrong password.

Beautiful! Now try the right password: iliketurtles.

Debugging with intercept_redirects

Ah! Woh! It did redirect to the homepage as if it worked, but with a nasty error. In fact, authentication did work, but there's a problem with fetching the User from the session. Let me prove it by showing you an awesome, hidden debugging tool.

Open up config_dev.yml and set intercept_redirects to true:

... lines 1 - 12
web_profiler:
... line 14
intercept_redirects: true
... lines 16 - 49

Now, whenever the app is about to redirect us, Symfony will stop instead, and show us the web debug toolbar for that request.

Go to /login again and login in with weaverryan+1@gmail.com and iliketurtles. Check this out: we're still at /login: the request finished, but it did not redirect us yet. And in the web debug toolbar, we are logged in as weaverryan+1@gmail.com.

So authentication works, but there's some issue with storing our User in the session. Fortunately, that's going to be really easy to fix.

Leave a comment!

10
Login or Register to join the conversation
Default user avatar
Default user avatar sunil kumar | posted 2 years ago | edited

public function getUser($credentials, UserProviderInterface $userProvider)
{
   try {
    $data = $this->jwtEncoder->decode($credentials);
      } catch (JWTDecodeFailureException $e) {
        throw new CustomUserMessageAuthenticationException('Invalid Token');
     }

     if ($data === false) {
        throw new CustomUserMessageAuthenticationException('Invalid Token');
     }
      if(isset($data['user_id']))
          $user_id = $data['user_id'];
       else
        throw new CustomUserMessageAuthenticationException('Invalid Token');

       return $this->em
            ->getRepository('AcmeApiBundle:User')
             ->findOneBy(['id' => $user_id]);
}

when using $this->getUser() getting this error
The Acme\ApiBundle\Security\JwtTokenAuthenticator::getUser() method must return a UserInterface. You returned Acme\ApiBundle\Entity\User.
when implement UserInterface in my User entity I get a null response from $this->getUser()
how to solve this?

1 Reply

Hi sunil kumar!

Hmm. So first, yes, your Acme\ApiBundle\Entity\User class must implement UserInterface. It's just one of the rules for security: whatever object you return from the security system (via getUser() in your authenticator) must be an object that implements this interface.

when implement UserInterface in my User entity I get a null response from $this->getUser()

This is odd. The fact that you got the previous error:

Acme\ApiBundle\Security\JwtTokenAuthenticator::getUser() method must return a UserInterface. You returned Acme\ApiBundle\Entity\User"

tells me that your authenticator's getUser() method IS successfully returning the Acme\ApiBundle\Entity\User object. So it's odd that, as soon as you add an interface to that object, it is not set into the security system. Basically, something weird is happening. Here is what I would do:

A) Add the UserInterface. You need this.
B) Verify that you are returning a User object from your authenticator's getUser() method. I would do something like this temporarily:



$user= $this->em
    ->getRepository('AcmeApiBundle:User')
    ->findOneBy(['id' => $user_id]);

dd($user);

return $user;

C) Verify that your authenticator's getCredentials() method returns true - it will literally be return true;.

D) Verify (by placing a temporary dd('here!');) that the onAuthenticationSuccess() method in your authenticator IS being called. We want to make sure that authentication WAS successful.

Let me know if this helps you find anything :).

Cheers!

Reply
Florian Avatar
Florian Avatar Florian | posted 4 years ago

Hi, I really enjoy the learning videos.
I'd like to know the following.
How do you get the green extra information (formFactory : \Symfony\ ...) in PHPStorm
Tutorial at 4:38?

Thank you

Reply

Hey Florian!

That's almost definitely from the PhpStorm Symfony plugin - make sure you've got it installed AND enabled on this project. It's a bit outdated now, but we talk about it on this tutorial: https://symfonycasts.com/sc...

Cheers!

1 Reply

I have a problem ERR_TOO_MANY_REDIRECTS in Chrome. I am using Symfony 3.4.10.

What is happening is that when I log in for the first time using POST, my authenticator guard in the $this->_security->getUser() (using the Supports method). is NULL which is correct and it authenticates and redirects to WelcomeController (using the onAuthenticationSuccess method). Up until here it's working fine. Upon redirection in the authenticator guard the $this->_security->getUser() is NULL AGAIN even though I have already logged in and thus it authenticates again which causes to go to LoginController (using the getLoginUrl method) where here again just because I have a logic that if session variable userId exists it should be redirected to WelcomeController. This goes forever.

Shouldn't the value $this->_security->getUser() be other than null upon my second redirection since I have already log in?

Here is the code below.

In my authenticator guard the first function.


 public function supports(Request $request)
    {
        if ($this->_security->getUser()) {
            return false;
         }
         return true;
    }

in my SecurityController.php


 public function loginAction()
    {
        $session = $this->get('session');
        if ($session->has('user_id')) {
            return $this->redirectToRoute('admin_welcome');
        } else {
            $authenticationUtils = $this->get('security.authentication_utils');
            // get the login error if there is one
            $error = $authenticationUtils->getLastAuthenticationError();
            // last username entered by the user
            $lastUsername = $authenticationUtils->getLastUsername();
            $user = new \TB_Models_User();
            $user->setUsername($lastUsername);
            $userForm = $this->createForm(UserType::class, $user);
            $userForm->remove('_plainPassword');
        }
        return $this->render('admin/login/index.html.twig',[
                'userForm' => $userForm->createView(),
                'error' => $error,
            ]);
    }

and security.yml


       login:
            pattern:  ^/admin/login$
            security: true
            anonymous: true
            provider: admin_tb_user_provider
            stateless: false
            guard:
                authenticators:
                    - app.admin_authenticator

        admin:
            pattern: ^/admin
            provider: admin_tb_user_provider
            security: true
            stateless: false
            guard:
                authenticators:
                    - app.admin_authenticator

access_control:
        - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: [ROLE_USER] }

Any advice?

Reply

Hey Alexandros_Spyropoulos!

Apologies for my slow reply! So.... hmm, this is a mystery! First, yes, $this->_security->getUser() should return the User object once your User is authenticated. The idea behind your code here is correct! So, here is my guess: after you redirect, you are somehow losing authentication. This is actually something that can happen if your code isn't setup quite correctly. Basically, you authenticate, redirect, but then there is a problem reloading your User object from the session.

Here is how you can see if this is your problem. In loginAction, instead of checking for $session = $this->get('session');, do the same check that you have in your authenticator - get the security service and call getUser(). If I am correct, you will find that you are being immediately logged out, and so, this will now render the login form.

IF this is your problem, let me know: the issue is with the serialization of your User object. If you have a serialize() method on your User class, that could be the problem. The temporary fix would be to implement an EquatableInterface on your user, which requires you to add a new isEqualTo(UserInterface $user method. Try returning true from this. If it fixes your problem, then my guess is right. Let me know, and then we can talk about the correct solution.

But, if I'm totally wrong, also let me know!

Cheers!

Reply

Hi Ryan,

First of all thank you for you reply!
Secondly I did the test that you asked and I also did the whole cycle noticing the security getUser() value. There are happening 3 redirects before accessing Security Controller and loginAction. First is the credentials post, here $this->_security->getUser() is NULL so authentication goes on. On authentication success I should be redirected to WelcomeController again authenticator starts and the $this->_security->getUser() IS AGAIN NULL so I am redirecting back to LoginController. Now in authenticator this time the $this->_security->getUser() has the authenticated user object so no authentication takes place and I am redirecting straight to my LoginController. Here I used the same service as in my authenticator $this->get('security.helper')->getUser() and the user object exists! However due to my session logic I redirect back to WelcomeController. I am again inside authenticator and guess what...? $this->_security->getUser() is NULL AGAIN so I am redirecting back to LoginController to see that $this->get('security.helper')->getUser() has the user. I hope I didn't make you feel dizzy :)

So as you see it seems like something is writing and deleting the security getUser? Also there isn't any serialize() method on my User Class. If you need to provide any extra code please let me know!

Thanks again for your help

Reply

Hey @alexandrosspylitopoulos!

Hmm, this is indeed very strange! It's very strange that after the first redirect, you are not authenticated (not user) and then after the second redirect, you are authenticated. Basically, something *very* strange is happening, and I would recommend removing as much code as you can to try to find the problem. A few questions:

A) Did you try to implement EquatableInterface on your User and return true from isEqualTo()? I don't think this will fix the problem, but we need to try it to be sure.

B) Do you have just 1 authenticator or other authenticators?

C) Can you post your security.yml file and authenticator code?

Cheers!

Reply

Hey @weaverryan ,

I solved the problem. I didn't need to follow any of the steps provided.
As it seems the problem was most probably due to having two firewalls set up instead of one.

If you check I had the login and the admin firewall so that mean two provider keys. The two provider keys means two different tokens ( check createAuthenticatedToken(UserInterface $user, $providerKey) at GuardAuthenticatorInterface ) and two different session variables. So when I was logged in the first time my request was routing via login firewall ($providerKey) it was creating a token that was then stored in session under key _security_login ( session key pattern is _security_$providerKey). The second time my request was routing through the admin firewall ($providerKey) and was try to find in the session key _security_admin which was empty and thus failing to get the serialized token that contained the User object.

What I did was to creating one firewall and then use the access_control correctly as I had to do from the beginning I suppose :P nevertheless thank you so much for your support!

Reply

Hey @alexandrosspylitopoulos!

Bah! Good fix! That was my fault - I completely missed the fact that you had two firewalls! And yes, I like your solution: you rarely need 2 real firewalls.

Thanks for updating me on the fix!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1" // 1.1.1
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}
userVoice