un-dev-suisse / services-identities-client
Verifies the service tokens issued by Services Identities and obtains them for outgoing calls, in Laravel services.
Package info
gitlab.com/un_dev_suisse/packages/services-identities-client
pkg:composer/un-dev-suisse/services-identities-client
Requires
- php: ^8.3
- ext-sodium: *
- illuminate/contracts: ^13.0
- illuminate/http: ^13.0
- illuminate/support: ^13.0
- lcobucci/jwt: ^5.6
Requires (Dev)
- larastan/larastan: ^3.9
- laravel/pint: ^1.27
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-25 00:07:06 UTC
README
Laravel package for the services of the platform. It does two things with the tokens issued by Services Identities: it verifies the token of an incoming call, and it obtains (and caches) the token for an outgoing call.
Requires PHP 8.3 with ext-sodium, Laravel 13.
Install
The package is read straight from its public GitLab repository, with a vcs repository: no package
registry and no credentials are needed. Declare it once in the composer.json of the service, then require
a version (the git tags):
composer config repositories.services-identities-client vcs https://gitlab.com/un_dev_suisse/services-identities-client.git
composer require un-dev-suisse/services-identities-client:^0.1
composer update then fetches new tags from GitLab, from a developer machine as well as while an image is
built. The repository holds no secret, only the verification logic.
The service provider is discovered. Publish the configuration only if you need to change it
(php artisan vendor:publish --tag=services-identities-config).
Configure
| Variable | Role |
|---|---|
SERVICES_IDENTITIES_URL | Base URL of Services Identities on the internal network, for example http://services-identities_app |
SERVICES_IDENTITIES_SERVICE | Identifier of this service. Incoming tokens must be issued for it |
SERVICES_IDENTITIES_ISSUER | Expected issuer: IDENTITIES_TOKEN_ISSUER of Services Identities |
SERVICES_IDENTITIES_CLIENT_ID, SERVICES_IDENTITIES_CLIENT_SECRET | Credentials of this service, to obtain tokens. Only needed to call other services |
SERVICES_IDENTITIES_JWKS_URL | Where the keys are published. Default: <url>/api/jwks.json |
SERVICES_IDENTITIES_JWKS_CACHE_TTL | Seconds the key set stays cached (300) |
SERVICES_IDENTITIES_JWKS_REFETCH_COOLDOWN | Minimum seconds between two refreshes triggered by an unknown key (30) |
SERVICES_IDENTITIES_TOKEN_RENEW_MARGIN | A token is renewed this many seconds before it expires (30) |
SERVICES_IDENTITIES_LEEWAY | Tolerated clock difference in seconds (5) |
SERVICES_IDENTITIES_HTTP_TIMEOUT | Seconds before a request to Services Identities is abandoned (5) |
A required setting left empty raises MissingConfiguration instead of falling back to a default.
Protect routes
Route::middleware('service.token')->get('/status', ...); // any service allowed to call this one
Route::middleware('service.token:mail:send')->post('/mails', ...); // needs the mail:send scope
Route::middleware('service.token:mail:send,mail:read')->get('/mails', ...); // needs both
The middleware checks the signature (EdDSA only, with the key published under the token's kid), the
issuer, that the audience is this service, and the validity period. It answers 401 invalid_token with a
WWW-Authenticate header, 403 insufficient_scope when a scope is missing, and 503
temporarily_unavailable when the keys can be neither read from the cache nor fetched. The reason of a
refusal goes to the logs, never to the caller. In the route, the verified token is the service_token
request attribute:
$token = $request->attributes->get('service_token');
$token->caller; // 'budgets'
$token->scopes; // ['mail:send']
$token->hasScope('mail:send');
Call another service
use UnDevSuisse\ServicesIdentities\ServiceClient;
$response = app(ServiceClient::class)
->http('mail', ['mail:send']) // the target service and the scopes to ask for
->post('http://mail_app/api/mails', $payload);
http() returns an HTTP client request already carrying Authorization: Bearer <token>. The scopes are
optional: without them the token carries every scope granted to this service on the target. The token is
cached, per target and scopes, until SERVICES_IDENTITIES_TOKEN_RENEW_MARGIN seconds before it expires,
then renewed. A refusal raises TokenRequestFailed, whose status and error hold the HTTP status and
the OAuth code (invalid_client, access_denied, invalid_scope).
Keys
The public keys come from the JWKS of Services Identities and are cached for
SERVICES_IDENTITIES_JWKS_CACHE_TTL seconds. A token with a kid that is not in the cached set refreshes
it, at most once per cooldown: a key rotation needs no action in the services.
Development
composer install
composer test # PHPUnit with Orchestra Testbench
composer lint # Pint
composer analyse # PHPStan (Larastan)
Publish a version
Tag the commit and push the tag: services pick it up on their next composer update.
git tag v0.1.0 && git push origin v0.1.0