When developing an API for a mobile application or a SPA with Laravel, a robust option for authentication is to use Laravel Fortify. In this article, we’ll explore how to properly handle logout for users, especially when using Laravel Sanctum for token-based authentication.
1. Create a Custom Controller
To properly handle logout, we need to create a custom controller that extends Laravel\Fortify\Http\Controllers\AuthenticatedSessionController. In this controller, we override the destroy method to delete the user's personal access token (PersonalAccessToken).
use App\Models\User;
use Illuminate\Http\Request;
use Laravel\Fortify\Contracts\LogoutResponse;
use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController as FortifyAuthenticatedSessionController;
use Laravel\Sanctum\PersonalAccessToken;
use Override;
final class AuthenticatedSessionController extends FortifyAuthenticatedSessionController
{
/**
* Destroy an authenticated session.
*
* @param Request $request
* @return LogoutResponse
*/
#[Override]
public function destroy(Request $request): LogoutResponse
{
$this->guard->logout();
/** @var User $currentUser */
$currentUser = $request->user();
$currentAccessToken = $currentUser->currentAccessToken();
if ($currentAccessToken instanceof PersonalAccessToken) {
$currentAccessToken->delete();
}
if ($request->hasSession()) {
$request->session()->invalidate();
$request->session()->regenerateToken();
}
return app(LogoutResponse::class);
}
}
2. Define the Route in api.php
Once we have the controller, we need to add the corresponding route in the routes/api.php file:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\Auth\AuthenticatedSessionController as ApiV1AuthenticatedSessionController;
// Sanctum guard routes
Route::prefix(config('fortify.prefix'))
->middleware('auth:sanctum')
->group(function (): void {
Route::post('/logout', [ApiV1AuthenticatedSessionController::class, 'destroy']);
});
This ensures that only authenticated users can access this route.
3. Customize the Logout Response
To adapt the logout response to our project's structure, we can override it in the FortifyServiceProvider.php file inside the register method:
namespace App\Providers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\LogoutResponse;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
Fortify::ignoreRoutes();
$this->app->instance(LogoutResponse::class, new class () implements LogoutResponse {
public function toResponse($request): JsonResponse|RedirectResponse
{
if ($request->wantsJson()) {
return response()->json([
'data' => [
'message' => 'You are successfully logged out',
],
]);
}
return redirect()->intended(Fortify::redirects('login'));
}
});
}
}
4. Test the Functionality
Finally, it's a good practice to write automated tests to verify that the logout functionality works as expected:
use App\Http\Controllers\Api\V1\Auth\AuthenticatedSessionController;
use Database\Factories\UserFactory;
it('should logout a user', function (): void {
$user = UserFactory::new()->create();
$token = $user->createToken('test-token')->plainTextToken;
$this->actingAs($user)
->postJson(action([AuthenticatedSessionController::class, 'destroy']))
->assertStatus(200)
->assertJsonStructure([
'data' => [
'message',
],
])
->assertJsonFragment([
'message' => 'You are successfully logged out',
]);
$this->assertDatabaseMissing('personal_access_tokens', [
'token' => $token,
]);
});
it('should not logout a user if not authenticated', function (): void {
$this->postJson(action([AuthenticatedSessionController::class, 'destroy']))
->assertStatus(401)
->assertJsonStructure([
'message',
])
->assertJsonFragment([
'message' => 'Unauthenticated.',
]);
});
Conclusion
Properly handling logout is essential to ensure security and a good user experience in our API. With Laravel Fortify and Sanctum, we can efficiently revoke access tokens and customize the response according to our project's needs.