API tokens and rate limiting
A mobile app or another service cannot log in through a form and keep a session. They receive a token — a long random string — and send it with every request. In Laravel the Sanctum package issues tokens.
Installation
php artisan install:apiThe command installs Sanctum, creates the tokens table and the routes/api.php file. Add the trait to the user model:
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
}Issuing a token
// routes/api.php
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
Route::post('/login', function (Request $request) {
$data = $request->validate([
'email' => 'required|email',
'password' => 'required',
'device' => 'required',
]);
$user = User::where('email', $data['email'])->first();
if (!$user || !Hash::check($data['password'], $user->password)) {
throw ValidationException::withMessages([
'email' => 'Wrong email or password.',
]);
}
return ['token' => $user->createToken($data['device'])->plainTextToken];
});Protected routes
Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', fn (Request $request) => $request->user());
Route::apiResource('posts', PostController::class);
});The client sends the token in a header:
curl https://example.com/api/user -H "Authorization: Bearer 1|Xk3f9aB..." -H "Accept: application/json"Without a token, or with a wrong one, the response is a 401 code.
Token abilities
A token can be allowed only some actions:
$token = $user->createToken('reports', ['posts:read'])->plainTextToken;public function store(Request $request)
{
if (!$request->user()->tokenCan('posts:write')) {
abort(403);
}
// …
}Revoking
$request->user()->currentAccessToken()->delete(); // log out on this device
$request->user()->tokens()->delete(); // log out everywhereRate limiting
To stop one client from overloading the server, the throttle middleware limits the number of requests:
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
// no more than 60 requests a minute
});When the limit is exceeded, the response is a 429 code with a Retry-After header.
Your own rules
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('uploads', function (Request $request) {
return Limit::perHour(20)->by($request->user()?->id ?: $request->ip());
});
}Route::post('/files', [FileController::class, 'store'])->middleware('throttle:uploads');The by() method sets what to count by: the user or the address.
Login is limited too
Route::post('/login', [AuthController::class, 'login'])->middleware('throttle:5,1');Five attempts a minute make password guessing pointless.
Common mistake
The plainTextToken value is available only at the moment of creation: the database stores a hash, and the token cannot be shown again. Hand it to the client at once. And a token is as much a secret as a password: do not write it to the log, do not pass it in the request address, and set a lifetime with the expiration setting in config/sanctum.php.