Skip to content

Instantly share code, notes, and snippets.

@atomjoy
Last active August 8, 2025 15:10
Show Gist options
  • Save atomjoy/ee9f0c9e782c1d2a7f9e74d282a69a5c to your computer and use it in GitHub Desktop.
Save atomjoy/ee9f0c9e782c1d2a7f9e74d282a69a5c to your computer and use it in GitHub Desktop.
Laravel Sanctum Expired Token Middleware

Laravel Sanctum Expired Token Middleware.

How to catch expired token json exception in Laravel Sanctum with middleware.

Docs

https://laravel.com/docs/12.x/sanctum#api-token-authentication

Create Middleware

<?php

namespace App\Http\Middleware\Api;

use Closure;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;

/**
 * Sanctum expired token middleware.
 *
 * Add middleware in bootstrap/app.php
 * $middleware->api(prepend: [ \App\Http\Middleware\Api\ExpiredToken::class ]);
 */
class ExpiredToken
{
    public function handle(Request $request, Closure $next)
    {
        $bearer = $request->bearerToken();

        if ($bearer) {
            $token = PersonalAccessToken::findToken($bearer);

            if ($token instanceof PersonalAccessToken) {
                if($token->expires_at && $token->expires_at->isPast()) {
                    $request->merge([
                        'token_expired' => $token->expires_at && $token->expires_at->isPast(),
                        'token_details' => $token
                    ]);

                    return response()->json([
                        'message' => 'Expired Token.',
                        'token_expired' => $token->expires_at && $token->expires_at->isPast(),
                        'token_details' => $token
                    ], 403);
                }
            }
        }

        return $next($request);
    }
}

Add Middleware

// bootstrap/app.php

->withMiddleware(function (Middleware $middleware): void {
	// Sanctun SPA
	$middleware->statefulApi();
	// Sanctum API
	$middleware->api(prepend: [
		\App\Http\Middleware\Api\ExpiredToken::class
	]);
})

Cors

php artisan config:publish cors

# Change config/cors.php
'supports_credentials' => true,

Login with Api Token

<?php
// Login and get token
Route::post('/login-api', function () {
	// Validate user here ...
	$user = User::first();
	
	// Create api bearer token
	// Use this token with Authorization: Bearer token from RapidApi Client
	return $user->createToken('user-token-mobile', ['*'], now()->addYear())->plainTextToken;
});

Logged user in SPA

<?php
	
if(auth()->guard('web')->check() && request()->hasSession()) {
	// 
}

Tips

  • Do testowania REST API używać RapidApi Client w vscode.
  • Nie używać multiple guards z sanctum (to bez sensu), dodaj lepiej spatie permissions z rolami lub użyj alilities z sanctum.
  • W SPA $request->user()->currentAccessToken()->token nie działa (tylko z bearer tokens).
  • Pobieranie usera w sanctum auth('sanctum')->user() lub z $request->user().
