Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Target Path: Redirecting an Anonymous User

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

After changing the access_control back to ROLE_ADMIN:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_ADMIN }
... lines 43 - 44

If we try to access /admin/comment again, we see that same "Access Denied" page: 403 forbidden.

Customizing the Error Page

Like with all the big, beautiful error pages, these are only shown to us, the developers. On production, by default, your users will see a boring, generic error page that truly looks like it was designed by a developer.

But, you can - and should - customize this. We won't go through it now, but if you Google for "Symfony error pages", you can find out how. The cool thing is that you can have a different error page per status code. So, a custom 404 not found page and a different custom 403 "Access Denied" page - with, ya know, like a mean looking alien or something to tell you to stop trying to hack the site.

Redirecting Anonymous Users: Entry Point

Anyways, I have a question for you. First, log out. Now that we are anonymous: what do you think will happen if we try to go to /admin/comment? Will we see that same Access Denied page? After all, we are anonymous... so we definitely do not have ROLE_ADMIN.

Well... let's find out! No! We are redirected to the login page! That's... awesome! If you think about it, that's the exact behavior we want: if we're not logged in and we try to access a page that requires me to be logged in, we should totally be sent to the login form so that we can login.

The logic behind this actually comes from our authenticator. Or, really, from the parent AbstractFormLoginAuthenticator. It has a method - called start() - that decides what to do when an anonymous user tries to access something. It's called an entry point, and we'll learn more about this later when we talk about API authentication.

Redirecting Back on Success

But for now, great! Our system already behaves like we want. But now... check this out. Log back in with spacebar1@example.com, password engage. When I hit enter, where do you think we'll be redirected to? The homepage? /admin/comment? Let's find out.

We're sent to the homepage! Perfect, right? No, not perfect! I originally tried to go to /admin/comment. So, after logging in, to have a great user experience, we should be redirected back there.

The reason that we're sent to the homepage is because of our code in LoginFormAuthenticator. onAuthenticationSuccess() always sends the user to the homepage, no matter what:

... lines 1 - 18
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 21 - 71
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
return new RedirectResponse($this->router->generate('app_homepage'));
}
... lines 76 - 80
}

Hmm: how could we update this method to send the user back to the previous page instead?

Symfony can help with this. Find your browser, log out, and then go back to /admin/comment. Whenever you try to access a URL as an anonymous user, before Symfony redirects to the login page, it saves this URL - /admin/comment - into the session on a special key. So, if we can read that value from the session inside onAuthenticationSuccess(), we can redirect the user back there!

To do this, at the top of your authenticator, use a trait TargetPathTrait:

... lines 1 - 17
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
use TargetPathTrait;
... lines 23 - 87
}

Then, down in onAuthenticationSuccess(), add if $targetPath = $this->getTargetPath(). This method comes from our handy trait! It needs the session - $request->getSession() - and the "provider key", which is actually an argument to this method:

... lines 1 - 19
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 22 - 74
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
... line 78
}
... lines 80 - 81
}
... lines 83 - 87
}

The provider key is just the name of your firewall... but that's not too important here.

Oh, and, yea, the if statement might look funny to you: I'm assigning the $targetPath variable and then checking to see if it's empty or not. If it's not empty, if there is something stored in the session, return new RedirectResponse($targetPath):

... lines 1 - 19
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 22 - 74
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
... lines 80 - 81
}
... lines 83 - 87
}

That's it! If there is no target path in the session - which can happen if the user went to the login page directly - fallback to the homepage:

... lines 1 - 19
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator
{
... lines 22 - 74
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->router->generate('app_homepage'));
}
... lines 83 - 87
}

Let's try it! Log back in... with password engage. Yea! Got it! I know, it feels weird to celebrate when you see an access denied page. But we expected that part. The important thing is that we were redirected back to the page we originally tried to access. That's excellent UX.

Next - as nice as access controls are, we need more granular control. Let's learn how to control user access from inside the controller.

Leave a comment!

29
Login or Register to join the conversation
Default user avatar
Default user avatar Peter Bowyer | posted 3 years ago

How can I specify the target path via a hidden input in my form? Every page has the login form embedded in it on my site, so there isn't the initial redirect to the Login page to set the target path. Once logged in I want to redirect back to the page the user was on.

I've tried
<input type="hidden" name="_target_path" value="{{ path('current_page') }}"/>
but that doesn't work with Guard (I understand it's part of another login system in Symfony called `form_login` ¯\_(ツ)_/¯

16 Reply

Hey Peter,

Yeah, probably it won't work out of the box, though you can read the "_target_path" using Request object in your Guard authenticator and do a proper redirect :)

