Testing Token Authentication
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 SubscribeWhat about a test like this... but where we log in with an API key? Let's do that! Create a new method: public function testPostToCreateTreasureWithApiKey()
:
// ... lines 1 - 10 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 13 - 61 | |
public function testPostToCreateTreasureWithApiKey(): void | |
{ | |
// ... lines 64 - 70 | |
} | |
} |
This will start pretty much the same as before. I'll copy the top of the previous test, remove the actingAs()
... and add a dump()
near the bottom:
// ... lines 1 - 10 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 13 - 61 | |
public function testPostToCreateTreasureWithApiKey(): void | |
{ | |
$this->browser() | |
->post('/api/treasures', [ | |
'json' => [], | |
]) | |
->dump() | |
->assertStatus(422) | |
; | |
} | |
} |
So, like before, we're sending invalid data and expect a 422 status code.
Copy that method name, then spin over and run just this test:
symfony php bin/phpunit --filter=testPostToCreateTreasureWithApiKey
And... no surprise: we get a 401 status code because we're not authenticated.
Let's send an Authorization
header, but an invalid one to start. Pass a headers
key set to an array with Authorization
and then word Bearer
and then... foo
.
This should still fail:
symfony php bin/phpunit --filter=testPostToCreateTreasureWithApiKey
And... it does! But with a different error message: invalid_token
. Nice!
Using a Real Token
To pass a real token, we need to put a real token into the database. Do that with $token = ApiTokenFactory::createOne()
:
// ... lines 1 - 12 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 15 - 63 | |
public function testPostToCreateTreasureWithApiKey(): void | |
{ | |
$token = ApiTokenFactory::createOne([ | |
// ... line 67 | |
]); | |
// ... lines 69 - 79 | |
} | |
} |
Do we need to control any fields on this? We actually do. Open up DragonTreasure
. If we scroll up, the Post
operation requires ROLE_TREASURE_CREATE
:
// ... lines 1 - 27 | |
( | |
// ... lines 29 - 30 | |
operations: [ | |
// ... lines 32 - 37 | |
new Post( | |
security: 'is_granted("ROLE_TREASURE_CREATE")', | |
), | |
// ... lines 41 - 49 | |
], | |
// ... lines 51 - 64 | |
) | |
// ... lines 66 - 83 | |
class DragonTreasure | |
{ | |
// ... lines 86 - 243 | |
} |
When we authenticate via the login form, thanks to role_hierarchy
, we always have that. But when using an API key, to get that role, the token needs the corresponding scope.
To make sure we have it, back in the test, set the scopes
property to ApiToken::SCOPE_TREASURE_CREATE
:
// ... lines 1 - 4 | |
use App\Entity\ApiToken; | |
// ... lines 6 - 12 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 15 - 63 | |
public function testPostToCreateTreasureWithApiKey(): void | |
{ | |
$token = ApiTokenFactory::createOne([ | |
'scopes' => [ApiToken::SCOPE_TREASURE_CREATE] | |
]); | |
// ... lines 69 - 79 | |
} | |
} |
Now pass this to the header: $token->getToken()
. Oh... and let me fix scopes
: that should be an array:
// ... lines 1 - 12 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 15 - 63 | |
public function testPostToCreateTreasureWithApiKey(): void | |
{ | |
$token = ApiTokenFactory::createOne([ | |
'scopes' => [ApiToken::SCOPE_TREASURE_CREATE] | |
]); | |
// ... line 69 | |
$this->browser() | |
->post('/api/treasures', [ | |
// ... line 72 | |
'headers' => [ | |
'Authorization' => 'Bearer '.$token->getToken() | |
] | |
]) | |
// ... lines 77 - 78 | |
; | |
} | |
} |
I think we're ready! Run that test:
symfony php bin/phpunit --filter=testPostToCreateTreasureWithApiKey
And... got it! We see the beautiful 422 validation errors!
Testing a Token with a Bad Scope
Let's test to make sure we don't have access if our token is missing this scope. Copy the entire test method... then paste below. Call it testPostToCreateTreasureDeniedWithoutScope()
.
This time, set scopes
to something else, like SCOPE_TREASURE_EDIT
. Below, we now expect a 403 status code:
// ... lines 1 - 12 | |
class DragonTreasureResourceTest extends ApiTestCase | |
{ | |
// ... lines 15 - 80 | |
public function testPostToCreateTreasureDeniedWithoutScope(): void | |
{ | |
$token = ApiTokenFactory::createOne([ | |
'scopes' => [ApiToken::SCOPE_TREASURE_EDIT] | |
]); | |
$this->browser() | |
->post('/api/treasures', [ | |
'json' => [], | |
'headers' => [ | |
'Authorization' => 'Bearer '.$token->getToken() | |
] | |
]) | |
->assertStatus(403) | |
; | |
} | |
} |
This time, let's run all the tests:
symfony php bin/phpunit
And... all green! A 422 then a 403. Go remove the dumps from both those spots.
By the way, if you use API tokens a lot in your tests, passing the Authorization
header can get annoying. Browser has a way where we can create a custom Browser object with custom methods. For example, you could add an authWithToken()
method, pass an array of scopes, and then it would create that token and set it into the header
$this->browser()
->authWithToken([ApiToken::SCOPE_TREASURE_CREATE])
// ...
;
This totally does not work right now, but check out Browser's docs to learn how.
Next: in API Platform 3.1, the behavior of the PUT
operation is changing. Let's talk about how, and what we need to do in our code to prepare for it.
For those who need it, I always got a 415 response and in the error document that I created, it appeared
{ "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "The content-type \"application/json\" is not supported. Supported MIME types are \"application/ld+json\".", .....more ....
I had to add the Content-Type in the headers to fix it and continue with the flow of the course.