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.
// 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
}
}