Cheers!

Reply

Use the following code if you care about readability:


public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
$targetPath = $this->getTargetPath($request->getSession(), $providerKey) ?:
$this->router->generate("app_homepage");

return new RedirectResponse($targetPath);
}
1 Reply
Abelardo L. Avatar
Abelardo L. Avatar Abelardo L. | posted 1 year ago

Hi there,

I don't know why but my app doesn't work properly.

Firstly, I don't follow your paths. My ones are:
path: "/" , undefined (no pages pointed);
path: "/login" shows the login form;
"/admin" enters in the admin zone (obvious).

I added an user into my database ("admin", role ["ADMIN"]...)

In prod environment:
_ when an anonymous user attempts enter into the "/admin" sees the "403" error page (I have a customized page) but my app doesn't redirect them to login page. -> is it correct?
_ when I login as admin, my app shows the 403 page. why?

This said, my access_control should be:


access_control:
- { path: ^/admin, roles: ADMIN } // the same role for the admin user in database;

and my onAuthenticationSucess method contains the following code:


public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($this->router->generate('app_login'));
}
return new RedirectResponse($this->router->generate('app_admin'));
}

To my mind, this logic is right for me....but in prod when I enter as "admin" it fails.

What should be the value of "target_path" for me? What is its role? What am I missing?

Best regards.

Reply

Hi again @AbelardoLG!

Hmm. So yes, your app is behaving oddly.

> when an anonymous user attempts enter into the "/admin" sees the "403" error page (I have a customized page) but my app doesn't redirect them to login page. -> is it correct?

This is not the correct behavior. Because your user is anonymous, it should trigger something called an "entry point", which usually is something that redirects to your login page. Can you paste your firewalls config? The entry point is usually something that is configured automatically for you based on your "authentication mechanisms" under your firewall.

> when I login as admin, my app shows the 403 page. why?

I think I know the problem here. There is a rule with roles: they must start with ROLE_. So, when you set the role on your User, use ROLE_ADMIN instead of ADMIN. Then protect it in the access control with ROLE_ADMIN. Here is some background about why this is a requirement, if you're curious: https://symfonycasts.com/sc...

> and my onAuthenticationSucess method contains the following code

Your onAuthentiationSuccess is definitely not the problem in this situation - so you can rest easy about that :).

Cheers!

1 Reply
Abelardo L. Avatar

Hi there!
First of all, thanks for replying to my question.

Secondly, here is my security.yaml file:


security:
encoders:
App\Entity\User:
algorithm: auto
# 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: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: lazy
provider: app_user_provider
http_basic:
realm: Secured Area
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_logout
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication

# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true

# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/admin, roles: ROLE_USER }
- { path: ^/page/1, roles: [ADMIN, PAGE_1] }
# - { path: ^/profile, roles: ROLE_USER }

Thanks for helping me! :) Brs.

Reply

Hey @AbelardoLG!

Ok, so you have 2 authentication mechanisms: http_basic and your LoginFormAuthenticator. Each of these has an entry point inside of them - entry point is logic that says "what should we do if an anonymous user tries to access a protected page?". When you have multiple authentication mechanisms, Symfony will choose one of them, unless you explicitly tell it which one to use. I believe it's choosing your LoginFormAuthenticator.

So, a few questions:

1) What behavior *do* you want when an anonymous user tries to access a protected page? Probably redirect to the login page?

2) Does your LoginFormAuthenticator have a start() method? That is the method that's called when an anonymous user tries to access a protected page (that function is your entry point). If you're extending AbstractLoginFormAuthenticator, then it's implemented for you - https://github.com/symfony/... - which would not explain why nothing is happening in your case.

Anyways, let me know - we're close to figuring out what's wrong :).

Cheers!

Reply
Abelardo L. Avatar

Hi there!
1) Yes: a user without any session goes to /page/1 or /page/2 -> the system redirects the user to a login page with a 302 HTTP response code
2) No, I haven't overriden that method. Yes, my LoginFormAuthenticator implements AbstractFormLoginAuthenticator
Cheers!

Reply

Hey @AbelardoLG!

Ok then! So, the "start" method in AbstractFormLoginAuthenticator should already be called whenever an anonymous user accesses a protected page. Based on what you're telling me, that may or may not actually be happening. So, let's do some debugging!

1) When an anonymous user tries to access a protected page, what is the error you see *exactly*? You said it's a 403, but what is the exact error message from Symfony? A screenshot would be best ;).

2) Try opening the core AbstractFormLoginAuthenticator. Inside the start() method (on the VERY first line), add a die('here'). Now, when an anonymous user tries to access a protected page, do you see this "here"? Or do you still see the 403 page?

