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.

Maintainers

Package info

github.com/dwisupartama/api-client-guard

pkg:composer/itmm/api-client-guard

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-08-25 10:03 UTC

This package is auto-updated.

Last update: 2026-08-26 01:57:08 UTC


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.

  1. composer require itmm/api-client-guard (once published — for now, path-repository it or composer require itmm/api-client-guard:@dev with a local path repo).
  2. Install Laravel's own Sanctum support first, if you haven't already:
    php artisan install:api
    This creates personal_access_tokens. This package's own migration only adds the useable morph columns to that table — it does not create it.
  3. Publish and run migrations:
    php artisan vendor:publish --tag=api-client-guard-migrations
    php artisan migrate
    This copies four migration files into your own database/migrations/ (clients, client_ip_whitelists, api_activity_logs, and the useable columns added to personal_access_tokens) — edit them before migrating if you need to. Nothing migrates automatically just from composer require.
  4. (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 by auth: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's abilities array.

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/token route with your own controller. Resolve/authorize whatever model the client should be scoped to, then call AuthService::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_options in the published config — validation uses Rule::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 signature
  • X-Timestamp: unix timestamp (must be within asr.timestamp_tolerance seconds, 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).