Chapters
-
Course Code
Subscribe to download the code!Compatible PHP versions: >=5.5.9
Subscribe to download the code!Compatible PHP versions: >=5.5.9
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Registering the Authenticator (Part 2)
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe authenticator class is done - well done enough to see it working. Next, we need to register it as a service. Open up app/config/services.yml
to add it: call it jwt_token_authenticator
. Set its class to AppBundle\Security\JwtTokenAuthenticator
:
Show Lines
|
// ... lines 1 - 5 |
services: | |
Show Lines
|
// ... lines 7 - 35 |
jwt_token_authenticator: | |
class: AppBundle\Security\JwtTokenAuthenticator | |
autowire: true |
And instead of adding an arguments
key: here's your permission to be lazy! Set autowire
to true
to make Symfony guess the arguments for us.
Finally, copy the service name and head into security.yml
. Under the firewall, add a guard
key, add authenticators
below that and paste the service name:
security: | |
Show Lines
|
// ... lines 2 - 8 |
firewalls: | |
main: | |
Show Lines
|
// ... lines 11 - 20 |
guard: | |
authenticators: | |
- 'jwt_token_authenticator' | |
Show Lines
|
// ... lines 24 - 32 |
As soon as you do that, Symfony will call getCredentials()
on the authenticator on every request. If we send a request that has an Authorization
header, it should work its magic.
Let's try it! Run our original testPOSTProgrammerWorks()
test: this is sending a valid JSON web token.
./vendor/bin/phpunit --filter testPOSTProgrammerWorks
And this time... it passes!
Hold on, that's pretty amazing! The authenticator automatically decodes the token and authenticates the user. By the time ProgrammerController
is executed, our user is logged in. In fact, there's one other spot we can finally fix.
Down on line 37, we originally had to make it look like every programmer was being created by weaverryan
:
Show Lines
|
// ... lines 1 - 18 |
class ProgrammerController extends BaseController | |
{ | |
Show Lines
|
// ... lines 21 - 24 |
public function newAction(Request $request) | |
{ | |
Show Lines
|
// ... lines 27 - 36 |
$programmer->setUser($this->findUserByUsername('weaverryan')); | |
Show Lines
|
// ... lines 38 - 50 |
} | |
Show Lines
|
// ... lines 52 - 191 |
} |
Without authentication, we didn't know who was actually making the API requests, and since every Programmer needs an owner, this hack was born.
Replace this with $this->getUser()
:
Show Lines
|
// ... lines 1 - 18 |
class ProgrammerController extends BaseController | |
{ | |
Show Lines
|
// ... lines 21 - 24 |
public function newAction(Request $request) | |
{ | |
Show Lines
|
// ... lines 27 - 36 |
$programmer->setUser($this->getUser()); | |
Show Lines
|
// ... lines 38 - 50 |
} | |
Show Lines
|
// ... lines 52 - 191 |
} |
That's it.
Our controller doesn't know or care how we were authenticated: it just cares that $this->getUser()
returns the correct user object.
Run the test again.
./vendor/bin/phpunit --filter testPOSTProgrammerWorks
It still passes! Welcome to our beautiful JWT authentication system. Now, time to lock down every endpoint: I don't want other users messing with my code battlers.
30 Comments
Hi Cesar C.!
The ApiProblem class should also work on Symfony 5 :). It's actually not a class from Symfony - it's one we created ourselves in episode 2 of this series - you can find it here - https://symfonycasts.com/sc...
Let me know if that helps!
Cheers!
Hey Ryan,
I can't authenticate via the custom jwt(token)authenticator :(
I get the Jwt(Token)Authenticator->start() method processed with returning a custom error message like this:
{
"error": "auth required",
"request": {
"attributes": {},
"request": {},
"query": {},
"server": {},
"files": {},
"cookies": {},
"headers": {}
},
"$authException->getToken": null,
"$authException->getMessageKey": "Authentication credentials could not be found.",
"$authException->getMessageData": []
}
Would be great if you could help me out. I have spend yesterday whole day on this issue :(
I've solved it! 🎉🎉🎉🎉🎉🎉
I had to add this line of code inside my supports() method in JwtAuthenticator!
public function supports( Request $request ) {
return $request->headers->has( 'Authorization' ) && 0 === strpos( $request->headers->get( 'Authorization' ), 'Bearer ' );
}
Found it in the final code of the new Security Tutorial inside the ApiTokenAuthenticator.php file.
Woohoo! Nice work Technomad!
Yes, the supports() method is the FIRST method that Symfony calls at the beginning of each request. If it returns false (which is what was happening before), then no other methods are called on your authenticator and your request continues anonymously. But then, if that same anonymous request tries to access page that requires login, Symfony "kicks" them out. Specifically, it alls the *start()* method on your authentication, which is where we tell the user that they need to send authentication information.
Anyways, nice work - and I hope it makes a bit more sense now!
Cheers!
Hello,
I'm trying to use this in my project. But I will always get stuck in the start() function.
- symfony/symfony: v3.4.1
- lexik/jwt-authentication-bundle: v2.5.4
When I debug my JwtTokenAuthenticator.php I get the following:
01] getCredentials(){
...
var_dump($token);die;
}
I get a token.
So when I debug:
02] getUser(){
...
var_dump($data);var_dump(date('d-M-Y H:i:s',$data['exp']));var_dump(date('d-M-Y H:i:s')); die;
}
Gives me:
array(3) {
["iat"]=>
int(1534753991)
["exp"]=>
int(1534757591)
["username"]=>
string(11) "tim@foo.com"
}
string(20) "20-Aug-2018 09:33:11"
string(20) "20-Aug-2018 08:33:11"
The ->findOneBy(['username' => $username]);
also gives me a valid $user object.
03] I expect to go to:
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
But instead I end up in:
public function start(Request $request, AuthenticationException $authException = null)
{
return new JsonResponse([
'error' => 'auth requirerd'
],402);
}
Can somebody explain to me why what is going wrong.
Hey Tim V.!
Hmm... this IS strange! So, a few questions:
1) Once you get your User object, you ARE returning this from getUser() correct? Could you post the important parts of your authenticator?
2) Are you 100% sure that checkCredentials()
is never called?
I'm asking these simple questions because the situation just doesn't make sense yet :). As you probably know, if you return a UserInterface object from getUser(), then checkCredentials() is ALWAYS called. Here's the core code that handles that: https://github.com/symfony/symfony/blob/2df7320c1c70a2a7938fcb583090e26a6c5439a5/src/Symfony/Component/Security/Guard/Provider/GuardAuthenticationProvider.php#L101-L113
The ONLY thing that is checked between these two methods is the $this->userChecker->checkPreAuth($user);
. Here is the core UserChecker - https://github.com/symfony/symfony/blob/2df7320c1c70a2a7938fcb583090e26a6c5439a5/src/Symfony/Component/Security/Core/User/UserChecker.php#L29 - it IS possible that this is causing some sort of failure - I would double check.
Let me know what you find out!
Cheers!
Hey Ryan,
1) This is my getUser():
public function getUser($credentials, UserProviderInterface $userProvider)
{
try {
$data = $this->jwtEncoder->decode($credentials);
} catch (JWTDecodeFailureException $e) {
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
$username = $data['username'];
// For debug only
$userObj = $this->em
->getRepository('AppBundle\Entity\User\User')
->findOneBy(['username' => $username]);
var_dump($userObj);die;
// End debug
return $this->em
->getRepository('AppBundle\Entity\User\User')
->findOneBy(['username' => $username]);
}
This is the User Object that I get from the var_dump().
object(AppBundle\Entity\User\User)#1141 (14) {
["id":protected]=>
int(1)
["username":protected]=>
string(11) "tim@foo.com"
["usernameCanonical":protected]=>
string(11) "tim@foo.com"
["email":protected]=>
string(11) "tim@foo.com"
["emailCanonical":protected]=>
string(11) "tim@foo.com"
["enabled":protected]=>
bool(false)
["salt":protected]=>
NULL
["password":protected]=>
string(88) "R44WYsdJmxHRz88jnfOEmDVUjXjHDcV8Ub1g6sivkRqiX7MEHkB6u2DKWJgRmZ/RGnISWg7TFkucx/VnpaeCVw=="
["plainPassword":protected]=>
NULL
["lastLogin":protected]=>
NULL
["confirmationToken":protected]=>
NULL
["passwordRequestedAt":protected]=>
NULL
["groups":protected]=>
NULL
["roles":protected]=>
array(0) {
}
}
2) This is the solution! When I dump the credentials I get:
PHPUnit 6.5.11 by Sebastian Bergmann and contributors.
Host: localhost:8000
Date: Mon, 20 Aug 2018 15:04:04 +0000, Mon, 20 Aug 2018 15:04:04 GMT
Connection: close
X-Powered-By: PHP/7.1.16
Cache-Control: no-cache, private
Content-Type: application/problem+json
{
"detail": "Account is disabled.",
"status": 401,
"type": "about:blank",
"title": "Unauthorized"
}
F 1 / 1 (100%)
Time: 1.23 seconds, Memory: 24.00MB
There was 1 failure:
1) AppBundle\Test\Controller\API\JobOfferControllerTest::testPOSTJobOfferWorks
Failed asserting that 401 matches expected 201.
That clearly tells me the User is disabled!!!
So Now I've updated my createUser class with:
$user->setEnabled(true);
And everything works Fine now.
Thank you very much!
Awesome! so the security check works properly by not allowing you to authenticate if the given user is disabled. Nice job man!
Hi guys,
I've implemented JWT Authenticator with Guard in my app just like the one in this tutorial, however the Symfony Debug bar shows the user as unauthenticated, is this expected behaviour?
Hey Shaun T.!
Hmm. MAYBE :). When you load the site in your browser, you're probably not (somehow) sending the JWT token, right? So it makes sense that you would not be authenticated? Or are you doing something interesting for this :).
If you use your JWT to make a few authenticated API requests to your system, you should then be able to go to /_profiler to see a list of those requests. If you click the link next to one of them, you'll enter into the profiler for that page. Then, click on the Security tab on the left to see if you are authenticated on that request. You *should* be there.
Cheers!
Aaah yes, I can see in the profile bar that each ajax request is authenticated, but the user remains unauthenticated in the bar. I presume this is because the state is stored in the client side when you setup a web app as an api, as opposed to a traditional web app where the authenticated user is stored is a server session variable.
Thanks for your help :)
You nailed it! Well also, because when you refresh the page, that bar at the bottom is final, "static" information about the request that just finished. Even if you DID login via another tab using a traditional session-based approach, the bar on the original page would still just sit there and say "Anonymous". The only magic-updating part of the bar is the cool part that shows the AJAX requests :).
Happy to help! Cheers!
[Symfony\Component\Config\Definition\Exception\InvalidConfigurationException]
Unrecognized option "guard" under "security.firewalls.main"
why Unrecognized ? cant move forward :-(
did as you said . help pls
Hey Rakib,
It's weird. Could you please show your current content of security.yml file? At least security.firewalls section. You could show it in comment here or better use GitHub Gist for that.
Cheers!
Hy, can you update the finish folder in the download project?
I think the TokenControllerTest is missing and the ProgrammerControllerTest is not up to date.
I followed your tutorial and I still get the 200 instead of the 201 status code for testPOSTProgrammerWorks.
I put an echo json_encode($request); in the getCredentials Function. And by running the test, i got an empty request:
{"attributes":{},"request":{},"query":{},"server":{},"files":{},"cookies":{},"headers":{}}
I don't know, whats going wrong?
Hey einue ,
The code in start/ and finish/ is different for this course. Also, status code should be 201 (not 200) as we have in "symfony-rest4/finish/src/AppBundle/Controller/ApiProgrammerController" file -> "newAction()" method -> and see 201 status code on line 45: "$response = $this->createApiResponse($programmer, 201);".
Do you watch videos or are you following this tutorial by course scripts and code blocks only? Because, right in this video I see we return 201 in newAction().
I'd be glad to help you to figure out this problem.
Cheers!
Hi victor , thanks for the fast repsonse. I followed the video tutorial. I also changed the status code in the newAction. Then i get the error: Failed asserting that 200 matches expected 201.
If I change the 'Authorization' key in getCredentials to 'Test' and also in the testPOSTProgrammerWorks-Method. It works fine.
It seems like the Authorization-Header doesn't work. I use a vagrant box with apache.
Hey einue!
Sorry for the late reply - for some reason Disqus put your comment in Spam :(.
Check out this issue: https://stackoverflow.com/q... - it is likely your problem!
Cheers!
It seems like the authorization header is saved in the apache_request_headers.
If I add this Code
if (!$request->headers->has('Authorization') && function_exists('apache_request_headers')) {
$all = apache_request_headers();
if (isset($all['Authorization'])) {
$request->headers->set('Authorization', $all['Authorization']);
}
}
at the beginning of getCredentials, it works with the authorization key. But that's in fact not a really nice solution... Now i implement an eventListener to fix that.
Hey einue ,
I believe you can get access to all those headers via Symfony Request object, i.e:
dump($request->headers->all());
I mean, you can avoid apache_request_headers() function call and that extra check with function_exists(). Isn't that data set? I thought it should be parsed with Symfony Request.
Cheers!
Hi, I have one question, I would like to call $this->getUser() not in Controller like in LocaleSubscriber class, how could I do that??
Hey Ayham Hasan!
In that case you could inject the "TokenStorage" into your Subscriber class (By using dependency injection), and then, retrieve the User from it. Actually you would be "almost" duplicating all the logic that Symfony's BaseController does
This is the right namespace of the TokenStorage:
Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
// in somewhere from your subscriber class
$user = $this->tokenStorage->getToken();
if (!is_object($user = $token->getUser())) {
// user is not logged in
}
Cheers!
Hi,
It's me again ;)
I have a little question about guard if I want to use guard for the api and for the web.
I suppose I need 2 classes one for the web and one for the API but in security.yml, do I just declare the 2 services under the authenticators key ?
Thanks again for all.
Edit: One more time I really should to watch all the videos before I open my mouth ;)
2 firewalls are the solution.
isn't it?
Cheers!
Hey @Greg!
haha, it's ok to ask, someone else might find it useful :)
and yes, if you have two different ways of accessing to your app (web, API), it's recommended to configure two firewalls
Cheers!
When I configure autowire i get an error
jwt_token_authenticator:
class: AppBundle\Security\JwtTokenAuthenticator
autowire: true
Unable to autowire argument of type "Doctrine\ORM\EntityManager" for the service "jwt_token_authenticator". Multiple services exist for this class (doctrine.orm.default_entity_manager, sonata.admin.entity_manager).
How to resolve this error, or is this normal behavior when using SonataAdminBundle?
------------
Currently I am manually configuring and it works:
arguments: ['@lexik_jwt_authentication.encoder', '@doctrine.orm.entity_manager', '@api.response_factory']
Hey Zuhayer,
When you have more than one service which extends the same class - you need to stop using "autowire: true" and set your dependencies manually. It's a normal behavior - system just can't determine by itself what service to inject. So you have to take this work on yourself. It's rare, but sometimes it happens like in your example with SonataAdminBundle.
Cheers!
i have done all the code according to tutorial but its always return 401 Unauthorized.I am sending authorization header with JWT tokens but its not working can anyone help me please?
Hey Rajesh Patel
Could you show us your code? So we can help you debugging
Cheers!
Yes! Code would be very helpful :). In general, the 401 happens when you try to access a secured endpoint, and there is NO authentication information on the request. Have you added code to your start() method yet? If so, if you modify the response in that method (e.g. change it to a 402), does that change your response to a 402 or is it still a 401? Typically, the start() method is responsible for returning the 401, but I want to be sure.
To debug, I would look hard at your getCredentials() method. My guess is that this method is returning null, even when there is a JWT in the request. If I'm correct, then because you are returning null, the authenticator is being skipped, and so you ultimately receive the 401.
Cheers!
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.0.*", // v3.0.3
"doctrine/orm": "^2.5", // v2.5.4
"doctrine/doctrine-bundle": "^1.6", // 1.6.2
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // v2.10.0
"sensio/distribution-bundle": "^5.0", // v5.0.4
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.14
"incenteev/composer-parameter-handler": "~2.0", // v2.1.2
"jms/serializer-bundle": "^1.1.0", // 1.1.0
"white-october/pagerfanta-bundle": "^1.0", // v1.0.5
"lexik/jwt-authentication-bundle": "^1.4" // v1.4.3
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.6
"symfony/phpunit-bridge": "^3.0", // v3.0.3
"behat/behat": "~3.1@dev", // dev-master
"behat/mink-extension": "~2.2.0", // v2.2
"behat/mink-goutte-driver": "~1.2.0", // v1.2.1
"behat/mink-selenium2-driver": "~1.3.0", // v1.3.1
"phpunit/phpunit": "~4.6.0", // 4.6.10
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}
Hi! ApiProblem is not support in Symfony 5, how can I replace that, cheers!