Buy
Buy

Redirecting on Success & the User Provider

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

Login Subscribe

If our authenticator is able to return a User from getUser() and we return true from checkCredentials():

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

Then, congrats! Our user is logged in! The last question Symfony asks us is: now what? Now that the user is authenticated, what do you want to do?

For a form login system, the answer is: redirect to another page. For an API token system, the answer is... um... nothing! Just allow the request to continue like normal.

This is why, once authentication is successful, Symfony calls onAuthenticationSuccess():

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

We can either return a Response object here - which will be immediately sent back to the user - or nothing... in which case, the request would continue to the controller.

Redirecting on Success

So, hmm, we want to redirect the user to another page. So... how do we redirect in Symfony? If you're in a controller, there's a redirectToRoute() shortcut method. Hold Command or Ctrl and click into that. I want to see what this does.

Ok, it leverages two other methods: redirect() and generateUrl(). Look at redirect(). Oh.... So, to redirect in Symfony, you return a RedirectResponse object, which is a sub-class of the normal Response. It just sets the status code to 301 or 302 and adds a Location header that points to where the user should go. That makes sense: a redirect is just a special type of response!

The other method, generateUrl(), is a shortcut to use the "router" to convert a route name into its URL. Go back to the controller and clear out our dummy code.

Back in LoginFormAuthenticator, return a new RedirectResponse(). Hmm, let's just send the user to the homepage. But, of course, we don't ever hardcode URLs in Symfony. Instead, we need to generate a URL to the route named app_homepage:

... lines 1 - 13
class ArticleController extends AbstractController
{
... lines 16 - 25
/**
* @Route("/", name="app_homepage")
*/
public function homepage(ArticleRepository $repository)
{
... lines 31 - 35
}
... lines 37 - 63
}

We know how to generate URLs in Twig - the path() function. But, how can we do it in PHP? The answer is... with Symfony's router service. To find out how to get it, run:

php bin/console debug:autowiring

Look for something related to routing... there it is! Actually, there are a few different router-related interfaces... but they're all different ways to get the same service. I usually use RouterInterface.

Back on top, add a second constructor argument: RouterInterface $router:

... lines 1 - 7
use Symfony\Component\Routing\RouterInterface;
... lines 9 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 18
public function __construct(UserRepository $userRepository, RouterInterface $router)
{
... lines 21 - 22
}
... lines 24 - 59
}

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

... lines 1 - 7
use Symfony\Component\Routing\RouterInterface;
... lines 9 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... line 16
private $router;
public function __construct(UserRepository $userRepository, RouterInterface $router)
{
... line 21
$this->router = $router;
}
... lines 24 - 59
}

Then, back down below, use $this->router->generate() to make a URL to app_homepage:

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 50
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->router->generate('app_homepage'));
}
... lines 55 - 59
}

Ok! We still have one empty method:

... lines 1 - 13
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 16 - 55
protected function getLoginUrl()
{
// TODO: Implement getLoginUrl() method.
}
}

But, forget that! We're ready! Go back to your browser, and hit enter to show the login page again. Let's walk through the entire process. Use the same email, any password and... enter! It worked! How do I know? Check out the web debug toolbar! We are logged in as [email protected]!

Authentication & the Session: User Provider

This is even cooler than it looks. Think about it: we made a POST request to /login and became authenticated thanks to our authenticator. Then, we were redirected to the homepage... where our authenticator did nothing, because its supports() method returned false.

The only reason we're still logged in - even though our authenticator did nothing on this request - is that user authentication info is stored to the session. At the beginning of every request, that info is loaded from the session and we're logged in. Cool!

Look back at your security.yaml file. Remember this user provider thing that was setup for us?

security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 9 - 33

This is a class that helps with the process of loading the user info from the session.

Honestly, it's a little bit confusing, but super important. Here's the deal: when you refresh the page, the User object is loaded from the session. But, we need to make sure that the object isn't out of date with the database. Think about it. Imagine we login at work. Then, we login at home and update our first name in the database. The next day, when we go back to work, we reload the page. Well... if we did nothing else, the User object we reloaded from the session for that browser would have our old first name. That would probably cause some weird issues.

