Building a secure API is essential for modern web and mobile products. Laravel Sanctum offers a lightweight, Laravel-native way to protect APIs—without the overhead of a full OAuth2 server.
Whether you’re shipping a mobile app, SPA, or third-party integration, Sanctum helps you issue tokens, validate requests, and guard routes with policies and middleware you already use in Laravel.
Sanctum solves two common authentication needs:
For REST APIs and mobile apps, personal access tokens are the usual choice: after login, the user receives a token; each protected request includes that token; Laravel resolves the user and applies gates/policies before returning data.
composer require laravel/sanctum php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" php artisan migrate app/Models/User.php
?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password', 'remember_token'];
}
bootstrap/app.php or app/Http/Kernel.php — ensure API routes use Sanctum stateful domains when needed for SPAs; for token-only APIs, auth:sanctum on routes is enough.
config/sanctum.php — set stateful domains for SPA cookie auth; for mobile/token APIs, focus on expiration if you want token expiry.
?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\LoginRequest;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class AuthController extends Controller
{
public function login(LoginRequest $request): JsonResponse
{
$user = User::where('email', $request-email)-first();
if (! $user || ! Hash::check($request-password, $user-password)) {
return response()-json(['message' = 'Invalid credentials.'], 401);
}
// Optional: revoke old tokens for this device name
$user-tokens()-where('name', $request-device_name)-delete();
$token = $user-createToken(
$request-device_name ?? 'mobile-app',
['posts:read', 'profile:update'] // optional abilities
)-plainTextToken;
return response()-json([
'token' = $token,
'token_type' = 'Bearer',
'user' = [
'id' = $user-id,
'name' = $user-name,
'email' = $user-email,
],
]);
}
public function logout(Request $request): JsonResponse
{
// Revoke current token only
$request-user()-currentAccessToken()-delete();
return response()-json(['message' = 'Logged out successfully.']);
}
public function me(Request $request): JsonResponse
{
return response()-json($request-user());
}
}
app/Http/Requests/LoginRequest.php
?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LoginRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'email' = ['required', 'email'],
'password' = ['required', 'string'],
'device_name' = ['nullable', 'string', 'max:100'],
];
}
}
?php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
use Illuminate\Support\Facades\Route;
Route::post('/login', [AuthController::class, 'login']);
Route::middleware('auth:sanctum')-group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/me', [AuthController::class, 'me']);
Route::apiResource('posts', PostController::class)-only(['index', 'show', 'store']);
});
?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$posts = Post::where('user_id', $request-user()-id)-latest()-paginate(10);
return PostResource::collection($posts);
}
public function store(Request $request)
{
$data = $request-validate([
'title' = ['required', 'string', 'max:160'],
'body' = ['required', 'string'],
]);
$post = $request-user()-posts()-create($data);
return new PostResource($post);
}
}
When creating tokens with abilities:
$token = $user-createToken('mobile-app', ['posts:read', 'posts:create'])-plainTextToken;
In routes or controllers:
Route::post('/posts', [PostController::class, 'store']) -middleware(['auth:sanctum','abilities:posts:create']);
Or in controller:
if ($request-user()-tokenCan('posts:create')) { // allowed }
cURL
curl -X GET https://api.yoursite.com/api/posts \ -H"Accept: application/json"\ -H"Authorization: Bearer 1|yourPlainTextTokenHere"
const token = localStorage.getItem('api_token'); // prefer secure storage on mobile
const response = await fetch('https://api.yoursite.com/api/me', {
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
});
const user = await response.json();
Flutter (Dio example)
dio.options.headers['Authorization'] = 'Bearer $token'; final response = await dio.get('/api/posts');
A well-secured API protects customer data, reduces abuse, and builds trust. Sanctum keeps authentication simple, maintainable, and aligned with Laravel—so your team can focus on features, not reinventing auth.
At ULT (Universal Links Technology), we use Laravel Sanctum to build secure, scalable APIs for SPAs, mobile apps, and integrations—pairing token auth with clean architecture, queues, and performance best practices.