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 already know that if the client forgets to send a token, Symfony calls the start()
method:
... lines 1 - 19 | |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 22 - 83 | |
public function start(Request $request, AuthenticationException $authException = null) | |
{ | |
... lines 86 - 94 | |
} | |
} |
But what happens if authentication fails?
Let's find out! Copy testRequiresAuthentication()
, paste it, and rename it to testBadToken()
:
... lines 1 - 6 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 9 - 277 | |
public function testBadToken() | |
{ | |
$response = $this->client->post('/api/programmers', [ | |
'body' => '[]', | |
'headers' => [ | |
'Authorization' => 'Bearer WRONG' | |
] | |
]); | |
$this->assertEquals(401, $response->getStatusCode()); | |
$this->assertEquals('application/problem+json', $response->getHeader('Content-Type')[0]); | |
} | |
} |
In this case, we will add a headers
key and we will send an Authorization
header... but set to Bearer WRONG
.
If this happens, we definitely want a 401 status code and - like always - an application/problem+json
response header. Let's just look for these two things for now.
When JWT authentication fails, what handles that? Well, onAuthenticationFailure()
of course:
... lines 1 - 19 | |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 22 - 68 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
} | |
... lines 73 - 95 | |
} |
The getUser()
method must return a User
object. If it doesn't, then onAuthenticationFailure()
is called. In our case, there are two possible reasons: the token might be corrupted or expired or - somehow - the decoded username doesn't exist in our database. In both cases, we are not returning a User object, and this triggers onAuthenticationFailure()
.
To start, just return a new JsonResponse
that says Hello
, but with the proper 401 status code:
... lines 1 - 19 | |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 22 - 68 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
return new JsonResponse('Hello!', 401); | |
} | |
... lines 73 - 95 | |
} |
Copy the testBadToken
method name and give it a try!
./vendor/bin/phpunit --filter testBadToken
It almost works - that's a good start. It proves our code in onAuthenticationFailure()
is handling things. Now, let's setup a proper API problem response, just like we did before: $apiProblem = new ApiProblem
with a 401 status code:
... lines 1 - 19 | |
class JwtTokenAuthenticator extends AbstractGuardAuthenticator | |
{ | |
... lines 22 - 68 | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception) | |
{ | |
$apiProblem = new ApiProblem(401); | |
... lines 72 - 75 | |
} | |
... lines 77 - 99 | |
} |
Then, use $apiProblem->set()
to add a detail
field. And in this case, we always have an AuthenticationException
that can hint what went wrong. Use its getMessageKey()
method:
... lines 1 - 70 | |
$apiProblem = new ApiProblem(401); | |
// you could translate this | |
$apiProblem->set('detail', $exception->getMessageKey()); | |
... lines 74 - 101 |
Oh, and by the way - if you want, you can send this through the translator
service and translate into multiple languages.
Finish this with return
$this–>responseFactory->createResponse() to turn the $apiProblem
into a nice JSON response:
... lines 1 - 70 | |
$apiProblem = new ApiProblem(401); | |
// you could translate this | |
$apiProblem->set('detail', $exception->getMessageKey()); | |
return $this->responseFactory->createResponse($apiProblem); | |
... lines 76 - 101 |
That's it! We did all the hard work earlier.
I want to actually see how this response looks. So, add a $this->debugResponse()
at the end of testBadToken()
:
... lines 1 - 6 | |
class ProgrammerControllerTest extends ApiTestCase | |
{ | |
... lines 9 - 277 | |
public function testBadToken() | |
{ | |
... lines 280 - 287 | |
$this->debugResponse($response); | |
} | |
} |
Now, re-run the test!
./vendor/bin/phpunit --filter testBadToken
Check that out - it's beautiful! It has all the fields it needs, including detail
, which is set to Invalid token
.
That text is coming from our code, when we throw the CustomUserMessageAuthenticationException
. The text - Invalid token
- becomes the "message key" and this exception is passed to onAuthenticationFailure()
.
This gives you complete control over how your errors look.
Hey Zuhayer,
I think you can wrap your code related to the DefaultEncoder with a try-catch block, then catch the exception which you need (as I see it's the JWTDecodeFailureException) and create a new ApiProblem in this catch block with any message you want in this case. Does it makes sense for you?
Cheers!
I am having trouble with the error not returning as a json response when I use a wrong or invalid token. I get a ton of html that overloads my terminal on a curl. It works fine if I put a valid token in the Bearer Header. My route method is GET. Any ideas what maybe happening? also, another issue I ran into earlier but forgot to comment there was the autowire for jwt service. I got this error:
Unable to autowire argument of type "Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface" for the service "app.security.jwt_token_authenticator". Multiple services exist for this interface (lexik_jwt_authentication.encoder.default, lexik_jwt_authentication.encoder.lcobucci).
So I used arguments: ['@lexik_jwt_authentication.encoder.default', '@doctrine.orm.entity_manager', '@api.response_factory']
Hey John!
Hmm, so usually, when you see a ton of HTML in your terminal, it's because you're seeing Symfony's HTML 500 exception page. But, it's also possible is that you're somehow seeing one of *your* HTML pages (not an exception page). But, it's probably an exception of some sort - our system only returns JSON if (a) we return JSON from the matched controller or (B) we are handling some ApiProblemException, where we've built extra logic to return JSON.
Try one of these 2 things:
1) Make the request that fails (I assume when you do this, you're hitting your application in the "dev" environment?). Then, go to http://localhost:8000/app_dev.php/_profiler. You should see a list of requests - and the one that you JUST made should be on top (or perhaps second, but at least very near the top). Click the sha link on the right to enter the profiler for that request. Then, check out the Exception tab - it should show you the HTML exception so you can read it. If you're doing this in a test, then you can also do this - but you'll need to temporarily turn the profiler on by setting the "collect" key to true and the "toolbar" key also to true in config_test.yml (https://github.com/symfony/.... Then, make sure you go to http://localhost:8000/app_test.php/_profiler
2) Or, just try to read the HTML exception message in your terminal. It's not super-easy at first, but if you scroll all to the top and start scanning down, you'll eventually see an h1 that has the exception message in it. You could alternatively try to tail var/cache/dev.log (or test.log) to see the exception there.
So, these are both strategies to see *what* the exception is, so that we can fix it (I'm still assuming that the problem is an exception!).
About the JWT fix, thanks for posting that! It sounds like an update to the library may have introduced a second service which implements this interface. I'll go look at the library now to see if I can have those guys fix that in the bundle to make our lives easier.
Cheers!
I found the h1 in the terminal, this error makes no sense to me:
Unable to verify the given JWT through the given configuration. If the "lexik_jwt_authentication.encoder" encryption options have been changed since your last authentication, please renew the token. If the problem persists, verify that the configured keys/passphrase are valid.
If the token is right, I don't get this error. I get my response from the controller.
Also, I did post another question on part 3 of this course about trying to get those key files up to Heroku and configured properly. I've found nothing on the subject for guidance. Does anyone use Heroku for Symfony apps/apis??
Hey John!
Well, I'm actually glad you installed the brand-new version 2 - I'm hoping we can make that work without too much effort! And thanks to your comment and the awesome maintainer of that bundle, version 2.0.1 fixes the autowiring problem: https://github.com/lexik/Le.... Fast service!
About your error, if the token is correct, you don't get that error. That's good :). So, when exactly *do* you get that error? Is it when you pass *no* token? Or is it when you pass an invalid token (perhaps because you're testing to see how that works)? Is that behavior different on 2.0 versus 1.4 of the bundle?
Cheers!
I was passing an invalid token and that's when it responds with that html error. But on the 1.4 version, with an invalid token, I get the json response of "Invalid token".
Hey John!
Ah, ok - I've got it on my list to run through the tutorial with v2 and see what we need to change on our side.
The problem (actually it's awesome - the bundle author we great enough to make this change by my request for version 2) is in getUser() of our JwtTokenAuthenticator. In v1.4, if decode() failed, it returned false - and we're handling this very nicely. The problem was that, as a user of the bundle, we couldn't get more information about why it failed (was the token invalid? expired?). So in v2, the bundle throws different types of exceptions on failure, which kind of awesome (different exception classes for each failure type).
Because of this change, we need to surround that line in a try-catch, something like this:
public function getUser($credentials, UserProviderInterface $userProvider)
{
try {
$data = $this->jwtEncoder->decode($credentials);
} catch (JWTDecodeFailureException $e) {
// if you want to, use can use $e->getReason() to find out which of the 3 possible things went wrong
// and tweak the message accordingly
// https://github.com/lexik/LexikJWTAuthenticationBundle/blob/05e15967f4dab94c8a75b275692d928a2fbf6d18/Exception/JWTDecodeFailureException.php
throw new CustomUserMessageAuthenticationException('Invalid Token');
}
// ...
}
That should do it :). Also, another cool thing about the new version is that they have added their own Guard authenticator to the bundle, which is based off of our's in this tutorial. If you want to, you can actually use it instead of building your own (building your own is still a good exercise, but now you have the opportunity to not need to do this).
Cheers!
Just for the record, I dropped version 2 of the jwt bundle and installed the version you used 1.4. Everything works like a charm!
I've got the other exception using Symfony 3.4, and I don't know why. On Exception method onAuthenticationFailure is not called, I just see a different error message:
"message": "Invalid JWT Token",
"class": "Lexik\\Bundle\\JWTAuthenticationBundle\\Exception\\JWTDecodeFailureException",
Script failes on line:
$data = $this->JWTEncoder->decode($credentials);
Can you help me with that? I don't really know how to fix that :(
Hey Bartlomeij!
Ah yes! So, this can be tricky, because the error means that the token is invalid. But, it could be invalid for many different reasons: you're fetching it from the request wrong, you have some problem with your public/private key, etc. The very nature of the cryptography makes it difficult to know what's going wrong :).
But, here are a few tips to debug:
1) If you wrap the line that is failing in a try-catch, then in the catch, add dump($e->getReason());
. This should one of these constants: https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Exception/JWTDecodeFailureException.php - which may help debugging.
2) Double-check (by dumping) that $credentials
looks how you expect before calling decode() on it.
If the cause is invalid_token, and you've verified that the $credentials DOES look correct, then there is likely some issue with your signing process, which could mean there's an issue with your public/private key.
Cheers!
Hi,
I'm still using symfony2.8,
I'm having a weird issue, the firewall is correclty triggered on urls containing /api but even without the token i'm able to get the response from the controller.
Here is part of the logs:
Checking for guard authentication credentials. {"firewall_key":"api","authenticators":1} []
[2018-04-05 21:53:30] security.DEBUG: Calling getCredentials() on guard configurator. {"firewall_key":"api","authenticator":"HME\\HMEBundle\\Security\\TokenAuthenticator"} []
[2018-04-05 21:53:30] security.DEBUG: Passing guard token information to the GuardAuthenticationProvider
The token is false after the extract and thus going into the 'return;'
[EDIT]
I'll restart from the beginning and implement the testClasses, maybe i overlooked something in the configuration
// 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
}
}
How can I handle the exceptions in vendor/lexik/jwt-authentication-bundle/Encoder/DefaultEncoder.php with custom JSON response as you did in AppBundle/Api/ApiProblem.php?
Should I try to handle them in conditions in ApiProblem::__construct or make a new JWT exception class?
e.g. In DefaultEncoder.php::isExpired() shows this long message -> http://pastebin.com/u4mqscVy
I want it to show JSON response as:
{
"error": 1,
"status": 500(or which ever is appropriate),
"msg": "Expired JWT Token",
"data": { }
}