Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Graceful Errors for an Invalid JWT

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 $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We 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?

Testing with a bad Token

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.

How Authentication Fails

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

ApiProblem on Failure

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.

Controlling Error Message

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.

Leave a comment!

16
Login or Register to join the conversation
Default user avatar
Default user avatar Zuhayer Tahir | posted 5 years ago

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": { }
}

Reply

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!

Reply
Default user avatar
Default user avatar John Armstrong | posted 5 years ago

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']

Reply

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!

Reply
Default user avatar
Default user avatar John Armstrong | weaverryan | posted 5 years ago

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

Reply

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!

Reply
Default user avatar
Default user avatar John Armstrong | weaverryan | posted 5 years ago

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".

Reply

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!

1 Reply
Default user avatar

Thank you so much. It solved my issue too.

Reply
Default user avatar

Had the same issue. Code above works like a charm. Thanks

Reply
Default user avatar
Default user avatar John Armstrong | weaverryan | posted 5 years ago

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!

Reply
Bartlomeij Avatar
Bartlomeij Avatar Bartlomeij | posted 5 years ago

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 :(

Reply

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!

Reply
Default user avatar
Default user avatar nOograss | posted 5 years ago

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

Reply

Hey nOograss

Sorry for the late response. Do you still have this problem?

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses an older version of Symfony. The concepts of API tokens & JWT are still valid, but integration in newer Symfony versions may be different.

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