Cheers!

1 Reply
Abelardo L. Avatar

Hi there,
Finally, I opted by the http basic authentication due to a constraint in my requirements; therefore, the AbstractFormLoginAuthenticator (and the related code) were removed from my code.

My app works fine :)

Thanks for your time&help for solving my issue. :)

Warmest regards, weaverryan

1 Reply
Eduard Avatar

In this video we made sure that the user is redirected after the login to the same page that he tried to access without being logged in.
How can I exclude routes from this? I ran into an issue where my users get redirected to a page after login which I don't want. For example an api route which was called from the frontend and returns json.

Don't get me wrong here I want to keep this functionality in general I just want exclude some routes so that the user is never redirected to them right after logging in.

This is the code I'm talking about : (class LoginFormAuthenticator)


public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
if($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}

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

Hey Ecludio,

I see. Well, it's possible, while you're in PHP code - you can do whatever you want before returning the response. It means, that you can check if the targetPath does not match some undesirable routes, and if it's not - redirect to the targetPath. But if it does match - redirect to homepage for example :) I suppose you can use "strpos()" for this kind of check, or more complex "preg_match()" for some regular expressions. Unfortunately, there's is no such functionality out of the box.

I hope this helps!

Cheers!

Reply
Eduard Avatar

Thank you very mutch. It worked wonderful!

Reply

Hey Ecludio,

Perfect! Thanks for confirming it works for you :)

Cheers!

Reply
Akavir S. Avatar
Akavir S. Avatar Akavir S. | posted 2 years ago

Hello symphony cast !
What if i want to catch the target from a direct href pointing on 'app_login' ? Is it not risky to store _security.main.target_path from controller ? What if someone spam the request ?

Reply

Hey Virgile,

How exactly? Could you give a little example? I don't see we save "_security.main.target_path" from controller in this chapter...

Cheers!

Reply
Akavir S. Avatar

Hey Victor,

I used it because i did want to save the target path even if the user came directly from href="app_login". So i wonder, if there is a risk if i save the target path each time someone is reaching the page where i use it (_security.main.target_path).
I wonder, what if someone discover this feature of my app, is there a risk that this person could "overflow the session" by spaming the url ?

Reply

Hey Virgile,

Well, if you afraid of overflowing your session - you might want to add an extra check before setting that target path. And if something is weird and you think it's spam - set it to default URL for example. But even without any checks it should not make any harm I think, at least if you do not do any update/delete actions on your website via GET method, i.e. use POST or DELETE for it. GET methods should not be harmful at all, because they are just for getting information, not modifying or deleting it.

Cheers!

Reply
Akavir S. Avatar

Awesome thanks for your clear answer <3

Reply
Default user avatar

i'm getting "Unable to generate a URL for the named route "routename" as such route does not exist".
Anyone got this one ? It has something to do with annotations because i was able to find the route that i declared manually in routes.yaml.

Reply
Default user avatar

I don't why it didn't work before but it works now :|

Reply

Hey Wuwu,

It might be due to cache, so it's always good to try to clear the cache first. If the problem still exist - then dig more deep :) Glad to hear it works for you now

P.S. you can use "bin/console debug:router" for debugging the route names and URLs in your application - it's pretty useful.

Cheers

Reply
Viktor Z. Avatar
Viktor Z. Avatar Viktor Z. | posted 3 years ago

Hi!
I give all users during registration process "ROLE_UNACTIVATED".
How is it possible to redirect all users with ROLE_UNACTIVATED from any site page to an activation page?
Thanks a lot!

Reply

Hey Viktor

You can add a subscriber php bin/console make:subscriber and listen to the kernel.request event. Such subscriber would check for logged in user roles and if it has that role, then you can redirect him to wherever you need to

Cheers!

Reply
Dan_M Avatar

Hey guys,

Redirecting after login works if the target path is listed the access_control part of my security.yaml. However, if I have a controller with the following code,

        
$user = $this->getUser();
if (null === $user) {
return new RedirectResponse($this->container->get('router')->generate('app_login'));
}


The target path is null in my LoginFormAuthenticator.

How do I set the target path before returning the redirect response?

Thanks!

Reply

Hey Dan Meigs

You need to set the new route into the user session by doing something like this:


$request->getSession()->set('_security.main.target_path', $returnPath);

Cheers!

Reply
Gaston P. Avatar
Gaston P. Avatar Gaston P. | posted 4 years ago

This content is the same as previous chapter

Reply

Hey guys,

Thanks for mention it. Yeah, this is just a draft yet and looks like it was fixed already :)

Cheers!

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
    }
}