Method Illuminate\Auth\RequestGuard::logout does not exist Laravel Passport
Asked Answered
A

5

25

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

Note: am using password grant tokens

Note 2: revoking the user's token doesn't work

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?

Apterygial answered 5/9, 2019 at 22:47 Comment(0)
C
20

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);
}
Chausses answered 15/9, 2019 at 4:23 Comment(4)
Perfect explanation, one thing tho, the middleware returns a 401 instead of 403 so the assertion should be $response->assertUnauthorized();Apterygial
@CaddyDZ Good catch. I had just copied that assertion from your original code, and didn't think about updating it. I just updated my answer with a direct status code check, which is what I prefer to use anyway. Glad I was able to help.Chausses
It gives me the error ErrorException: Undefined property: $app in file. I am using Laravel 8Anemophilous
@fahadshaikh Does your test class extend Tests\TestCase?Chausses
D
21

auth()->guard('web')->logout()

Dabster answered 19/4, 2022 at 16:1 Comment(1)
This worked for me, although fwiw I used Auth::guard('web')->logout(). I'm surprised that the "web" guard isn't used by default; I have a Laravel 6 project that I didn't need to specify the guard on, so perhaps this is a somewhat recent change.Koenraad
C
20

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);
}
Chausses answered 15/9, 2019 at 4:23 Comment(4)
Perfect explanation, one thing tho, the middleware returns a 401 instead of 403 so the assertion should be $response->assertUnauthorized();Apterygial
@CaddyDZ Good catch. I had just copied that assertion from your original code, and didn't think about updating it. I just updated my answer with a direct status code check, which is what I prefer to use anyway. Glad I was able to help.Chausses
It gives me the error ErrorException: Undefined property: $app in file. I am using Laravel 8Anemophilous
@fahadshaikh Does your test class extend Tests\TestCase?Chausses
B
4

using auth()->guard('web')->logout() and auth()->guard('web')->login($user) solved my problem, it is really weird to find misleading steps on the official documentation.

Bondwoman answered 21/12, 2022 at 20:36 Comment(1)
This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From ReviewMignon
P
1

Use:

auth()->forgetGuards();

The accepted answer was not working for me. I finally noticed when revoke the user token AND call resetAuth() from the accepted answer it works:

protected function resetAuth() {
    $this->app['auth']->guard('api')->user()->token()->revoke(); //Revoke (or delete()) the passport user token.
    //$this->app['auth']->guard('web')->logout(); //If you also want to call the logout function on your web guard. 
    $protectedProperty = new \ReflectionProperty($this->app['auth'], 'guards');
    $protectedProperty->setAccessible(true);
    $protectedProperty->setValue($this->app['auth'], []);
}

But this threw an exception when using methods such as $this->isAuthenticated() - //League\OAuth2\Server\Exception\OAuthServerException: The resource owner or authorization server denied the request.

I eventually came across: auth()->forgetGuards(); which seems to do the trick of properly clearing authentication without any subsequent issues.

Parkinson answered 21/1, 2023 at 9:3 Comment(0)
M
0

this codes works for me, hope this will help

// UserController.php

public function deactivateUser(Request $request): RedirectResponse {
            $user = Auth::user();
            if ($user->status === 'active') {
                $user->status = 'inactive';
                $user->save();

                if (Auth::check()) {
                    Auth::guard('web')->logout();
                    $request->session()->invalidate();
                    $request->session()->regenerateToken();
                }

            }

            return redirect()->route('login')->with('success', 'Logout successfully' );

        }

Miliary answered 30/10, 2023 at 0:33 Comment(1)
Thank you for contributing to the Stack Overflow community. This may be a correct answer, but it’d be really useful to provide additional explanation of your code so developers can understand your reasoning. This is especially useful for new developers who aren’t as familiar with the syntax or struggling to understand the concepts. Would you kindly edit your answer to include additional details for the benefit of the community?Posy

© 2022 - 2024 — McMap. All rights reserved.