Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Entry Point: Helping Users Authenticate

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

We now have a database table full of API Tokens where each is related to a User. I can already feel the API power! So here's our new goal: when an API request sends a valid API token string, we'll read it and authenticate that request as the User who owns the token:

... lines 1 - 9
class ApiToken
{
... lines 12 - 28
/**
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="apiTokens")
* @ORM\JoinColumn(nullable=false)
*/
private $user;
... lines 34 - 60
}

make:auth ApiTokenAuthenticator

This will be the second way that users can authenticate in our app. So, we need a second authenticator. Find your terminal and run:

php bin/console make:auth

If you see a question about choosing which type of authentication you want, choose an "Empty authenticator". I'm using an older version of the command, which only generates empty authenticators. Call it ApiTokenAuthenticator. Oh, and you may also be asked a question about an "Entry point". We'll talk about that soon, but choose the LoginFormAuthenticator option.

Ok, go check this out!

... lines 1 - 2
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
class ApiTokenAuthenticator extends AbstractGuardAuthenticator
{
public function supports(Request $request)
{
// todo
}
public function getCredentials(Request $request)
{
// todo
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// todo
}
public function checkCredentials($credentials, UserInterface $user)
{
// todo
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// todo
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// todo
}
public function start(Request $request, AuthenticationException $authException = null)
{
// todo
}
public function supportsRememberMe()
{
// todo
}
}

Hey! I know this class! It's that same, big, adorable empty authenticator we saw earlier. To tell Symfony to use this, open config/packages/security.yaml and add the new class under authenticators:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
guard:
authenticators:
... line 26
- App\Security\ApiTokenAuthenticator
... lines 28 - 58

If you're using that newer, fancier version of this command, it already did this for you. Lucky you!

As soon as we do this, the supports() method will be called at the beginning of every request. But... refresh. Woh! Big error!

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

What is an Entry Point?

If you did not see this error, it's your lucky day! Well, really, it's because the newer make:auth command took care of this step for you! But, it is important to understand. Move back to security.yaml and, under guard, make sure you have key called entry_point. Your make:auth command probably added it for you. If not, add it, copy the LoginFormAuthenticator class and paste:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
guard:
... lines 25 - 28
# redirect anonymous users to the login page
entry_point: App\Security\LoginFormAuthenticator
... lines 31 - 61

So... what the heck is an entry point anyways? Your firewall has exactly one "entry point" and its job is simple: to determine what should happen when an anonymous user tries to access a protected page. So far, if we, for example, went to /admin/comment without being logged in, our "entry point" has been redirecting users to /login.

But, where does that entry point code live? Actually, it's inside our LoginFormAuthenticator! Ok, really, it's in the parent class. Hold Command or Ctrl and click to open AbstractFormLoginAuthenticator.

Every authenticator has a method called start() and it is the entry point. This is the method that Symfony calls when an anonymous user tries to access a protected page. And, no surprises: it redirects you to the login page.

Nice! Except... there's a slight problem: while you can have as many authenticators as you want for a firewall, you can only have one entry point. Why? Think about it: when an anonymous user tries to access a protected page, well, they're not using any of our authenticators yet: it's just an anonymous user sending no authentication info. So, Symfony doesn't know which of your authenticators it should use as the entry point. That's why we need to tell it specifically which authenticator's start() method to use.

In our app, we will always redirect anonymous users to the login form. Of course, if you want to make this logic smarter, you could override the start() method in LoginFormAuthenticator and make it do different things under different conditions. Like, maybe you return an API response instead of redirecting if the URL starts with /api.

Anyways, when we refresh now, it works just like we expect: it redirects us to /login. Log back in with password engage and.... awesome! We're back!

Time to start filling in our authenticator!

Leave a comment!

4
Login or Register to join the conversation

What is the difference between AbstractGuardAuthenticator and SimplePreAuthenticatorInterface when it comes to API authentication?

1 Reply

Hey Ahmad Mayahi!

Great question! They are in fact quite similar :). Here is the full story:

1) Years ago, because custom security was hard, the SimplePreAuthenticatorInterface system was added to Symfony
2) A few years later, we realized that a lot of things were *still* too hard. So, we built Guard, which is similar to pre-auth, but makes your life even easier and gives you more power.
3) A few weeks ago, we finally agreed that we should fully recommend Guard and the "pre-auth" system was deprecated. Starting in Symfony 4.2, you'll see deprecation warnings when using it.

So, you're seeing a bit of the development & improvement cycle inside of Symfony :).

Cheers!

7 Reply
m3tal Avatar

Hi.

I have this in my security.yaml

anonymous: true
lazy: true
json_login:
check_path: app_login
username_path: email
password_path: password

and if I add the

guard:
authenticators:
- App\Security\LoginAuthenticatorFormAuthenticator
entry_point: App\Security\LoginAuthenticatorFormAuthenticator

I receive the error

[Application] Feb 16 11:47:00 |INFO | REQUES Matched route "app_login". method="POST" request_uri="https://127.0.0.1:8000/login" route="app_login" route_parameters={"_c
ontroller":"App\\Controller\\SecurityController::login","_route":"app_login"}
[Application] Feb 16 11:47:00 |DEBUG | SECURI Checking for guard authentication credentials. authenticators=1 firewall_key="main"
[Application] Feb 16 11:47:00 |DEBUG | SECURI Checking support on guard authenticator. authenticator="App\\Security\\LoginAuthenticatorFormAuthenticator"
[Application] Feb 16 11:47:00 |DEBUG | SECURI Calling getCredentials() on guard authenticator.
[Application] Feb 16 11:47:00 |DEBUG | SECURI Passing guard token information to the GuardAuthenticationProvider
[Application] Feb 16 11:47:00 |INFO | SECURI Guard authentication failed.

What can be the problem?

thank you.

Reply

Hey bogdan macsim

The first thing to check is the JSON data you're sending matches to the username and password paths you have configured. If that's correct, then, double-check that such a user exists in your system. If nothing worked, I'm afraid I'd need to see your authenticator's code

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