So, that's the job of the user provider. When we refresh, the user provider takes the User object from the session and uses its id to query for a fresh User object. It all happens invisibly, which is great. But it is an important, background detail.

Next, I want to see what happens when we fail authentication. What does the user see? How are errors displayed? And how can we control them?

Leave a comment!

  • 2018-12-31 weaverryan

    Hi there!

    That's really interesting! How did you create your App\Entity\User class? Very simply, that class MUST implement Symfony's UserInterface - we talk about that here: https://symfonycasts.com/sc...

    Very simply, this error tells me that you are not implementing this interface. You may simply not be implementing it, OR it's possible that you ARE implementing it, but you forgot the use statement for UserInterface.

    Let us know what you find out!

    Cheers!

  • 2018-12-29 MajkellVZ

    I am getting this error.
    The App\Security\LoginFormAuthenticator::getUser() method must return a UserInterface. You returned App\Entity\User.

  • 2018-12-09 weaverryan

    Hey Kristof Kreimeyer!

    Hmm, sorry about the problem - that's no fun! We take a lot of pride in the code in the tutorial being correct and being exactly what we use in the video. However, sometimes, things change over time - or other issues happen. So, I apologize if you've been having any issue that are our fault.

    Let's figure out this problem! On the surface, the problem is obvious (but the cause is quite NOT obvious): the getSession() method on the Request is returning null. Basically, there is no session for some reason! That is quite bizarre. Here is some background on this the session is set onto the request (these are normally not details you should care about - but something isn't working correctly):

    1) A class called SessionListener is called early in Symfony. It fetches the "session" service from the container and sets it on the request. It does this unless, for some reason, there is NOT session in the container.

    2) So, the most likely cause for this to happen is that (somehow) there is no session service in your container. You can verify this by running: php bin/console debug:container session. This *should* return information about the session service. But, my guess is that, for you, this will throw an error about the session not being found (but tell me if I'm wrong!). So, why would the session service not exist? The session needs to be enabled via some configuration. But, when you start a Symfony project, this configuration is included automatically for you in your config/packages/framework.yaml file. You should have this file with some code that looks like this: https://github.com/symfony/...

    So, does any of this look different on your project? Does the session service exist or is it missing? Do you have the session configuration that I listed above?

    Let me know and we can keep debugging if this doesn't help!

    Cheers!

  • 2018-12-05 Kristof Kreimeyer

    Another day, another error or in other words: "same procedure as every video" :
    at the end i tried to refresh the page and get the error "Call to a member function set() on null"
    The error is in in src\Security\LoginFormAuthenticator.php (line 51) and the content of this line is :
    $request->getSession()->set(

    So i asked Google about it, but i haven't found an answer yet.

  • 2018-10-25 Diego Aguiar

    Hey Matt Johnson

    Actually, you do nothing! You just let the request to continue its lifecycle and hit a controller's action to handle the response. Probably you will be interested in watching this chapter (and the next one): https://symfonycasts.com/sc...

    Cheers!

  • 2018-10-25 Matt Johnson

    You briefly mention what onAuthenticationSuccess looks like when doing API tokens. Can you elaborate on that a bit, and what this entire structure would look like if we were using a token? Is it something like:

    1. Credentials submitted & verified
    2. We return bearer token to API (and store it locally, probably with an expires timer)
    3. API sends bearer token to all endpoints except the login authenticator

    In the case above we'd still need this login authenticator, but our success would be a response that just contains the token. Subsequent requests would have another security voter that looks for the bearer token and votes yes or no based on whether or not the token exists and is (not) expired. Right? Any other considerations here?

    Forgive me if this is answered later on in this course.

    Cheers,

    Matt

  • 2018-10-18 weaverryan

    Hey Mickaël Andrieu!

    Oh man, SUCH a good question. You probably didn't realize you were asking something I have such a passionate idea about ;). Basically, form_login is so "invisible" that I'd argue that you'd spend as much time trying to figure out how it works as it would be to implement an authenticator yourself (where you can see all of the code and it's clear what's happening). Well, truthfully, the Guard authentication still *would* take longer to write: but I think it's worth it for how much more "clear" the end result is: you see the logic in your code, vs it being hidden somewhere (and you're not even sure where).

    But, I have 2 other big reasons:

    1) Today's announcement of the php bin/console make:auth command makes using Guard authentication faster to setup than form_login (or, at least equally fast, if you consider that we could, in theory, have made a generator that used form_login). This means you get more clarity with zero cost.

    2) For my actual big reason: form_login is impossible to customize. Suppose form_login works great for 70% of people. Awesome - that's a lot! But now, what if you need to create a custom query for the user? That's possible... but not obvious. What if you want to prevent login if someone has failed there password 5 times? That is *quite* difficult. And what if you need a *third* field in your form (e.g. "company dropdown") and you need to use that in the query for the User? You're dead :). I'm fine with a solution that covers the 70% perfectly, as long as making the last 30% of use-cases work is reasonable and obvious. With form_login, it is neither reasonable nor obvious. That's the *real* reason. And, the core team recently agreed internally to start promoting this as the "main" way, over form_login (though some of the super simple built-in authentication providers like http_basic are still quite good I think).

    Cheers!

  • 2018-10-17 Mickaël Andrieu

    Hello Ryan,

    thinking about it, I guess that for a simple form login we can rely on "form_login" of the Security Bundle...
    what are the pros/cons to use an authenticator instead of what is available in the Security Bundle?

  • 2018-09-27 Diego Aguiar

    Actually you don't have to inject a user like that, you can rely on calling $this->getUser(); in any controller (both ways gives you the same User object)

  • 2018-09-27 shing

    ah ok. missed that 👍

  • 2018-09-27 Victor Bocharsky

    Hey Shing,

    I see it calls refreshUser() anyway, see https://github.com/symfony/...

    Cheers!

  • 2018-09-27 shing

    AH. I ok i understand.

    If anyone is curious, in ContextListener->handle(), it checks for a hasPreviousSession(). It uses that User object if present, else queries for a fresh one.

  • 2018-09-27 shing

    👍👍👍.

    I get it. Because there will always only have 1 user provider thats being set in the security.yaml
    Thats why when u inject a UserInterface, thats the user provider thats going to be injected in.


    Controller.php
    public function index(UserInterface $user )
    {
    dump($user);die;
    }

    Wow. This User object is elegant and a big hairy beast at the same time.

  • 2018-09-25 Diego Aguiar

    Hey @shing

    > I'm guessing the injected UserInterface $user is the fresh user?
    Yes, you are right :)

    > And if using REST JWT, EquatableInterface is not needed...
    Yep, there is no session, you would be passing the token on every request and Symfony will fetch the user based on that token, but I believe it won't execute a query every time you call $this->getUser(), it will just get it from the token object.

    > Is this the same $this->getUser() fresh object?
    Yes, at that point of the request lifecycle you are working with the fresh user object

    Cheers!

  • 2018-09-25 shing

    Just a quick add on.

    in the controller


    public function index(UserInterface $user )
    {
    dump($user);die;
    }

    Is this the same $this->getUser() fresh object?

  • 2018-09-25 shing

    👍👍👍. Thank u! Super clear and concise A-B-C. I've been searching high and low for this explanation.

    Sorry, I made a mistake with
    > \UserInterface->getUser() in the controller.
    I swear, i was trying that and getUser() was there. I think I'm confusing it with the EquatableInterface.


    public function isEqualTo(UserInterface $user)
    {
    //......
    if ($this->username !== $user->getUsername()) {
    return false;
    }
    //......
    }

    I'm guessing the injected UserInterface $user is the fresh user?
    And if using REST JWT, EquatableInterface is not needed. There is not session so $this->getUser() always queries for a fresh user obj. I'm going through ur Guard and Rest tuts on this one.

    Thank u for being so patient. KNP is a very valuable resource!

  • 2018-09-25 weaverryan

    Hi shing!

    Ah, very interesting questions :). First:


    $this->getUser()

    This is always, 100% of the time the "fresh" user. Why? Here's how it works:

    A) At the end of the request, your User object (the one you can get via $this->getUser()) is serialized to the session.
    B) At the beginning of the next request (very early), that User object is deserialized from the session. At this point, it may be out-of-date
    C) At the next moment (so still very early in the request), Symfony reads the "id" from the "out-of-date" User object and uses it to query for a fresh User object. This is what you will receive whenever you access the User for the rest of the request.

    So, there IS one query per request, but it happens just once, automatically, and very early in Symfony. If you want to look at the exact code, the class is called ContextListener.

    You also mentioned EquatableInterface and isEqualTo(). That is a related topic. After Symfony "refreshes the User" (queries the database for a new User), it compares the old User (the one just deserialized from the session) to the new, fresh User. If they are "different" (I'll explain this next), then Symfony logs you out. To determine if they are different, by default, it just compares a few important methods on both objects. You can see that in AbstractToken - https://github.com/symfony/.... You can ALSO see there that IF you User class implements EquatableInterface, then it calls your isEqualTo method so that YOU can control this comparison.

    But, what is the purpose of this comparison? Why are we comparing the old and fresh object to see if they are different? The reason is security. Suppose you are on one computer, logged in. You realize that someone has obtained your password and is logged in on another computer somewhere else. So, you change your password. When you do that, you want that other person to be logged out. That's what this comparison does: once you change your password, the next time that person refreshes, their session User object will not be equal to the database User and they'll be logged out.

    Oh, and there was one part of your question that I didn't understand:

    > so everytime I call \UserInterface->getUser()...

    I don't understand what you mean by this? UserInterface is just an interface that your User class implements. There is no getUser() method on it. Let me know :).

    Cheers!

  • 2018-09-24 shing

    Hi,

    In the controller, there are


    $this->getUser();
    \UserInterface->getUser();

    It also appears in


    public function isEqualTo(UserInterface $user)
    {
    //......
    if ($this->username !== $user->getUsername()) {
    return false;
    }
    //......
    }

    "Finally, the fresh User object is compared to the deserialized User object to make sure that they represent the same user."

    My question is:
    - I'm guessing

    $this->getUser()

    is the in-memory/sessionId deserialized User object.
    - The fresh User object is

    \UserInterface->getUser()

    .
    so everytime I call

    \UserInterface->getUser()

    it queries the DB? Which class handles that?

    -"It may also be useful to implement the EquatableInterface interface, which defines a method to check if the user is equal to the current user"
    How is user and current user different?

    Thank u.

  • 2018-09-24 Victor Bocharsky

    Hey Niumis,

    Thank for helping us answering questions! ;) But your suggestion is relevant if you do not use Guard but Symfony "form_login" feature and affects only failed login. In order to do so with Guard, see my answer: https://symfonycasts.com/sc...

    Cheers!

  • 2018-09-24 Victor Bocharsky

    Hey Dmitriy,

    Good question! Hm, a referer can be not perfect if you write invalid credentials, I suppose on the next login the referer will be the same login page. Actually, we'll cover this feature further in this course.

    But in case you wonder right now, see TargetPathTrait inside AbstractFormLoginAuthenticator that we extend in this course. It has TargetPathTrait::getTargetPath() method - that's exactly what you need, redirect user to the returned URL. In onAuthenticationSuccess() I do:


    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
    // ...

    $targetPath = $this->getTargetPath($request);
    if ($targetPath) {
    return new RedirectResponse($targetPath);
    }

    return null;
    }

    Cheers!

  • 2018-09-22 Дмитрий Ченгаев

    Yes thank you. I solved the problem by adding a hidden field to the login form with the value of the referer page.

    It would be nice to look at a more detailed description of the solution to this problem from symfonycasts.

  • 2018-09-22 niumis
  • 2018-09-21 Дмитрий Ченгаев

    How can I redirect a user to the last page on which he was using before he went to the login page and entered a username and password?