itmm / api-client-guard
Reusable Laravel API client security module: client credential/token management, Sanctum token caching, per-token TTL, IP whitelisting, ASR request signing, token-ability enforcement, and API activity logging.
Requires
- php: ^8.4
- ext-openssl: *
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/support: ^13.0
- itmm/easy-repository: ^2.0
- laravel/sanctum: ^4.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^11.0
README
Reusable Laravel API client security module: client credential/token management, Sanctum token caching, per-token TTL, IP whitelisting, ASR (asymmetric request signing), token-ability enforcement, and API activity logging — extracted from m-upload so any itmm project can install it instead of re-implementing the same middleware stack.
Frontend is out of scope by design — every host project has its own UI patterns. This package is backend-complete: install Sanctum, run migrations, and the middleware/services/admin controllers are ready to wire into your own routes.
What this package does NOT decide for you
Every host project scopes API clients differently — one app might scope a token to an Application, another to a Location, another to nothing at all. This package stays deliberately opinion-free about that: personal_access_tokens gets a generic nullable useable_type/useable_id morph, and Client::createToken() accepts an optional ?Model $useable parameter. You decide what model (if any) that is, and whether/how a client is authorized to request it — write your own thin controller for that and call AuthService::generateToken($data, $resolvedModel) directly.
Installation
Migrations and models follow the same publish-then-own pattern as spatie/laravel-permission: nothing runs straight out of vendor/, you publish your own copy into the app and that's what actually executes.
composer require itmm/api-client-guard(once published — for now, path-repository it orcomposer require itmm/api-client-guard:@devwith a local path repo).- Install Laravel's own Sanctum support first, if you haven't already:
php artisan install:api
This createspersonal_access_tokens. This package's own migration only adds theuseablemorph columns to that table — it does not create it. - Publish and run migrations:
php artisan vendor:publish --tag=api-client-guard-migrations php artisan migrate
This copies four migration files into your owndatabase/migrations/(clients,client_ip_whitelists,api_activity_logs, and theuseablecolumns added topersonal_access_tokens) — edit them before migrating if you need to. Nothing migrates automatically just fromcomposer require. - (Optional) Publish the config to customize cache keys, route prefix, ASR tolerance, etc.:
php artisan vendor:publish --tag=api-client-guard-config
That's it — the service provider auto-registers middleware aliases, observers, cache-invalidation listeners, and the Sanctum personal-access-token model. No bootstrap/app.php edits needed for the package itself.
What's auto-registered vs. what you wire yourself
- Auto-loaded routes (safe regardless of your app's auth stack):
POST {route_prefix}/auth/token— credential exchange (client_id + client_secret → Sanctum token). Inherently public.GET {route_prefix}/tokens/current— info about the token making the request. Protected byauth:sanctum+ this package's own expiry/ability middleware.
- Opt-in routes (
routes/admin.php, never auto-loaded): client CRUD, toggle, IP whitelist CRUD, ability listing. The package can't know your admin-auth stack (session, SSO, Breeze, Jetstream, ...), so auto-registering these would be a security foot-gun. Require the file yourself inside your own auth-protected route group:// routes/web.php Route::middleware(['auth', 'can:client.view'])->group(function () { require base_path('vendor/itmm/api-client-guard/routes/admin.php'); });
- Middleware aliases, registered automatically under a configurable prefix (default
client-guard):client-guard.logger,client-guard.ip-whitelist,client-guard.asr-verify,client-guard.response,client-guard.token-expired,client-guard.token-access. Apply them to your own API routes, e.g.:Route::middleware(['auth:sanctum', 'client-guard.token-expired', 'client-guard.token-access']) ->prefix('api/v1') ->group(function () { Route::get('widgets', [WidgetController::class, 'index'])->name('v1.widgets.index'); });
Token abilities are matched against the current route's name, so give every protected route a name and include that name in a client'sabilitiesarray.
Customizing models
Every model this package uses — Client, ClientIpWhitelist, PersonalAccessToken, ApiActivityLog — resolves through config('api-client-guard.models.*'), the same pattern spatie/laravel-permission uses for its Role/Permission models. Nothing is hardcoded, so you can add your own columns, relationships, or overridden methods without touching this package's source:
php artisan vendor:publish --tag=api-client-guard-models
This copies four thin, empty subclasses into app/Models/ (e.g. App\Models\Client extends Itmm\ApiClientGuard\Models\Client {}) — real, editable files you own from that point on, not something loaded from vendor/. Add whatever you need to them, then point the config at your classes:
// config/api-client-guard.php 'models' => [ 'client' => \App\Models\Client::class, 'client_ip_whitelist' => \App\Models\ClientIpWhitelist::class, 'personal_access_token' => \App\Models\PersonalAccessToken::class, 'api_activity_log' => \App\Models\ApiActivityLog::class, ],
Every repository, service, observer, and the Sanctum token model wiring picks up the swap automatically — nothing else to change. The one place this doesn't reach is routes/admin.php's implicit route-model-binding (Route::get('clients/{client}', ...)), which still resolves against this package's own base classes; override that controller yourself if you need swapped-model behavior there too.
Extension points
- Scoped tokens: replace the default
POST auth/tokenroute with your own controller. Resolve/authorize whatever model the client should be scoped to, then callAuthService::generateToken($data, $model). - Cross-module cache invalidation: dispatch
Itmm\ApiClientGuard\Events\ClientUpdated::dispatch($clientId, $clientStringId)whenever something outside this package should invalidate a client's cached payload (e.g. you renamed/deleted the "useable" model a client depends on). - Token TTL tiers: edit
token_ttl_optionsin the published config — validation usesRule::in(config('api-client-guard.token_ttl_options')), not a hardcoded enum.
ASR (Asymmetric Request Signing)
When a client has asr_enabled with an uploaded public key, requests to ASR-guarded routes must include:
X-Signature: base64-encoded signatureX-Timestamp: unix timestamp (must be withinasr.timestamp_toleranceseconds, default 300)X-Nonce: random string, at least 16 alphanumeric characters, single-use
The signed payload is:
{METHOD}:{path, no leading/trailing slash}:{sha256 hex of the canonical JSON body}:{timestamp}:{nonce}
The canonical JSON body is the request body decoded, recursively key-sorted (ksort, lists left in order), and re-encoded with JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE. An empty body canonicalizes to {}. Supported algorithms: RS256/RS384/RS512 (RSA), ES256/ES384/ES512 (EC) — auto-detected from the uploaded public key.
Config reference (config/api-client-guard.php)
| Key | Default | Purpose |
|---|---|---|
register_routes |
true |
Auto-load routes/api.php (auth/token + tokens/current) |
route_prefix |
api/client-guard |
Prefix for the auto-loaded routes |
middleware_prefix |
client-guard |
Alias prefix for the six registered middleware |
cache.client.key / .ttl |
:client-guard-client / 86400s |
Client cache payload |
cache.token.key / .ttl |
:client-guard-token / 86400s |
Sanctum token cache |
asr.timestamp_tolerance |
300s | Max clock drift for X-Timestamp |
asr.nonce_cache_buffer |
60s | Extra seconds a used nonce stays cached beyond the tolerance window |
activity_log.redacted_keys |
auth/secret/token/... | Keys redacted from logged headers/body |
activity_log.preview_limit / .preview_truncate |
10000 / 1000 | JSON payload truncation for api_activity_logs |
token_ttl_options |
1 day/week/month/year (seconds) | Allowed token_ttl values on client create/update |
Testing
composer install vendor/bin/phpunit
Tests use Orchestra Testbench against an in-memory SQLite database and cover the riskiest pieces of logic: token issuance, ASR signature verification (valid/tampered/expired/replayed), IP whitelisting, token-ability enforcement, and the config-driven model swap described above (container resolution, repository/observer/token-issuance behavior all going through a published subclass).