Laravel Sanctum: A Setup Guide That Actually Works
Laravel Sanctum is the authentication package we reach for on the vast majority of Laravel API projects. It is lightweight, well-integrated with Laravel's auth system, and handles the two most common use cases - API token authentication for mobile and third-party clients, and session-based authentication for SPAs - without requiring external services or complex key management. It is also a consistent source of configuration confusion, particularly for developers new to it. This post covers both modes in full, the mistakes that silently break each, and how to choose between them.
What Sanctum actually does
Sanctum issues a plain-text token string, stores a hashed version in a personal_access_tokens database table, and validates it on every request via a database lookup. That is the entirety of how API token authentication works in Sanctum. There is no JWT, no signature verification, no decoding - just a database row. This is important to understand because it explains both Sanctum's strengths and its trade-offs.
The strength: token revocation is trivially simple. Delete the row, the token is invalid immediately. With JWT, you need a denylist, a Redis store, or an expiry-based approach that leaves a window of valid tokens after revocation. With Sanctum, there is no window. If you need to invalidate a token - because a user logs out, because their account is compromised, or because you want to force re-authentication across all devices - one database operation achieves it.
The trade-off: every authenticated request hits the database. In high-throughput scenarios this matters. For most applications it does not.
Installation
Laravel 11 and 12 ship with Sanctum installed and configured. If you are on Laravel 10 or below:
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Add the HasApiTokens trait to your User model. This is the step most commonly forgotten, and it produces a cryptic error when missed:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable { use HasApiTokens; }
Mode one: API token authentication
API token authentication is the right mode for mobile applications, third-party API consumers, and any client that cannot maintain a session cookie. The flow is: the client authenticates with credentials, receives a token, stores it, and sends it on every subsequent request in the Authorization header.
Routes:
Route::post('auth/login', [AuthController::class, 'login']);
Route::post('auth/register', [AuthController::class, 'register']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('auth/me', [AuthController::class, 'me']);
Route::post('auth/logout', [AuthController::class, 'logout']);
});
Login and token issuance:
public function login(Request $request)
{
$request->validate(['email' => 'required|email', 'password' => 'required']);
if (! Auth::attempt($request->only('email', 'password'))) {
return response()->json(['message' => 'Invalid credentials'], 401);
}
$token = Auth::user()->createToken('auth_token')->plainTextToken;
return response()->json(['token' => $token, 'user' => Auth::user()]);
}
The Authorization header format:
Authorization: Bearer 1|yourtoken...
Note the format: a numeric token ID, a pipe character, then the random token string. The full string must be sent. Sending only the random portion after the pipe will not work.
Logout:
$request->user()->currentAccessToken()->delete(); // Current device
$request->user()->tokens()->delete(); // All devices
Token expiry
By default, Sanctum tokens never expire. In config/sanctum.php:
'expiration' => 60 * 24 * 60, // 60 days in minutes
'expiration' => null, // Never expire
For most API projects we set a sensible expiry - 30 to 90 days depending on the security requirements - and provide a token refresh mechanism or require re-login when it lapses. Long-lived tokens with no expiry are a liability if one is compromised.
Token abilities
Sanctum supports token abilities, which allow you to scope what a given token can do. This is particularly useful for third-party integrations where you want to issue a read-only token or a token restricted to specific operations:
$token = $user->createToken('mobile-app', ['read:orders', 'create:orders'])->plainTextToken;
In your route middleware or controller, check abilities against the current token:
$request->user()->tokenCan('create:orders');
Or use the CheckAbilities and CheckForAnyAbility middleware:
Route::middleware(['auth:sanctum', 'abilities:read:orders'])->get('/orders', ...);
Token abilities are worth building into your token issuance flow from the start, even if your initial implementation only uses a single ability level. Retrofitting scoped tokens into an existing API is considerably more work.
Mode two: SPA authentication
SPA authentication is the right mode for a first-party single-page application served from the same top-level domain as your Laravel API. Instead of tokens, Sanctum uses Laravel's session and CSRF protection - standard cookie-based authentication. This mode is stateful and requires that your SPA and API share a domain (or are on the same domain).
The SPA flow differs significantly from API token flow:
- The SPA makes a
GETrequest to/sanctum/csrf-cookieto initialise the CSRF cookie. - The SPA sends login credentials via
POST /login, which creates a session. - Subsequent requests include the session cookie and the CSRF token automatically.
In config/sanctum.php, configure your stateful domains:
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1'))
In production, this should include your SPA's domain. The middleware stack in app/Http/Kernel.php (or the middleware configuration in Laravel 11+) must include EnsureFrontendRequestsAreStateful on your API routes that serve the SPA.
SPA authentication uses the web guard internally, not the api guard. This is a common source of confusion: even though you are building an API, Sanctum's SPA mode routes through the session-based web guard. Your routes still use auth:sanctum middleware - Sanctum handles the guard detection automatically - but the underlying mechanism is session cookies, not tokens.
The mistakes that silently break Sanctum
1. Missing HasApiTokens on the User model. The error message when this is absent is not immediately obvious. Always confirm the trait is present first when debugging token-related issues.
2. Wrong Authorization header format. The header must be Authorization: Bearer FULL_TOKEN_STRING. Not Token, not a query parameter, not a custom header. The full token string including the numeric prefix is required.
3. CORS misconfiguration. For SPA authentication specifically, your CORS configuration in config/cors.php must include your frontend's origin and set supports_credentials to true:
'allowed_origins' => ['https://yourapp.com'],
'supports_credentials' => true,
Without supports_credentials, browsers will not send or receive cookies on cross-origin requests, and SPA authentication will silently fail.
4. Not setting SANCTUM_STATEFUL_DOMAINS in production. The default stateful domains are localhost values. Forgetting to set this environment variable in production means SPA authentication will appear to work locally and fail completely in production.
5. Using API token auth for an SPA on the same domain. If your SPA is served from the same domain as your API, session-based SPA authentication is simpler, more secure (no token storage on the client), and better aligned with how browsers handle cookies. Using API token authentication in this scenario introduces unnecessary complexity and client-side token storage concerns.
6. No token expiry in production. The default is no expiry. This is acceptable in development; it is not acceptable in production for any application handling sensitive data.
Sanctum or JWT?
We have covered API authentication in more depth elsewhere, but the short version: use Sanctum for first-party APIs serving your own SPA or mobile application. Use JWT only when you need stateless, cross-service token validation in a microservices architecture where you cannot afford a database lookup per request, or where tokens need to be validated by services that do not share your database.
For the vast majority of Laravel projects - a backend API consumed by a React SPA or a mobile application - Sanctum is the right choice. It is simpler to set up, simpler to reason about, and token revocation works correctly without additional infrastructure.
