Am using Laravel Passport
to build an API, I removed the web routes and its guard accordingly
How can I test user logout?
This is what I have so far:
Logout Test
/**
* Assert users can logout
*
* @return void
*/
public function test_logout()
{
// $data->token_type = "Bearer"
// $data->access_token = "Long string that is a valid token stripped out for brevety"
$response = $this->json('POST', '/api/logout', [], [
'Authorization' => $data->token_type . ' ' . $data->access_token
]);
$response->assertStatus(200);
}
routes/api.php
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
The controller method uses the AuthenticatesUsers
trait so the default function is kept
/**
* Log the user out of the application.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function logout(Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
return $this->loggedOut($request) ?: redirect('/');
}
Error Method Illuminate\Auth\RequestGuard::logout does not exist
The Laravel Documentation talks about issuing and refreshing access tokens but nothing about revoking them or performing logout
public function logout(Request $request)
{
$request->user()->token()->revoke();
return $this->loggedOut($request);
}
Test Fails on second assertion
public function test_logout()
{
$response = $this->json('POST', '/api/logout', [], [
'Authorization' => $data->token_type . ' ' . $data->access_token
]);
$response->assertStatus(200); // Passes
$check_request = $this->get('/api/user');
$check_request->assertForbidden(); // Fails
}
Given the default route requiring authentication
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Response status code [200] is not a forbidden status code.
So what's going on? and how can I test user logout with Passport?
Thanks in advance
Laravel Passport is an OAuth 2.0 server implementation for stateless authentication.
Laravel JWT authentication vs. Passport uses JWT authentication as standard but also implements full OAuth 2.0 authorization. OAuth allows authorization from third-party applications like Google, GitHub, and Facebook, but not every app requires this feature.
Revoking the token is working. It is the test that is not working, but it is not obvious why.
When making multiple requests in one test, the state of your laravel application is not reset between the requests. The Auth manager is a singleton in the laravel container, and it keeps a local cache of the resolved auth guards. The resolved auth guards keep a local cache of the authed user.
So, your first request to your api/logout
endpoint resolves the auth manager, which resolves the api guard, which stores a references to the authed user whose token you will be revoking.
Now, when you make your second request to /api/user
, the already resolved auth manager is pulled from the container, the already resolved api guard is pulled from it's local cache, and the same already resolved user is pulled from the guard's local cache. This is why the second request passes authentication instead of failing it.
When testing auth related stuff with multiple requests in the same test, you need to reset the resolved instances between tests. Also, you can't just unset the resolved auth manager instance, because when it is resolved again, it won't have the extended passport
driver defined.
So, the easiest way I've found is to use reflection to unset the protected guards
property on the resolved auth manager. You also need to call the logout
method on the resolved session guards.
I have a method on my TestCase class that looks something like:
protected function resetAuth(array $guards = null)
{
$guards = $guards ?: array_keys(config('auth.guards'));
foreach ($guards as $guard) {
$guard = $this->app['auth']->guard($guard);
if ($guard instanceof \Illuminate\Auth\SessionGuard) {
$guard->logout();
}
}
$protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
$protectedProperty->setAccessible(true);
$protectedProperty->setValue($this->app['auth'], []);
}
Now, your test would look something like:
public function test_logout()
{
$response = $this->json('POST', '/api/logout', [], [
'Authorization' => $data->token_type . ' ' . $data->access_token
]);
$response->assertStatus(200);
// Directly assert the api user's token was revoked.
$this->assertTrue($this->app['auth']->guard('api')->user()->token()->revoked);
$this->resetAuth();
// Assert using the revoked token for the next request won't work.
$response = $this->json('GET', '/api/user', [], [
'Authorization' => $data->token_type . ' ' . $data->access_token
]);
$response->assertStatus(401);
}
auth()->guard('web')->logout()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With