Content Index
- What is Laravel Sanctum?
- 1 SPA Authentication
- 2 API Token-Based Authentication
- Generating Access Tokens
- More Examples with Sanctum
- Strategies for Renewing Access Tokens in a Rest API with Laravel Sanctum
- 1. Hybrid Authentication: Cookies vs. Tokens
- 2. The Token Lifecycle: Mitigating Dead Records
- Strategy Zero: Scheduled Cleanup (Cron Jobs)
- Strategy One: Active Renewal (Silent Refresh)
- Strategy Two: Dynamic Token Extension
- 3. Practical Implementation: Early Validation and Session Extension
- Justification for Instance Verification
- 4. Centralization and Unique Device Identification
- Limitations of Device-Based Control
Laravel Sanctum is the official authentication package for single page applications (SPAs) and mobile applications within the Laravel ecosystem. Its main purpose is to protect a Rest API in Laravel through required authentication, without the complexity of implementing a full OAuth server. Unlike adding a traditional session, REST APIs are recommended to be stateless, and that is precisely where laravel/sanctum comes into play: it allows each user of your application to generate multiple API tokens for their account, offering a lightweight authentication system for both SPAs and simple APIs.
Laravel Sanctum is an ideal tool when you want to protect a Rest API created with Laravel, either through personal access tokens or via a hybrid scheme combining session and cookies. Before looking at how it is implemented in practice, it is helpful to thoroughly understand what Sanctum is and what two authentication schemes it offers.
Using Laravel Sanctum ensures that only authenticated users can access routes that require it, automatically returning a 401 Unauthorized error for requests without valid credentials.
The idea behind Sanctum is to protect Laravel application APIs through authentication and access tokens. By doing so, it ensures that only authenticated and authorized requests are permitted, securing the integrity of the application and the data involved.
Laravel Sanctum offers two main schemes to work with, and choosing the right one depends on your client type:
What is Laravel Sanctum?
Laravel Sanctum is Laravel's lightweight authentication package that provides two distinct mechanisms: authentication for single page applications (SPAs) —based on cookies and session— and authentication via personal API tokens (Personal Access Tokens). Sanctum allows generating and managing API tokens: the backend issues them through a login endpoint, the client stores them and sends them in the Authorization: Bearer <token> header on each subsequent request to access protected resources.
These tokens are very versatile, as you can use them to authenticate API requests, restrict access to specific routes, and even assign abilities (scopes) to granularly control what actions each token can perform. From Laravel, you can always identify the user making the request and apply any traces or rate limits you need.
1 SPA Authentication
This scheme is useful when you have a SPA (Single Page Application) —for example, built with Vue or React— and you want to authenticate the user and protect your Rest API. Instead of issuing a token, it relies on a session and cookie mechanism that Sanctum manages transparently. It is the easiest of the two schemes to configure:
At the kernel level, you must enable the Sanctum middleware for the api group, which is responsible for ensuring that requests from the frontend are stateful:
//App/Http/Kernel
//***
class Kernel extends HttpKernel
{
//***
'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
Next, you must specify the domain that cookie-based authentication will use in the config/sanctum.php file, specifically under the stateful key. This list of domains is preferably read from the SANCTUM_STATEFUL_DOMAINS environment variable defined in your .env file:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1,laraprimerospasos.test',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
If you are developing on Mac or Linux with Laravel Sail, you generally do not need to modify this file, as localhost is already included by default. For production, simply add your application's domain to the SANCTUM_STATEFUL_DOMAINS variable in your .env.
In the config/cors.php file, you must enable the supports_credentials key to true. This is necessary for the browser to send session cookies along with cross-origin requests to your API.
To use Axios on the frontend, find the resources/js/bootstrap.js file and add the following line:
axios.defaults.withCredentials = true;This tells Axios to include cookies with every request, allowing Sanctum to transparently identify the user without needing an explicit token in the header.
Finally, protect your routes using the auth:sanctum middleware. As an example, we will protect a set of routes in routes/api.php:
Route::group(['middleware' => 'auth:sanctum'], function () {
Route::resource('category', CategoryController::class)->except(["create", "edit"]);
Route::resource('post', PostController::class)->except(["create", "edit"]);
});With this, everything is ready: the user only needs to log in once, and Sanctum will manage the session automatically. This scheme fits perfectly with SPAs that reside on the same domain or subdomain as the API.
2 API Token-Based Authentication
The second scheme is the most traditional and common one for protecting APIs consumed by mobile clients, third-party services, or SPAs on different domains. Here, upon authentication, the backend generates a personal access token that the client must store and send with every request.
If you were using the previous scheme (SPA) and want to switch to tokens, first disable the EnsureFrontendRequestsAreStateful middleware:
'api' => [ //\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
***
],Generating Access Tokens
It is worth mentioning that Sanctum already comes installed in modern Laravel projects (integrated by default since Laravel 11), so you normally do not need to install it manually. If you are working with an older project, you can install it via Composer.
In our REST controller to manage the user, within the context of our Laravel course, the file would be:
app\Http\Controllers\Api\UserController.php
We will create a login function that uses Auth::attempt() to verify the credentials provided in the request:
$credentials = [
'email' => $request->email,
'password' => $request->password
];Once the credentials are validated, we generate the token with createToken() and return its plain text representation via the plainTextToken property. It is important to understand that this is the only time the complete token can be read; afterwards, Sanctum only stores its hash in the database:
Auth::user()->createToken('myapptoken')->plainTextToken;Below is the complete code for the login method:
public function login(Request $request)
{
$credentials = [
'email' => $request->email,
'password' => $request->password
];
if (Auth::attempt($credentials)) {
$token = Auth::user()->createToken('myapptoken')->plainTextToken;
session()->put('token', $token);
return response()->json($token);
}
return response()->json("Invalid user and/or password", 422);
}The most important thing to note is that although Sanctum provides all the mechanisms to generate and validate the token, the issuance logic —when it is created, with what name, with what abilities— depends entirely on the developer. This gives you complete control over your API's authentication strategy.
To create the controller via Artisan:
$ php artisan make:controller Api/UserControllerWith the following complete content:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class UserController extends Controller
{
public function login(Request $request)
{
$credentials = [
'email' => $request->email,
'password' => $request->password
];
if(Auth::attempt($credentials)){
$token = Auth::user()->createToken('myapptoken')->plainTextToken;
return response()->json($token);
}
return response()->json("Invalid user and/or password");
}
}Remember: Sanctum already comes installed in modern Laravel projects.
More Examples with Sanctum
Here are some other examples featuring Laravel Sanctum authentication and route protection.
In case the project does not have Sanctum, install it via Composer:
composer require laravel/sanctumThen, publish the configuration files and required migrations:
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrateThe vendor:publish command will generate the config/sanctum.php file with all configurable options, and migrate will create the personal_access_tokens table in your database, which is where Sanctum stores the hashes of issued tokens.
You can now protect your routes using the auth:sanctum middleware in your routes/api.php file:
Route::middleware('auth:sanctum')->group(function () {
Route::get('/post', function () {
return view('dashboard');
});
});In this example, the /post route will only be accessible if the user is authenticated via the Sanctum middleware. Any request without a valid token will automatically receive a 401 Unauthorized response.
You can generate an API token for an authenticated user with a single line:
$token = $user->createToken('token-name')->plainTextToken;This code generates an API token for the given user with the name token-name. The client then sends it in the Authorization: Bearer <token> header to authenticate its requests. Here is another complete example of a login endpoint with Sanctum in routes/api.php:
use App\\Models\\User;
use Illuminate\\Http\\Request;
use Illuminate\\Support\\Facades\\Hash;
Route::post('/login', function (Request $request) {
$user = User::where('email', $request->email)->first();
if (! $user || ! Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
$token = $user->createToken($request->device_name)->plainTextToken;
return ['token' => $token];
});Note that in this example $request->device_name is used as the token name, which makes it possible to identify from which device each token was issued. This practice is especially useful when users manage their active sessions from their profile.
Strategies for Renewing Access Tokens in a Rest API with Laravel Sanctum
Laravel Sanctum is the standard tool within the Laravel ecosystem for managing authentication in modern applications. When building hybrid platforms —integrating web clients such as single page applications (SPA) in Vue and native mobile clients— it is essential to design a solid strategy for the management, persistence, and lifecycle of personal access tokens (Personal Access Tokens).
Unlike traditional web-only session schemes, the coexistence of multiple devices introduces security and performance challenges that must be resolved directly in the backend architecture.
1. Hybrid Authentication: Cookies vs. Tokens
Sanctum offers a dual approach to protect API resources, allowing two mechanisms to be combined in parallel:
- Cookie-Based Authentication (Stateful): Designed specifically for SPAs or web applications hosted on the same top-level domain. It employs encrypted cookies alongside CSRF attack protection. This method is active by default for the desktop environment and requires the
EnsureFrontendRequestsAreStatefulmiddleware. - Token-Based Authentication (Bearer Tokens): Mandatory for mobile devices or third-party external services that do not share browser cookie storage. The client receives a plain text token (
plainTextToken) after validating its credentials, and must attach it to theAuthorization: Bearer <token>header of every HTTP request.
The server processes both paths automatically through Sanctum's middleware, translating the origin into a single authenticated user instance that you can retrieve with $request->user().
2. The Token Lifecycle: Mitigating Dead Records
By default, Sanctum allows created tokens to have an indefinite validity if the expiration directive in the config/sanctum.php file is set to null.
config/sanctum.php
'expiration' => null, // Tokens do not expire by defaultAlthough this approach simplifies initial development, it represents an infrastructure and security vulnerability. If users frequently log in from multiple browsers or devices, the personal_access_tokens table will accumulate thousands of obsolete records ("dead records") that will slow down database queries since there is no automatic cleanup process.
To solve this problem, there are three main design strategies:
Strategy Zero: Scheduled Cleanup (Cron Jobs)
This consists of keeping tokens without an explicit expiration date in Sanctum's configuration, but delegating cleanup to a Laravel background process (Scheduled Tasks). The script periodically analyzes the last_used_at column and permanently deletes those records that have shown no activity over a range of several months.
Strategy One: Active Renewal (Silent Refresh)
This technique sets a strict expiration period in the configuration (for example, 30 days). The client (whether in Vue or Flutter) has a dedicated endpoint in the API that, before reaching the deadline, invalidates the current token and generates a new one, updating the device's local storage (localStorage or SharedPreferences) transparently to the user.
Strategy Two: Dynamic Token Extension
This is the strategy adopted in high-availability architectures. Instead of rotating the token's plain text string, the backend dynamically extends the validity of the record by modifying its internal metadata every time the user consumes the API. In this way, a session used daily never expires, but it is automatically invalidated if the device stops making requests for a prolonged period.
3. Practical Implementation: Early Validation and Session Extension
Regardless of the selected strategy, the fundamental principle of client architecture dictates that the application must verify session integrity upon initial startup. You should not wait until the user attempts a critical action (such as a download or purchase) to discover that their token has been revoked, as this degrades the user experience with unexpected cascading errors.
In both web and mobile environments, a lightweight verification endpoint (for example, /api/v1/user-info) should be consumed in global lifecycle initialization hooks (App.vue or Flutter's main() method). If the server returns a 401 Unauthorized status code, the client intercepts the response, immediately clears local state variables, and redirects to the login form.
Below is the backend implementation for a hybrid verification endpoint that extends the token's lifecycle through forced record manipulation:
use Illuminate\Http\Request;
use Laravel\Sanctum\TransientToken;
use Laravel\Sanctum\PersonalAccessToken;
Route::middleware('auth:sanctum')->get('/v1/user-info', function (Request $request) {
$user = $request->user();
$currentToken = $user->currentAccessToken();
// Check that the token comes from the database and not a cookie (TransientToken)
if ($currentToken instanceof PersonalAccessToken) {
/**
* Force updating the creation date.
* By shifting 'created_at' to the current instant, we automatically extend
* the 30-day expiration window configured in Sanctum.
*/
$currentToken->forceFill([
'created_at' => now(),
])->save();
}
return response()->json([
'success' => true,
'data' => $user,
]);
});Justification for Instance Verification
The instanceof PersonalAccessToken validation is mandatory in hybrid infrastructures. When the user accesses from the web client via cookies, the currentAccessToken() method returns a TransientToken object. Since this object does not physically reside in the database table, it lacks the forceFill() method. Trying to update it would cause a fatal execution error in the backend.
4. Centralization and Unique Device Identification
To implement restrictive commercial policies —such as limiting the number of simultaneous sessions allowed on shared accounts—, it is essential to centralize token issuance logic in a single service or controller class.
When generating a token via the createToken() method, the use of generic or static strings should be avoided. Instead, the incoming HTTP request's User-Agent header is analyzed to record the browser name or the client's native runtime environment:
public function generateDeviceToken(Request $request, User $user): string
{
$userAgent = $request->header('User-Agent', 'Generic Device');
$deviceName = 'Unknown Device';
// Modular classification of the request origin
if (str_contains($userAgent, 'Dart')) {
$deviceName = 'Mobile Application (Flutter)';
} elseif (str_contains($userAgent, 'Firefox')) {
$deviceName = 'Mozilla Firefox';
} elseif (str_contains($userAgent, 'Chrome')) {
$deviceName = 'Google Chrome';
} elseif (str_contains($userAgent, 'Safari')) {
$deviceName = 'Apple Safari';
}
// Length limitation to comply with table schema constraints
$sanitizedName = substr($deviceName, 0, 180);
// Centralization of business rules before persistence
return $user->createToken($sanitizedName)->plainTextToken;
}Limitations of Device-Based Control
Although registering the browser name provides clarity to the user in their configuration panels and allows them to revoke specific accesses, it is not an infallible unique identifier for applying strict blocks. If a user legitimately operates from two different computers using the same browser (for example, Google Chrome on their work computer and personal computer), both requests will share the same device name.
If business logic requires blocking or destroying duplicate sessions based solely on that label, the valid session of the second device will be mistakenly closed. For scenarios requiring a unique hardware identity signature, the mobile client must extract the device's unique identifier (UUID) at the operating system level and explicitly transmit it in the authentication request parameters sent to the API.
Now, explore Laravel's template engine, Blade, in depth.
Source code:
https://github.com/libredesarrollo/book-course-laravel-base-api-11/releases/tag/v0.1