Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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:api

The 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 everywhere

Rate 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.

Thanks for reading!

Donate to the site — we work hard for you.