Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Redirecting on Success & the User Provider

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

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 spacebar1@example.com!

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!

60
Login or Register to join the conversation
Default user avatar
Default user avatar Kristof Kreimeyer | posted 4 years ago

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.

10 Reply
picks Avatar
picks Avatar picks | posted 3 years ago | edited

Hi, I would like to redirect my users depending on their role. If the user has <i>ROLE_ADMIN</i>, I would like it to be redirected to <b>easyadmin</b> route. Else, users have to be redirected to <b>app_homepage</b>. I modified my <b>onAuthenticationSuccess</b> function to

`
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)

{
    $roles = $token->getRoleNames();
    if (in_array('ROLE_ADMIN', $roles, true)) {
        return new RedirectResponse($this->urlGenerator->generate('easyadmin'));
    }

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

    return new RedirectResponse($this->urlGenerator->generate('app_homepage'));
}

`

which is working well on a "normal" authentication.

But if my user with <i>ROLE_ADMIN</i> has checked "remember_me" checkbox, it is not redirected to <b>easyadmin</b> but to <b>app_homepage</b> like a "normal" user...
Could you please help? I tried to trigger this through a listener, but nothing worked so far...

1 Reply
picks Avatar
picks Avatar picks | picks | posted 3 years ago | edited

HI picks haven't seen your answer either!
In the meantime I tired to create a listener on the InteractiveLoginEvent, but it just does the same: it is called when the user log in "normally", but is the session is still active or there is a remember_me cookie, it is not. I can't find which even to target that fires at EVERY SINGLE login...

Reply

Hey picks!

> I can't find which even to target that fires at EVERY SINGLE logi

Do you literally want to run some code on *every* request where a user is authenticated (regardless of if they just logged in or were already logged in via a cookie)? If so, I would register a listener on kernel.request (called RequestEvent also in newer Symfony versions). Inside, check if the user is logged in, and then do something. The point is: I don't think you need to hook into security to do this: just allow security to "do it's thing", then register a listener after and check if the user is authenticated or not.

But... I may be missing the big picture - it's been a few months!

Cheers!

Reply
picks Avatar
picks Avatar picks | weaverryan | posted 3 years ago | edited

weaverryan So no, not on every request, just when the user arrives on the app actually... But maybe I can add a condition in the listener that would trigger the check only when the user tries to reach the login or homepage... or somthing like this.

Reply

Hey picks !

Sorry for the slow reply!

> just when the user arrives on the app actually

So that's tricky... because it depends on what you mean by this :). Do you mean:

A) When they log in?
or
B) When they come to the site... but maybe they are already logged in... but they haven't been there in awhile

For (A), you're looking for the InteractiveLoginEvent. But as you said earlier, that's only when they *actually* log in - it doesn't trigger if the session is already set up - i.e. if they are already authenticated. And so that's why I think (?) you really want situation (B). What's your use-case exactly? If the user hasn't been "active" on the site for awhile... and I want to do something when they *become* active, I would:

1) Set a lastActiveAt DateTime on the User and set it (via a listener to kernel.request) on every request (when the user is authenticated).
2) Before setting this (in the same listener), I would read the existing value, compare it to NOW, and if it's "been awhile" since they were active, run whatever code you need.

Let me know if I'm still missing the use-case - it's a bit fuzzy :).

Cheers!

Reply
picks Avatar
picks Avatar picks | weaverryan | posted 3 years ago | edited

weaverryan to be clearer, my use case is this:

  • my whole app requires to log in to access anything (it is an extranet)
  • if user 1 is a simple customer, then he can be redirected to the homepage (or any other page he tried to access) after the login
  • if user 2 is an admin (has ROLE_ADMIN), then I want him to be redirected to the /admin (easyadmin) part of the website, not the homepage or any other page

This being said, my app uses the Security feature "remember_me" which sets - if I'm correct - a cookie when it is activated. So I have also two different "login" cases:

  • the user doesn't have the cookie => in this case the login form is displayed, and I can listen to the <b>onSecurityInteractiveLogin</b> event to redirect the user (within a Listener)
    `public function onSecurityInteractiveLogin(InteractiveLoginEvent $event)
    {
    $token = $event->getAuthenticationToken();
    $roles = $token->getRoleNames();
    if (in_array('ROLE_ADMIN', $roles, true)) {
       return new RedirectResponse($this->urlGenerator->generate('easyadmin'));
    

    }
    }`

  • the user has the cookie => is this case I can't find any event to listen to make my redirection. The <b>onSecurityInteractiveLogin</b> is not called either.

