If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWe 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 | |
} |
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.
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!
Hey ahmadmayahi!
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!
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.
Hey m3tal
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!
// 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
}
}
What is the difference between
AbstractGuardAuthenticator
andSimplePreAuthenticatorInterface
when it comes to API authentication?