laravel / framework

The Laravel Framework.
https://laravel.com
MIT License
32.52k stars 11.02k forks source link

Auth:logout() is not working with sanctum as discribed in the documentation #47456

Closed la40 closed 1 year ago

la40 commented 1 year ago

Laravel Version

10.13.5

PHP Version

8.2.6

Database Driver & Version

8.0.33

Description

According to the documentation https://laravel.com/docs/10.x/authentication#logging-out the logout function of the Auth facade should work without pointing the guard just like that:

Auth::logout()

Unexpectedly I found that it does not work as expected. You should explicitely point the guard and it's working.

Auth::guard("web")->logout();

I use postman to test the requests and this is the error I got:

{ "message": "Method Illuminate\\Auth\\RequestGuard::logout does not exist.", "exception": "BadMethodCallException", "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Macroable/Traits/Macroable.php", "line": 112, "trace": [ { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Auth/AuthManager.php", "line": 340, "function": "__call", "class": "Illuminate\\Auth\\RequestGuard", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php", "line": 353, "function": "__call", "class": "Illuminate\\Auth\\AuthManager", "type": "->" }, { "file": "/path/to/file/app/Http/Controllers/Api/AuthController.php", "line": 42, "function": "__callStatic", "class": "Illuminate\\Support\\Facades\\Facade", "type": "::" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Controller.php", "line": 54, "function": "logout", "class": "App\\Http\\Controllers\\Api\\AuthController", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php", "line": 43, "function": "callAction", "class": "Illuminate\\Routing\\Controller", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Route.php", "line": 259, "function": "dispatch", "class": "Illuminate\\Routing\\ControllerDispatcher", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Route.php", "line": 205, "function": "runController", "class": "Illuminate\\Routing\\Route", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 799, "function": "run", "class": "Illuminate\\Routing\\Route", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 141, "function": "Illuminate\\Routing\\{closure}", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Middleware/SubstituteBindings.php", "line": 50, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Routing\\Middleware\\SubstituteBindings", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php", "line": 159, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php", "line": 125, "function": "handleRequest", "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Middleware/ThrottleRequests.php", "line": 87, "function": "handleRequestUsingNamedLimiter", "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Routing\\Middleware\\ThrottleRequests", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Auth/Middleware/Authenticate.php", "line": 57, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Auth\\Middleware\\Authenticate", "type": "->" }, { "file": "/path/to/file/vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php", "line": 25, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 141, "function": "Laravel\\Sanctum\\Http\\Middleware\\{closure}", "class": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php", "line": 78, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\VerifyCsrfToken", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php", "line": 121, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Session/Middleware/StartSession.php", "line": 64, "function": "handleStatefulRequest", "class": "Illuminate\\Session\\Middleware\\StartSession", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Session\\Middleware\\StartSession", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/AddQueuedCookiesToResponse.php", "line": 37, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Cookie/Middleware/EncryptCookies.php", "line": 67, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Cookie\\Middleware\\EncryptCookies", "type": "->" }, { "file": "/path/to/file/vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php", "line": 60, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 162, "function": "Laravel\\Sanctum\\Http\\Middleware\\{closure}", "class": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 116, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/sanctum/src/Http/Middleware/EnsureFrontendRequestsAreStateful.php", "line": 24, "function": "then", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Laravel\\Sanctum\\Http\\Middleware\\EnsureFrontendRequestsAreStateful", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 116, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 798, "function": "then", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 777, "function": "runRouteWithinStack", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 741, "function": "runRoute", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Routing/Router.php", "line": 730, "function": "dispatchToRoute", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 200, "function": "dispatch", "class": "Illuminate\\Routing\\Router", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 141, "function": "Illuminate\\Foundation\\Http\\{closure}", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/path/to/file/vendor/beyondcode/laravel-query-detector/src/QueryDetectorMiddleware.php", "line": 33, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "BeyondCode\\QueryDetector\\QueryDetectorMiddleware", "type": "->" }, { "file": "/path/to/file/vendor/barryvdh/laravel-debugbar/src/Middleware/InjectDebugbar.php", "line": 66, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Barryvdh\\Debugbar\\Middleware\\InjectDebugbar", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php", "line": 21, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ConvertEmptyStringsToNull.php", "line": 31, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TransformsRequest.php", "line": 21, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php", "line": 40, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TransformsRequest", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\TrimStrings", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/ValidatePostSize.php", "line": 27, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\ValidatePostSize", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php", "line": 86, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Http/Middleware/HandleCors.php", "line": 62, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Http\\Middleware\\HandleCors", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php", "line": 39, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 180, "function": "handle", "class": "Illuminate\\Http\\Middleware\\TrustProxies", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php", "line": 116, "function": "Illuminate\\Pipeline\\{closure}", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 175, "function": "then", "class": "Illuminate\\Pipeline\\Pipeline", "type": "->" }, { "file": "/path/to/file/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php", "line": 144, "function": "sendRequestThroughRouter", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/path/to/file/public/index.php", "line": 51, "function": "handle", "class": "Illuminate\\Foundation\\Http\\Kernel", "type": "->" }, { "file": "/path/to/file/.composer/vendor/laravel/valet/server.php", "line": 110, "function": "require" } ] }

Steps To Reproduce

Fresh laravel installation + sanctum

"require": {
        "php": "^8.1",
        "guzzlehttp/guzzle": "^7.2",
        "laravel/framework": "^10.10",
        "laravel/sanctum": "^3.2",
        "laravel/tinker": "^2.8",
        "predis/predis": "^2.1"
    },
    "require-dev": {
        "barryvdh/laravel-debugbar": "^3.8",
        "beyondcode/laravel-dump-server": "^1.9",
        "beyondcode/laravel-query-detector": "^1.7",
        "fakerphp/faker": "^1.9.1",
        "laravel/pint": "^1.0",
        "laravel/sail": "^1.18",
        "mockery/mockery": "^1.4.4",
        "nunomaduro/collision": "^7.0",
        "phpunit/phpunit": "^10.1",
        "spatie/laravel-ignition": "^2.0"
    },

Logout implementation according to the laravel's documentation. https://laravel.com/docs/10.x/authentication#logging-out

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * Log the user out of the application.
 */
public function logout(Request $request)
{
    Auth::logout();

    $request->session()->invalidate();

    $request->session()->regenerateToken();

    return response()->json();
}

Api routes

// v1 api routes
Route::prefix("v1")->group(function () {
    Route::post("login", [\App\Http\Controllers\Api\AuthController::class, "login"]);
    // authenticated routes
    Route::middleware("auth:sanctum")->group(function () {
        Route::delete("logout", [\App\Http\Controllers\Api\AuthController::class, "logout"]);
    });
});
driesvints commented 1 year ago

I'm not sure why you'd use logout with sanctum? Sanctum is just a token based auth for SPA's. You don't use logout with that as each request is checked separately through the provided token.

la40 commented 1 year ago

I also was confused at the beginning because I expected also sanctum to be a token based authentication but the documentation sujest that it is a cookie based authentication for SPA first and then token based.

SPA Authentication Second, Sanctum exists to offer a simple way to authenticate single page applications (SPAs) that need to communicate with a Laravel powered API. These SPAs might exist in the same repository as your Laravel application or might be an entirely separate repository, such as a SPA created using Vue CLI or a Next.js application.

For this feature, Sanctum does not use tokens of any kind. Instead, Sanctum uses Laravel's built-in cookie based session authentication services. Typically, Sanctum utilizes Laravel's web authentication guard to accomplish this. This provides the benefits of CSRF protection, session authentication, as well as protects against leakage of the authentication credentials via XSS.

Sanctum will only attempt to authenticate using cookies when the incoming request originates from your own SPA frontend. When Sanctum examines an incoming HTTP request, it will first check for an authentication cookie and, if none is present, Sanctum will then examine the Authorization header for a valid API token.

If you fallow the documentation you will go to the Logging in part where the documentation sujest you two ways to implement the authentication. If you fallow the manuall implementation you will end with the bug described above.

Logging In Once CSRF protection has been initialized, you should make a POST request to your Laravel application's /login route. This /login route may be implemented manually or using a headless authentication package like Laravel Fortify.

The second way is to use Fortify and if you check the logout method of Fortify you will see that the logout method also use auth (guard) logout method!

https://github.com/laravel/fortify/blob/1.x/src/Http/Controllers/AuthenticatedSessionController.php

 /**
     * Destroy an authenticated session.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Laravel\Fortify\Contracts\LogoutResponse
     */
    public function destroy(Request $request): LogoutResponse
    {
        $this->guard->logout();

        $request->session()->invalidate();

        $request->session()->regenerateToken();

        return app(LogoutResponse::class);
    }

I think you should point somewhere in the documentation that if you decide to use the manuall implementation you should destroy the sanctum's cookie based session with

Auth::guard("web")->logout();
notzilly commented 4 months ago

@la40 thanks for the explanation, it was very helpful!