filipescaglia's avatar

Laravel 11 Sanctum SPA logout issues

Can someone for the love of god help me? I've already wasted days trying to solve this... I'm trying to test the logout of my app but it simple does not works.

My LogouTest.php

<?php

use App\Models\User;

use function Pest\Laravel\{actingAs, assertGuest, getJson, postJson};

it('should be able to logout', function () {
    $user = User::factory()->create();
    actingAs($user);

    postJson(route('auth.logout'))
        ->assertNoContent();

    assertGuest('web');

    getJson(route('auth.profile.index'))->assertUnauthorized(); // this returns 200 instead of 401
});

My LogoutController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;

class LogoutController extends Controller
{
    public function __invoke()
    {
        Auth::guard('web')->logout();

		// $request->session() throws error
        session()->invalidate();
        session()->regenerateToken();

        return response()->noContent();
    }
}

My api routes:

<?php

Route::get('/profile', Profile\FindController::class)
    ->middleware('auth:sanctum')
    ->name('auth.profile.index');

Route::post('/logout', LogoutController::class)
    ->name('auth.logout')
    ->middleware('auth:sanctum');

My LoginController in case someone wants to know:

The process of logout itself works if i'm doing it through the SPA (sometimes it fails and i also don't know why), but in the test it always fails... why? I'm really considering switching to the token approach, none of the topics about this subject here helped.

Also, shouldn't the Auth::logout clear the user_id in my sessions table?

0 likes
3 replies
filipescaglia's avatar

@jlrdw I've already tried what they suggested and the problem persists. The problem is that if I stop the test at assertGuest, everything works fine, but if I make a request to an endpoint that should be protected, it returns success instead of failure.

Later I started looking at Laravel fortify, to see how it handles this and I realized that its routes are basically protected with the web guard: https://github.com/laravel/fortify/blob/1.x/routes/routes.php#L45

In a fresh installation this will resolve in the auth:web: https://github.com/laravel/fortify/blob/1.x/stubs/fortify.php#L18

So if I changed my api routes to use the auth:web instead of auth:sanctum, the test passes successfully, but I find it really strange to have to do that. The sanctum documentation suggests using auth:sactum...

Later still I found this issue and this comment that says that "making multiple HTTP calls in the same test isn't supported": https://github.com/laravel/sanctum/issues/256#issuecomment-792960317

And this comment on the same issue saying that using $this->refreshApplication() solved it: https://github.com/laravel/sanctum/issues/256#issuecomment-846637741

So I changed my classes a bit:

LogoutController

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LogoutController extends Controller
{
    public function __invoke(Request $request)
    {
        Auth::guard('web')->logout();

        if ($request->hasSession()) {
            $request->session()->invalidate();
            $request->session()->regenerateToken();
        }

        return response()->noContent();
    }
}

LogoutTest

it('should be able to logout', function () {
    $user = User::factory()->create();
    actingAs($user);

    postJson(route('auth.logout'))->assertNoContent();

    assertGuest('web');
    expect(Auth::guard('web')->user())->toBe(null);

    $this->refreshApplication(); // without this, the next request returns 200
	// it also could be `Auth::guard('sanctum')->forgetUser()` as suggested here:
	// https://github.com/laravel/sanctum/issues/256#issuecomment-1859511823
	// it works, but both ways feels wrong to me, like hacky...

    getJson(route('auth.profile.index'))->assertUnauthorized(); // now this returns 401
});

In my Vue SPA, the logout works fine both ways, but the test doesn't, that was my problem. It's solved, but I'm not convinced that it's done correctly.

Please or to participate in this conversation.