DEV3 Technologies home

Sanctum tokens with abilities and rate limits

Issue scoped personal access tokens with named abilities, enforce them per route, and rate limit API calls per token instead of per IP.

LaravelAPIssanctumtokensabilitiesrate-limit

Code

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class TokenController extends Controller
{
    /** Only these can ever be granted. */
    private const ABILITIES = ['orders:read', 'orders:write', 'products:read', 'products:write'];

    public function store(Request $request): JsonResponse
    {
        $data = $request->validate([
            'name' => ['required', 'string', 'max:60'],
            'abilities' => ['required', 'array', 'min:1'],
            'abilities.*' => ['string', Rule::in(self::ABILITIES)],
            'days' => ['nullable', 'integer', 'between:1,365'],
        ]);

        $token = $request->user()->createToken(
            $data['name'],
            $data['abilities'],
            now()->addDays($data['days'] ?? 90),
        );

        // The plain text value is shown once and cannot be recovered later.
        return response()->json(['token' => $token->plainTextToken, 'abilities' => $data['abilities']], 201);
    }

    public function destroy(Request $request, int $id): JsonResponse
    {
        $request->user()->tokens()->whereKey($id)->delete();

        return response()->json(status: 204);
    }
}

How it works

What it solves

An API key that can do everything is a liability. Integrations should receive only the permissions they need, and one noisy client should not exhaust the quota of everyone behind the same office IP. Sanctum already stores abilities on each token; this wires them to routes and to the limiter.

How it works

TokenController validates the requested abilities against an allow-list, so a client cannot invent *, and returns the plain text token exactly once. Tokens carry an expiry too, which is passed as the third argument to createToken.

Routes then declare what they need with the abilities middleware (all listed abilities required) or ability (any one is enough). Read routes ask for orders:read, write routes for orders:write.

The rate limiter named api-token is defined in the service provider. It keys on the token id when a user is authenticated through a token, falling back to the IP for anonymous traffic, and gives write-capable tokens a higher ceiling. Responses automatically include Retry-After and X-RateLimit-* headers.

Adapting it

Rename the abilities to your resources, load the allow-list from config, and add a listing endpoint that shows last_used_at so owners can revoke stale tokens.

Pitfalls

  • Register the abilities middleware aliases in bootstrap/app.php.
  • Token abilities are checked, but model policies still need to run for ownership.

How to use it

  1. Install Sanctum (php artisan install:api) and add HasApiTokens to your User model.
  2. Copy the controller; add the limiter to your service provider's boot().
  3. Register aliases in bootstrap/app.php: 'abilities' => CheckAbilities::class, 'ability' => CheckForAnyAbility::class.
  4. Add the routes excerpt to routes/api.php.
  5. Create a token with POST /api/tokens and send it as Authorization: Bearer <token>.

Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.

Related snippets