While session guards remember users through cookies and server-side sessions, token guards take a different approach. Laravel Sanctum's token authentication is stateless, fast, and perfect for APIs. But how does a simple bearer token authenticate millions of API requests? Let's explore the engineering behind Sanctum's token guard system.
What Are Token Guards?
Token guards authenticate users using tokens sent with each request, eliminating the need for server-side sessions. Unlike session-based authentication, token guards are completely stateless—every request contains all the information needed to identify the user.
// config/auth.php
'guards' => [
'sanctum' => [
'driver' => 'sanctum',
'provider' => 'users',
],
],
The Sanctum guard validates tokens from the Authorization header on every request.
How Sanctum Tokens Are Generated
When a user logs in via API, Sanctum creates a personal access token:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
}
// Creating a token
$user = User::find(1);
$token = $user->createToken('mobile-app');
// Returns: PersonalAccessToken object with plain-text token
$plainTextToken = $token->plainTextToken;
// Example: 1|abc123def456ghi789...
The token format consists of two parts:
1|abc123def456ghi789jkl012mno345pqr678stu901vwx234
│ │
│ └─ SHA-256 hashed random string (40 chars)
└─── Token ID in database
The ID allows quick database lookups, while the hash verifies authenticity.
Token Storage in Database
Sanctum stores tokens in the personal_access_tokens table:
// Migration
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable'); // User ID and type
$table->string('name'); // Token name/device
$table->string('token', 64)->unique(); // Hashed token
$table->text('abilities')->nullable(); // Permissions
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
Critical security detail: Sanctum stores the SHA-256 hash of the token, never the plain text. The plain-text token is only shown once during creation.
// What gets stored
DB::table('personal_access_tokens')->insert([
'tokenable_id' => 1,
'tokenable_type' => 'App\Models\User',
'name' => 'mobile-app',
'token' => hash('sha256', $plainTextToken), // Hashed!
'abilities' => '["*"]',
'created_at' => now(),
]);
The Token Validation Flow
When an API request arrives with a bearer token, Sanctum follows this validation process:
Step 1: Extract Token from Header
// Client sends request with Authorization header
Authorization: Bearer 1|abc123def456ghi789...
Sanctum extracts the token from the header:
// Behind the scenes
$token = $request->bearerToken();
Step 2: Parse Token ID and Hash
// Split token into ID and hash
[$id, $token] = explode('|', $bearerToken, 2);
The ID (before |) identifies which database record to query.
Step 3: Query Database
Sanctum queries the personal_access_tokens table:
// Behind the scenes
$accessToken = PersonalAccessToken::find($id);
if (!$accessToken) {
return null; // Invalid token ID
}
This is why the token format includes the ID—it enables efficient database lookups without scanning all tokens.
Step 4: Verify Token Hash
Sanctum compares the provided token hash with the stored hash:
// Behind the scenes
$hashedToken = hash('sha256', $token);
if (!hash_equals($accessToken->token, $hashedToken)) {
return null; // Token mismatch
}
The hash_equals() function prevents timing attacks by comparing hashes in constant time.
Step 5: Check Token Expiration
// If expires_at is set, check if expired
if ($accessToken->expires_at && $accessToken->expires_at->isPast()) {
return null; // Token expired
}
Step 6: Load User and Cache
Once validated, Sanctum loads the associated user:
// Behind the scenes
$user = $accessToken->tokenable; // Polymorphic relationship
// Cache for current request
$this->user = $user;
$this->currentAccessToken = $accessToken;
return $user;
The user and token are cached in memory for the request duration.
Token Abilities (Permissions)
Sanctum tokens can have specific abilities (scopes):
// Create token with limited abilities
$token = $user->createToken('read-only', ['posts:read', 'comments:read']);
// Check abilities in your code
if ($request->user()->tokenCan('posts:write')) {
// User can write posts
}
// Middleware protection
Route::middleware(['auth:sanctum', 'ability:posts:write'])
->post('/posts', [PostController::class, 'store']);
Abilities are stored as JSON in the database:
["posts:read", "comments:read", "posts:write"]
Sanctum checks abilities using simple array operations:
// Behind the scenes in tokenCan()
public function tokenCan($ability)
{
return in_array('*', $this->token->abilities) ||
in_array($ability, $this->token->abilities);
}
Token Lifecycle Management
Creating Tokens
// Basic token
$token = $user->createToken('device-name');
// Token with abilities
$token = $user->createToken('admin-token', ['*']);
// Token with expiration
$token = $user->createToken('temp-token');
$token->accessToken->update([
'expires_at' => now()->addDays(7),
]);
Revoking Tokens
// Revoke specific token
$user->tokens()->where('id', $tokenId)->delete();
// Revoke current token
$request->user()->currentAccessToken()->delete();
// Revoke all user tokens
$user->tokens()->delete();
Updating Last Used Timestamp
Sanctum automatically updates last_used_at:
// Behind the scenes after successful authentication
$accessToken->forceFill(['last_used_at' => now()])->save();
This helps track token activity and identify unused tokens.
Stateless vs Stateful Sanctum
Sanctum supports two authentication modes:
API Token Authentication (Stateless)
// Client includes token in every request
Authorization: Bearer 1|abc123...
// No session, no cookies
Auth::guard('sanctum')->user(); // Validates token each time
Perfect for mobile apps, SPAs on different domains, or third-party API access.
SPA Authentication (Stateful)
// Uses session cookies for same-domain SPAs
// Falls back to token authentication if cookie not present
// Best of both worlds
Sanctum intelligently detects which authentication method to use based on the request.
Performance Characteristics
Token authentication requires a database query on every request:
// Every API request with token guard:
// 1. SELECT from personal_access_tokens (by ID)
// 2. SELECT user data (via tokenable relationship)
// 3. UPDATE last_used_at (optional, can be disabled)
// Average time: 5-20ms depending on database
This is faster than session authentication which requires:
- Session lookup
- Session data deserialization
- User query
Caching Token Validation
For high-traffic APIs, consider caching token validation:
// Custom middleware
public function handle($request, $next)
{
$token = $request->bearerToken();
$user = Cache::remember("token:$token", 60, function () use ($token) {
return Auth::guard('sanctum')->user();
});
if (!$user) {
return response()->json(['error' => 'Unauthenticated'], 401);
}
return $next($request);
}
This reduces database queries but introduces a 60-second delay for token revocation.
Security Best Practices
1. Never Log Tokens
// WRONG - exposes tokens in logs
Log::info('User authenticated', ['token' => $token]);
// CORRECT - log only token ID
Log::info('User authenticated', ['token_id' => $accessToken->id]);
2. Use HTTPS Only
// Force HTTPS in production
if (!$request->secure() && app()->environment('production')) {
return response()->json(['error' => 'HTTPS required'], 403);
}
Tokens sent over HTTP can be intercepted.
3. Set Token Expiration
// Default: tokens never expire (risky)
$token = $user->createToken('app');
// Better: set expiration
$token->accessToken->expires_at = now()->addDays(30);
$token->accessToken->save();
4. Implement Rate Limiting
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])
->get('/user', function (Request $request) {
return $request->user();
});
Prevents token abuse through brute force.
Token Guard vs Session Guard
Understanding when to use each:
Use Token Guards When:
- Building APIs for mobile apps
- Third-party API access needed
- SPAs on different domains
- Stateless authentication required
- Microservices architecture
Use Session Guards When:
- Traditional server-rendered applications
- Single-domain web applications
- Browser-only access
- Need CSRF protection
- Existing session-based infrastructure
Common Implementation Patterns
API Login Endpoint
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['error' => 'Invalid credentials'], 401);
}
$token = $user->createToken('api-token')->plainTextToken;
return response()->json([
'token' => $token,
'user' => $user,
]);
}
Protected API Route
Route::middleware('auth:sanctum')->get('/profile', function (Request $request) {
return $request->user();
});
Multiple Device Management
// List all tokens
public function tokens(Request $request)
{
return $request->user()->tokens;
}
// Revoke specific device
public function revokeToken(Request $request, $tokenId)
{
$request->user()->tokens()->where('id', $tokenId)->delete();
return response()->json(['message' => 'Token revoked']);
}
Conclusion
Sanctum's token guard system elegantly solves API authentication through simple bearer tokens backed by robust database validation. By understanding token generation, validation flow, and security mechanisms, you can build secure, scalable APIs that serve millions of requests efficiently.
The stateless nature of token guards makes them perfect for modern applications where mobile apps, SPAs, and third-party integrations are the norm.
Building Laravel APIs or need help implementing Sanctum authentication? At NeedLaravelSite, we specialize in Laravel API development and application migrations from version 7 to 12. We build secure, performant APIs with proper authentication, rate limiting, and scalability.