Does it make more sense to you now?

Reply

Hey picks!

Yes, it makes more sense now... but maybe not all the sense... yet ;).

the user has the cookie => is this case I can't find any event to listen to make my redirection. The onSecurityInteractiveLogin is not called either

Let's talk about the exact "use flow" that you're trying to solve here. Obviously, if an admin users is actively clicking around the admin section, you don't want to "intercept" that request and redirect them back to "/admin" :). So, is the user flow you want to achieve this?

If the user leaves the site (closes the browser, navigates away, whatever) and then access the site by going directly to the domain (i.e. the homepage) - like https://MyExtraNet.com, then if they are an admin, they should be redirected to the homepage.

Is this the case you want to solve? And, if that same user navigated away, then directly tried to go to https://MyExtraNet.com/admin/users (for example), you would do nothing and allow them to visit this page, right?

If I am correct that this is the user flow you want (and that's a big if! Please tell me if I'm still wrong), then this sorta has nothing to do with authentication. There is no way for Symfony (or any backend technology) to know that the user navigated away for a little bit, and then came back. What you would really want to do here is add a listener to kernel.request with this logic:


public function onKernelRequest(RequestEvent $event)
{
    $request = $event->getRequest();

    // if on the homepage
    if ($request->getPathInfo() === '/') {
        $event->setResponse(new RedirectResponse($this->router->generate('easyadmin'));
    }
}

Does that makes sense? Or am I still "off" on what you want to accomplish. Let me know!

Cheers!

Reply

Hey picks !

Sorry for the slow reply :). Hmm. I'm not sure the cause, but I'd like you to check for the roles in a different way: inject the Security service and use is isGranted method - similar to what we do in voters: https://symfonycasts.com/screencast/symfony-security/custom-voter#checking-for-roles-inside-a-voter

The problem is that checking for the roles on the token (or the user object) is not exactly the same as using the isGranted() method - the isGranted method does several things that checking the roles directly does not do (for example, isGranted() also checks for role_hierarchy in security.yaml, which checking the roles directly does not).

So try this out and let me know how it goes. I can't explain (yet) what the cause is - but better to update it to the "right" way and ... we'll just see if that fixes things ;).

Cheers!

Reply
Michael L. Avatar
Michael L. Avatar Michael L. | posted 4 years ago

Hey, great video as always.

Im having a problem with redirecting my user. The connection process is functionnal but as soon as i change page my user goes back to anon.

1 Reply

Hey Michael L.!

Ah yes. So, what's *probably* happening is that your getting logged in successfully... then "losing" authentication immediately because of a problem loading the user's details from the session. Check out this conversation (and read the comments below as well) - https://symfonycasts.com/sc... - it contains some a debugging recommendation around using an EquatableInterface and an explanation of what's going on.

Let me know if that helps you discover anything!

Cheers!

1 Reply
Michael L. Avatar

Hey, thanks for the fast answer.

Im still having a problem. I tried to implement EquatableInterface and it's required isEqualTo method. I also dont have any serialized function in my user class.

I followed the course from the beginning

Reply

Hey Michael L.!

Hmm, interesting. Well, that at least (most likely) eliminates the possibility that it was the "serialization" thing mentioned on that other comment. So here's one other thing you can do to debug:

1) Go through the login process like normal and allow it to redirect you
2) Now go to https://localhost:8000/_profiler - you should see a list of the most recent requests into your app, with the newest on top. The 2nd (or maybe 3rd) on the list will be the POST request to /login. Click the little "sha" link (e.g. abc1234) on the right to open the profiler for that request.
3) On the left side, find the security tab and click it.

You're now looking at the security information from the login POST request - *before* redirecting. The key thing we're looking for here is: does it show you as properly authenticated or not?

A) If yes, then we DO have some sort of a session problem - because you're authenticated here, but it's being lost after the redirect
B) If no, then the problem is actually during authentication somehow - something is wrong with your authenticator, for example.

