The Entry Point: Inviting Users to Log In

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

Log back in using abraca_admin@example.com and password tada. When we go to /admin, like we saw earlier, we get "Access Denied". This is because of the access_control... and the fact that our user does not have ROLE_ADMIN.

But if we change this to ROLE_USER - a role that we do have - then access is granted:

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

And we get to see some pretty impressive graphs.

Let's try one more thing. Log out - so manually go to /logout. Now that we are not logged in, if I went directly to /admin: what should happen?

Well, right now, we get a big error page with a 401 status code. But... that's not what we want! If an anonymous user tries to access a protected page on our site, instead of an error, we want to be super friendly and invite them to log in. Because we have a login form, it means that we want to redirect the user to the login page.

Hello Entry Point!

To figure out what to do when an anonymous user accesses a protected page, each firewall defines something called an "entry point". The entry point of a firewall is literally a function that says:

Here's what we should do when an anonymous user tries to access a protected page!

Each authenticator under our firewall may or may not "provide" an entry point. Right now, we have two authenticators: our custom LoginFormAuthenticator and also the remember_me authenticator:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 25 - 27
remember_me:
secret: '%kernel.secret%'
signature_properties: [password]
#always_remember_me: true
... lines 32 - 44

But neither of these provides an entry point, which is why, instead of redirecting the user to a page... or something different, we get this generic 401 error. Some built-in authenticators - like form_login, which we'll talk about soon - do provide an entry point... and we'll see that.

Making our Authenticator an Entry Point

But anyways, none of our authenticators provide an entry point... so let's add one!

Open up our authenticator: src/Security/LoginFormAuthenticator.php. If you want your authenticator to provide an entry point, all you need to do is implement a new interface: AuthenticationEntryPointInterface:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 89
}

This requires the class to have one new method... which we actually already have down here. It's called start(). Uncomment the method. Then, inside, very simply, we're going to redirect to the login page. I'll steal the code from above:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 83
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new RedirectResponse(
$this->router->generate('app_login')
);
}
}

And done!

As soon as an authenticator implements this interface, the security system will notice this and start using it. Literally, if an anonymous user tries to access a protected page, it will now call our start() method... and we're going to redirect them to the login page.

Watch: refresh! Boom! It knocks us over to the login page.

A Firewall has Exactly ONE Entry Point

But there's one important thing to understand about entry points. Each firewall can only have one of them. Think about: at the moment we go to /admin as an anonymous user.... we're not trying to log in via a login form... or via an API token. We're truly anonymous. And so, if we did have multiple authenticators that each provided an entry point, our firewall wouldn't know which to choose. It needs one entry point for all cases.

Right now, since only one of our two authenticators is providing an entry point, it knows to use this one. But what if that were not the case? Let's actually see what would happen. Find your terminal and generate a second custom authenticator:

symfony console make:auth

Make an empty authenticator... and call it DummyAuthenticator.

Beautiful! Like This created a new class called DummyAuthenticator:

... lines 1 - 2
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
// public function start(Request $request, AuthenticationException $authException = null): Response
// {
// /*
// * If you would like this class to control what happens when an anonymous user accesses a
// * protected page (e.g. redirect to /login), uncomment this method and make this class
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntrypointInterface.
// *
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
// */
// }
}

And it also updated custom_authenticator in security.yaml to use both custom classes:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator:
- App\Security\LoginFormAuthenticator
- App\Security\DummyAuthenticator
... lines 27 - 46

In the new class, inside supports(), return false:

... lines 1 - 11
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
return false;
}
... lines 18 - 43
}

We're... not going to turn this into a real authenticator.

If we stopped right now... and tried to go to /admin, it would still use the entry point from LoginFormAuthenticator. But now implement AuthenticationEntryPointInterface:

... lines 1 - 10
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 38
}

And then go down... and uncomment the start() method. For the body, just dd() a message... because this won't ever be executed:

... lines 1 - 12
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 34
public function start(Request $request, AuthenticationException $authException = null): Response
{
dd('DummyAuthenticator::start()!');
}
}

Now the firewall will notice that we have two authenticators that each provide an entry point. And so, when we refresh any page, it panics! The error says:

Run for you liiiiives!

Oh wait, it actually says:

Because you have multiple authenticators in firewall "main", you need to set the entry_point key to one of your authenticators.

And it helpfully tells us the two authenticators that we have. In other words: it's making us choose.

Copy the entry_point key... then, anywhere under the firewall, say entry_point: and then point to the LoginFormAuthenticator service:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
entry_point: App\Security\LoginFormAuthenticator
... lines 25 - 47

Technically we can point to any service that implements AuthenticationEntryPointInterface... but usually I put that in my authenticator.

Now... if we go back to /admin.... it works fine! It knows to choose the entry point from LoginFormAuthenticator.

Speaking of LoginFormAuthenticator... um... we've been doing way too much work inside of it! That's my bad - we're doing things the hard way for... ya know... "learning"! But next, let's cut that out and leverage a base class from Symfony that will let us delete a bunch of code. We're also going to learn about something called TargetPathTrait: an intelligent way to redirect the user on success.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.4.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}