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