Let me know what you find out!

Cheers!

1 Reply
Michael L. Avatar

Finally got it working !

It was indeed a session problem and a kinda dummy one haha.

In an other project i limited "session.cookie_domain = " to a specific domain. This disabled access to cookies in this project hence the issue.

Thank you for your time :)

1 Reply
Szabolcs Avatar
Szabolcs Avatar Szabolcs | posted 2 years ago

Hi! Another day, another question :-p. I have watched this tutorial many times, but I still don't understand, how the session-handling works after the LoginFormAuthenticator has authenticated the user.

So the method "checkCredentials()" is called and than the "onAuthenticationSuccess()"-method. On "onAuthenticationSuccess()" we just return an RedirectResponse. But who creates the session-data about the loggedin-user? Is any way to customize this session-data? And the user-provider, which will read this session-data in the next request, is this also an security-guard-authenticator? Because in the end, it also just authenticates the user based on this session-data. Don't know!! I hope my question is not stupid :-D, I understand the idea about the LoginFormAuthenticator, just these magic behind the session-handling is not clear to me.

Thanks!

Reply

Hey Cseh,

Oh, tough questions :) Well, it happens somewhere in the core of Symfony Security, fairly speaking I don't know where exactly at a glance. Hm, first of all, why do you need to customize the way Symfony creates that logged in user in session? Probably you can later just add something new to your session, i.e. add something instead of modifying it at that exact moment? But in case you can to modify, there's a way: Symfony calls serialize()/unserialize() on the User object, and so if you will override those methods in your User class and add more information to the final erialized string - that will be autoamtically stored by Symfony in the session, so I believe that's exactly what you need.

I hope this helps!

Cheers!

Reply
Szabolcs Avatar

By the way, I have read the user-provider-documentation (https://symfony.com/doc/cur..., so an user-provider restores the user based on session and is not an security-guard but its doing the same job in the end: authentication :-D. So my question is just, where the session-data is written and how to customize. Thanks!

Reply
Metin Ö. Avatar
Metin Ö. Avatar Metin Ö. | posted 2 years ago

So, I have another dumb question:
Let's assume a user on my site would like to start a flow with a parameterized url, which can only be executed after login. Usually it ends up on the login page and s/he gets redirected after login success. But what if the user does not have an account and I want to transfer the parameters from the aforementioned URL to the reg process? How do I intercept the initial url + params? and e.g. save it to a session or other storages.
Thank you so much!

Reply

Hi @metin!

That’s actually an *excellent* question! It’s a 2 step process:

1) after registration, make sure to manually authenticate the user. Doing this will re-use the onAuthenticationSuccess from your authenticator, which already knows to redirect to the previous url:

https://symfonycasts.com/sc...

2) and... well, that might be all you need! Consider this flow:

A) user goes to some page they cannot access - like /download/secure/5
B) security redirects them to the login page. But of course, security also just stores the previous url (/download/secure/5) in the session
C) the user clicks to register
D) after registration, thanks to the code in step (1), the onAuthenticationSuccess reads the original url from the session (/download/secure/5) and redirects there.

Done!

Let me know if this fits your situation, or if I missed a complication :).

Cheers!

Reply
Gunjeet K. Avatar
Gunjeet K. Avatar Gunjeet K. | posted 2 years ago

Hello,
My requirement is to share session with legacy application. I tried to use:

$session = new Session(new PhpBridgeSessionStorage());
$session->start();
But this does not work. I found the below code in stackoverflow and it worked:

$storage = new NativeSessionStorage();
$session = new Session($storage, new NamespacedAttributeBag());

$session->start();
$session->replace($_SESSION);
$session->save();
$request->setSession($session);

$session = new Session(new PhpBridgeSessionStorage());
$session->start();
But then session does not persist. It is lost in another request
Am struggling with sharing sessions and authentication. I don't find anything for PhpBridgeSessionStorage in SymfonyCasts. Kindly help.

Thank You.

Reply

Hey Gunjeet K.!

I've done my best to answer this tricky question over here: https://symfonycasts.com/sc...

Cheers!

Reply
Gunjeet K. Avatar

