Service tokens (m2m)
Machine credentials for one backend calling another (e.g. ClaimLeo →
GlobalSKU “publish item”). This is not end-user identity — no
human, no session, no shadow user (that’s the
SSO bridge). It mirrors the Nest
adapter’s ServiceAuthClient / ServiceOnlyGuard.
Backed by the auth-server’s OAuth2 client_credentials grant: a
system_admin registers an m2m client once
(POST /admin/m2m-clients — the secret is shown once); the
calling service exchanges it for a short-lived service token
(token_type: "service", scopes claim, 15-minute lifetime); the
receiving service validates locally (shared HS256 secret) and
enforces scopes.
Outbound — mint + attach
AUTH_M2M_CLIENT_ID=claimleo-svcAUTH_M2M_CLIENT_SECRET=<from registration>AUTH_M2M_SCOPES="gsku:publish" # optional; empty = full client scope setuse rw3iss\AuthServer\Laravel\Facades\VenService;use Illuminate\Support\Facades\Http;
// Sugar — registered Http macro:Http::withVenServiceToken()->post('https://globalsku.app/api/…', $payload);
// Or explicitly:Http::withToken(VenService::token())->post($url, $payload);
// After a downstream 401 (early revocation), force a re-mint:VenService::fresh();Tokens are cached (vauth:svc_token:{client_id}) until
expires_in − AUTH_M2M_TTL_MARGIN (default 60 s) — a busy app mints
at most once per expiry window.
Inbound — machine-only routes
Route::post('/partner/publish', PublishController::class) ->middleware('vauth.service_only:gsku:publish');- Validates the bearer locally — no network hop.
- Requires
token_type: "service"; a valid user token gets403 SERVICE_REQUIRED. - Every middleware parameter is a required scope (ALL must be present; colons inside scope codes are fine).
- On success the
ServicePrincipalis available:
$svc = $request->attributes->get('ven_service');$svc->id; // client_id$svc->serviceName; // "ClaimLeo backend"$svc->hasScope('gsku:publish');Failure envelopes
| Status | code | When |
|---|---|---|
| 401 | UNAUTHENTICATED | missing / invalid / expired bearer |
| 403 | SERVICE_REQUIRED | a valid user token on a machine-only route |
| 403 | MISSING_SCOPE | service token lacking a required scope (details.required) |
Notes
- Scopes are
resource:actionstrings — same convention as user permissions, but they live on the m2m client, not on roles. - Rotation: register a replacement client, switch env, soft-revoke the old one; outstanding tokens die within their 15-minute lifetime.
- Full guide with registration walkthrough:
docs/SERVICE_TOKENS.md.