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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "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.2.7
        "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
    }
}