Thank you so much!!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

Btw, once you have made a make:entity. What is the best way to add / adujust the entitiy so migrations sync up?
I searched in the docs how to add a new column to an existing table, but I couldn't find anything. I think you explained this at the last episode of Symfony 5 > Doctrine, Symfony & the Database, by tricking the DateTime field. I am curious wether there are other ways.

Reply

Hey @Farry7!

You can always edit an entity class by hand of course. But to really answer your question, you can run make:entity again - that command is smart enough to create new entity classes or updating existing ones :). That's what I do. Then, if I need to tweak something further, I do it manually, like with the DateTime field you mentioned.

Cheers!

Reply
Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

Can you tell me where you created the Basefixtures? I am working on my own project which is slightly different than Couldren Overflow. Right now I only created UserFactory and AppFixtures. I followed the tutoral at Symfony 5 > Doctrine, Symfony & the Database. I can't remember that you created BaseFixtures there.

So what is the best way for me to continue, you think?

Reply

Hey @Farry7!

I've answered this more completely over here - https://symfonycasts.com/sc...

But to answer your question, you can either:

A) Code along with this tutorial using the starting code from this tutorial (that's the intended path)
B) Keep your code, but you will need to make some adjustments along the way. For example, in the Symfony 4 tutorials, I used a different system for loading fixtures. In Symfony 5, I use Foundry, which I much prefer.

Let me know if you have any other questions or doubts.

Cheers!

Reply
Farshad Avatar

Thanks for all your help so far. Do you maybe know on which course / chapter I can learn about Foundry? I can not remember if it was already in one of the Symfony 5 courses. Foundry sounds fimiliar though.

Reply
Farshad Avatar

Hi, at https://symfonycasts.com/sc... you don't talk about the Basefixtures.php
It's about logout.

Reply

Hey @Farry7!

The link above - https://symfonycasts.com/sc... - was to my comment conversation with you on that page - I didn't mean to refer you to that video :).

Cheers!

Reply

Hello, can you help me to fix this?

<b>I allow the users to login through two ways:</b>
1 - from the "/login" page
2 - from everywhere by using a modal with the same form used on "/login".

<b>The login works good, but the redirections not. I have three scenarios:</b>
1 - The user login being redirected from a secure page. I use your method $this->getTargetPath($request->getSession(), $providerKey).<b> This works perfect.</b>
2 - The user login using the modal, from any page. In this case I want to redirect the user back to that specified page. I do that using the referral method: return new RedirectResponse($request->headers->get("referer"));.<b> It works well, but I don't think it's perfect.</b>
3 - The user login from the "/login" page and I want to redirect the user to a specific route, let's call the route "app_homepage". <b>I don't know how to do this.</b>

<b>Here is what I try to do:</b>
`
//case 1
if($targetPath = $this->getTargetPath($request->getSession(), $providerKey)){
return new RedirectResponse($targetPath);
}

//case 3
if(login form page was "/login"){
return new RedirectResponse($this->router->generate('app_homepage'));
}

//case 2
return new RedirectResponse($request->headers->get("referer"));`

Thank you

Reply

Hey Alex,

I think I can give you a few tips!

About case "2" - you can add a hidden field to your popup form called "_target_path" (that's default name), see https://symfony.com/doc/cur... - this will help to determine where to redirect users when they use that form in popup instead of doing it via referer. Using referer is probably OK too, nice catch! But I'd recommend you to add some extra checks to make sure the URL you're going to redirect users not empty and is *your* website's URL just in case, as referer might be a different website - just a sanity check. But as I said, probably using _target_path form field would be better (correct) in this case.

And in your "case 3", you're thinking correct! All that you need is to determine whether the user is trying to authenticated from /login page or other pages. Once again, look at "_target_path" value and if it's value is "/login" - redirect to homepage instead. You may want to inject RouterInterface to be able to generate your "/login" URL dynamically instead of hardcoding it, just in case it will be changed someday. But probably if you cover this spot with a test - nothing to worry about :)

I hope this helps!

Cheers!

1 Reply

Thank you victor . Your tips helped me a lot. I will post bellow the code just in case someone needs it:

<b>1 - Add "_target_path" as hidden to the login form and set it to the actual route.</b>