<?php
namespace App\Http\Middleware\Api;
use Closure;
use Illuminate\Http\Request;
use Laravel\Sanctum\PersonalAccessToken;
/**
* Sanctum expired token middleware.
*
* Add middleware in bootstrap/app.php
* $middleware->api(prepend: [ \App\Http\Middleware\Api\ExpiredToken::class ]);
*/
class ExpiredToken
{
public function handle(Request $request, Closure $next)
{
$bearer = $request->bearerToken();
if ($bearer) {
$token = PersonalAccessToken::findToken($bearer);
if ($token instanceof PersonalAccessToken) {
if($token->expires_at && $token->expires_at->isPast()) {
$request->merge([
'token_expired' => $token->expires_at && $token->expires_at->isPast(),
'token_details' => $token
]);
return response()->json([
'message' => 'Expired Token.',
'token_expired' => $token->expires_at && $token->expires_at->isPast(),
'token_details' => $token
], 403);
}
}
}
return $next($request);
}
}
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\TokenAbility;
use App\Models\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
/**
* Login SPA application
*
* @param Request $request
* @return JsonResponse
*/
public function spaToken(Request $request): JsonResponse
{
$valid = $request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);
$user = User::where('email', $request->email)->first();
if(!Auth::guard('web')->attempt($valid, $request->boolean('remeber_me'))) {
return response()->json([
'success' => false,
'message' => 'The provided credentials are incorrect.'
], 401);
}
$request->session()->regenerate();
$token = $user->createToken('spa-token', [TokenAbility::USER_TOKEN])->plainTextToken;
return response()->json([
'success' => true,
'user' => $user,
'token' => $token,
], 200);
}
/**
* Login mobile application
*
* @param Request $request
* @return void
*/
public function apiToken(Request $request)
{
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
]);
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
return $user->createToken('api-token', [TokenAbility::USER_TOKEN])->plainTextToken;
}
public function logout(Request $request): JsonResponse
{
if(Auth::user()) {
if(Auth::guard('admin')->check()){
Auth::guard('admin')->logout();
} else {
Auth::guard('web')->logout();
}
}
if($request->user()) {
if($request->user()->currentAccessToken()) {
$request->user()->currentAccessToken()->delete();
}
}
$request->session()->invalidate();
return response()->json([
'success' => true,
'user' => null,
'token' => null,
], 200);
}
public function logoutAll(Request $request): JsonResponse
{
if(Auth::user()) {
if(Auth::guard('admin')->check()){
Auth::guard('admin')->logout();
} else {
Auth::guard('web')->logout();
}
}
if($request->user()) {
$request->user()->tokens()->delete();
}
$request->session()->invalidate();
return response()->json([
'success' => true,
'user' => null,
'token' => null,
], 200);
}
}
<?php
namespace App\Http\Controllers\Auth;
use App\Enums\TokenAbility;
use App\Models\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class RegisterController extends Controller
{
public function spaToken(Request $request): JsonResponse
{
$request->validate([
'name' => ['required', 'string', 'min:3'],
'email' => ['required', 'string', 'email', 'unique:users'],
'password' => ['required', 'string', 'min:12', 'confirmed'],
'password_confirmation' => ['required', 'string', 'min:12'],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$request->session()->regenerate();
$token = $user->createToken('spa-token', [TokenAbility::USER_TOKEN])->plainTextToken;
return response()->json([
'success' => true,
'user' => $user,
'token' => $token,
], 201);
}
public function apiToken(Request $request): JsonResponse
{
$request->validate([
'name' => ['required', 'string', 'min:3'],
'email' => ['required', 'string', 'email', 'unique:users'],
'password' => ['required', 'string', 'min:12', 'confirmed'],
'password_confirmation' => ['required', 'string', 'min:12'],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$request->session()->regenerate();
$token = $user->createToken('api-token', [TokenAbility::USER_TOKEN])->plainTextToken;
return response()->json([
'success' => true,
'user' => $user,
'token' => $token,
], 201);
}
}
<?php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::get('/logout', function () {
Auth::logout();
return response(null, 204);
});
Route::get('/login', function () {
// Login User
$user = User::first();
$token = $user->createToken('user-token-mobile', ['*'], now()->addYear())->plainTextToken;
Auth::login($user);
// Save api bearer token
return response()->json([
'message' => 'Authenticated.',
'token' => $token,
]);
})->name('login');
Route::get('/user', function (Request $request) {
return response()->json([
'user' => $request->user(),
// Or with guard
// 'user' => auth('sanctum')->user(),
// Only with bearer token
// 'token' => $request->user()->currentAccessToken()->token ?? null,
]);
})->middleware(['auth:sanctum']);
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SanctumTest extends TestCase
{
use RefreshDatabase;
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$this->seed();
$user = User::first();
$token = $user->createToken('user-token-mobile', ['*'], now()->addYear())->plainTextToken;
$response = $this->withToken($token)->getJson('/api/user');
$response->assertStatus(200);
}
/**
* Error test example.
*/
public function test_the_application_error_response(): void
{
$this->seed();
$response = $this->withToken('invalidtoken')->getJson('/api/user');
$response->assertStatus(401);
}
/**
* A middleware test example.
*/
public function test_the_application_expired_token_middleware_response(): void
{
$this->seed();
$user = User::first();
$token = $user->createToken('user-token-mobile', ['*'], now()->subYear())->plainTextToken;
$response = $this->withToken($token)->getJson('/api/user');
$response->assertStatus(403);
}
}
<template>
How To Install Vue 3 in Laravel 10 with Vite.
</template>
<script setup>
import axios from 'axios';
import { onMounted } from 'vue';
onMounted(async () => {
// Cookie
await axios.get('/sanctum/csrf-cookie');
// Logout
await axios.get('/api/logout');
// Login
const res2 = await axios.get('/api/login');
console.log(res2.data);
// Authenticated data
const res3 = await axios.get('/api/user');
console.log(res3.data);
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment