API authentication should be simple. Laravel Sanctum provides lightweight token authentication for SPAs and mobile apps without OAuth complexity. At ZIRA Software, Sanctum secures API access for React and Vue frontends.
Why Sanctum?
Sanctum vs Passport:
- Sanctum: Simple token auth for SPAs and mobile
- Passport: Full OAuth2 server for third-party access
Use Sanctum when:
- Building SPA with Laravel API backend
- Mobile app authentication
- Simple API token access
- Don't need OAuth2 complexity
Use Passport when:
- Third-party API access
- OAuth2 required
- Complex authorization scopes
Installation
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
Configuration:
// config/sanctum.php
return [
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1')),
'expiration' => null, // Token expiration in minutes (null = never)
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];
SPA Authentication
Setup for same-domain SPAs:
// app/Http/Kernel.php
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
API routes:
// routes/api.php
use App\Http\Controllers\Auth\LoginController;
Route::post('/login', [LoginController::class, 'login']);
Route::post('/logout', [LoginController::class, 'logout']);
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::apiResource('posts', PostController::class);
});
Login controller:
// app/Http/Controllers/Auth/LoginController.php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
class LoginController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return response()->json([
'message' => 'Logged in successfully',
'user' => Auth::user(),
]);
}
return response()->json([
'message' => 'Invalid credentials',
], 401);
}
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json([
'message' => 'Logged out successfully',
]);
}
}
Frontend (React/Vue):
// First, get CSRF cookie
await axios.get('/sanctum/csrf-cookie');
// Then login
await axios.post('/api/login', {
email: 'user@example.com',
password: 'password',
});
// Subsequent requests automatically authenticated
const response = await axios.get('/api/user');
Token Authentication (Mobile Apps)
Issue tokens:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
}
Create token on login:
// app/Http/Controllers/Auth/MobileLoginController.php
public function login(Request $request)
{
$credentials = $request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (!Auth::attempt($credentials)) {
return response()->json([
'message' => 'Invalid credentials',
], 401);
}
$user = Auth::user();
// Create token with abilities
$token = $user->createToken('mobile-app', ['posts:read', 'posts:write']);
return response()->json([
'token' => $token->plainTextToken,
'user' => $user,
]);
}
Use token in requests:
curl https://api.zirasoftware.com/api/user \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
Mobile app (React Native):
// Store token securely
await SecureStore.setItemAsync('api_token', token);
// Use in requests
const token = await SecureStore.getItemAsync('api_token');
const response = await axios.get('/api/user', {
headers: {
'Authorization': `Bearer ${token}`,
},
});
Token Abilities (Scopes)
Define abilities:
// Create token with specific abilities
$token = $user->createToken('app-token', [
'posts:read',
'posts:write',
'posts:delete',
]);
// Check abilities in controller
public function delete(Request $request, Post $post)
{
if (!$request->user()->tokenCan('posts:delete')) {
return response()->json(['message' => 'Forbidden'], 403);
}
$post->delete();
return response()->json(['message' => 'Post deleted']);
}
Middleware for abilities:
Route::middleware(['auth:sanctum', 'abilities:posts:delete'])
->delete('/posts/{post}', [PostController::class, 'destroy']);
// Multiple abilities (any)
Route::middleware(['auth:sanctum', 'ability:posts:read,posts:write'])
->get('/posts', [PostController::class, 'index']);
Token Management
List user tokens:
$tokens = $request->user()->tokens;
foreach ($tokens as $token) {
echo $token->name;
echo $token->last_used_at;
echo $token->created_at;
}
Revoke tokens:
// Revoke specific token
$request->user()->tokens()->where('id', $tokenId)->delete();
// Revoke current token
$request->user()->currentAccessToken()->delete();
// Revoke all tokens
$request->user()->tokens()->delete();
Token expiration:
// config/sanctum.php
'expiration' => 60 * 24, // 24 hours
// Or set per token
$token = $user->createToken('app-token', ['*'], now()->addDays(7));
CORS Configuration
For SPA on different domain:
// config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_origins' => [
'https://app.zirasoftware.com',
],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
Sanctum stateful domains:
SANCTUM_STATEFUL_DOMAINS=app.zirasoftware.com
Testing
Test token authentication:
use Laravel\Sanctum\Sanctum;
public function test_user_can_access_protected_route()
{
$user = User::factory()->create();
Sanctum::actingAs($user, ['posts:read']);
$response = $this->getJson('/api/posts');
$response->assertStatus(200);
}
public function test_user_cannot_delete_without_ability()
{
$user = User::factory()->create();
$post = Post::factory()->create();
Sanctum::actingAs($user, ['posts:read']);
$response = $this->deleteJson("/api/posts/{$post->id}");
$response->assertStatus(403);
}
Rate Limiting
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
Route::get('/posts', [PostController::class, 'index']);
});
Security Best Practices
1. Use HTTPS only:
// Force HTTPS in production
if (app()->environment('production')) {
URL::forceScheme('https');
}
2. Regenerate tokens periodically:
// Rotate token every 30 days
if ($user->currentAccessToken()->created_at->lt(now()->subDays(30))) {
$user->currentAccessToken()->delete();
// Force re-login
}
3. Store tokens securely:
- Mobile: Use secure storage (Keychain, Keystore)
- Web: HttpOnly cookies (automatic with SPA mode)
4. Implement logout everywhere:
public function logoutAllDevices(Request $request)
{
$request->user()->tokens()->delete();
return response()->json(['message' => 'Logged out from all devices']);
}
Conclusion
Laravel Sanctum provides simple, secure API authentication. Perfect for SPAs and mobile apps without OAuth2 complexity. Session-based for same-domain SPAs, token-based for mobile.
Building an API with Laravel? Contact ZIRA Software for secure API development.