<input type="hidden" name="_target_path"
           value="{{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}"
    >```



<b>2 - Update `onAuthenticationSuccess` to</b>
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
    if($targetPath = $this->getTargetPath($request->getSession(), $providerKey)){
        return new RedirectResponse($targetPath);
    }

    if($request->request->get('_target_path') == $this->router->generate('app_login')){
        return new RedirectResponse($this->router->generate('app_homepage'));
    }else{
        return new RedirectResponse($request->request->get('_target_path'));
    }
}```

So now it cover all three cases.

Reply

Hey Alex,

Glad it helped! And thanks for sharing your final working solution :)

Cheers!

Reply
Default user avatar
Default user avatar Cristóbal Rejdych | posted 3 years ago | edited

Hi guys,
I am writing here because I had problem with RedirectResponse after success authentication. At first something about sign in process in my project:
I wanna do ajax call from reactjs component with login form and when data of user is correct do redirect to another page, but when its not: send via JsonResponse error message. Could You explain me why RedirectResponse don't work after ajax response?? When I go to profiler It's look like I was in properly route, but in url in my browser it's not and page content don't reload. At least I did that by send JsonResponse with url to page what I wanna redirect user after success:


    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return new JsonResponse(['url' => $this->router->generate('app_home')]);
    }

After success ajax call i take json response and use js to read it and redirect:

 $.ajax({
        url: '/login',
        method: 'POST',
        data: JSON.stringify(userData)
    }).then(function(destinationUrl) {
        window.location = destinationUrl['url'];
    });```


It's any better way to do that??

Thank you for the reply.
Reply

Hey Cristóbal Rejdych !

Excellent question :).

When you make an AJAX request and that AJAX request returns a "redirect" (a 301 or 302 response code), it does not (as you noticed) make your browser redirect. Instead, your browser will see that redirect and make another AJAX request to the URL of the redirect... but that doesn't have any effect on your browser.

Here is how I usually solve this:

A) If there is an authentication error, you return JSON. But make sure that the response status code is a 401 - because this is an error response, not a successful response. Inside jQuery, you can handle this case with a fail(function() {}).

B) If authentication is successful, I like to return a 204 status code (it means, success but a response with no content). I then add a Location header with the URL that the user should be redirected to. I use that in JavaScript (just as you've done) to change the window.location. Here's some info about returning this response - https://symfonycasts.com/screencast/api-platform-security/login-response#codeblock-d462cd0049 - I also think it would be fine to return some JSON with a url key like you've done.

I hope that helps! RedirectResponse is really something that's useful for "traditional" form submits, but not in an API/AJAX context.

Cheers!

Reply
Default user avatar
Default user avatar Cristóbal Rejdych | weaverryan | posted 3 years ago

Thanks a lot for your fast reply. It helps me a lot ;)

Reply
Markus B. Avatar
Markus B. Avatar Markus B. | posted 4 years ago

Hello, I want to log the last login from an user in the user table, I have two fields (ip and datetime) in my user table.
The application doesn't have a special entry site, where it redirect the user on success. So I thought I could implement it in the LoginFormAuthenticator (onAuthenticationSuccess) but I have problems to catch the user, add the parameters and save it.

Reply

Hey MarBod,

It sounds good to do in LoginFormAuthenticator I think, could you explain what problems exactly do you have? As I understand on onAuthenticationSuccess() you should be able to get the actual user.

Cheers!

1 Reply
Default user avatar
Default user avatar MajkellVZ | posted 4 years ago

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

Reply

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!

Reply
GDIBass Avatar
GDIBass Avatar GDIBass | posted 4 years ago

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

Reply

Hey GDIBass

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!

Reply

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?

Reply

Hey mickaelandrieu!

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!

2 Reply
Default user avatar
Default user avatar shing | posted 4 years ago | edited

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.

Reply

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/symfony/blob/3d2124e12874e6d55ce4922956e0ef9fda9f794c/src/Symfony/Component/Security/Core/Authentication/Token/AbstractToken.php#L234-L254. 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!

2 Reply
Default user avatar

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.

Reply

Hey Shing,

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

Cheers!

Reply
Default user avatar

ah ok. missed that 👍

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice