bmatovu/oauth-negotiator
bmatovu/oauth-negotiator is a Laravel package for negotiating OAuth access tokens. It offers a fluent interface to obtain, refresh, and manage tokens from multiple providers, making third-party API integration simple with minimal configuration.
Install the package and publish its configuration:
composer require bmatovu/oauth-negotiator
php artisan vendor:publish --provider="Bmatovu\OAuthNegotiator\OAuthNegotiatorServiceProvider"
As of v2.1.0 the package supports PHP ^8.0, so it will install cleanly on modern PHP runtimes. Check composer.json for the Laravel/illuminate/* constraint before you commit — PHP 8 is now allowed, but that says nothing about which Laravel majors are permitted.
Because the package is small and the README is the primary source of truth, read these first, in order:
config/oauth-negotiator.php (after publishing) — this is where grant types, token URLs, client IDs/secrets, scopes, and cache settings live.Negotiator/token-manager class — the public methods here are your day-to-day API (requestToken, refreshToken, getToken, or similar).The first real use case is the classic client-credentials flow: your Laravel app needs to call a third-party API (payments, shipping, CRM, internal microservice) that issues short-lived bearer tokens. Instead of hand-rolling Http::post($tokenUrl, [...]) in every service class, you configure the client once and let the package fetch, cache, and refresh the token for you.
Minimal happy path:
// config/oauth-negotiator.php
'clients' => [
'acme' => [
'grant_type' => 'client_credentials',
'token_url' => env('ACME_TOKEN_URL'),
'client_id' => env('ACME_CLIENT_ID'),
'client_secret' => env('ACME_CLIENT_SECRET'),
'scopes' => ['orders.read', 'orders.write'],
],
],
Then resolve the client and pull a token before making your API call. Verify with php artisan tinker that a token comes back and that a second call hits the cache rather than the token endpoint.
Wrap it in a dedicated API client. Don't sprinkle negotiator calls through controllers. Create one class per upstream API that owns the token retrieval and the subsequent Http::withToken(...) call:
class AcmeClient
{
public function __construct(private Negotiator $oauth) {}
public function get(string $path, array $query = [])
{
$token = $this->oauth->client('acme')->getToken();
return Http::withToken($token)->baseUrl(config('services.acme.base_url'))
->get($path, $query)
->throw();
}
}
This gives you one place to swap in retries, logging, or a different auth mechanism later.
Use it in jobs and commands, not just HTTP requests. Token caching makes this cheap — a queued job that fans out 500 calls to the upstream API should fetch the token once and reuse it across the whole batch. Resolve the client once at the top of handle() rather than per-iteration.
Compose with Laravel's HTTP client. The cleanest integration is to build a macro or a PendingRequest factory that already has the token attached. This keeps call sites idiomatic and lets you keep using Http::fake() in tests.
Multiple clients / tenants. If the package supports a named-client registry (it should, given the config shape), register one entry per upstream. For multi-tenant apps where each tenant has its own credentials, keep per-tenant credentials in the database and pass them into the negotiator at runtime rather than relying solely on static config — verify the package allows overriding credentials per call.
Pre-warm tokens in a scheduled command. If cold-start latency matters (e.g., a webhook handler that immediately calls upstream), add a scheduled job that requests each token shortly before expiry so user-facing requests never pay the token round-trip.
Testing. Bind a fake negotiator in your test container that returns a fixed token string, or point the token URL at a Http::fake() route. Assert on your API client's behavior, not on the negotiator's internals.
php: ^8.0), which removes the PHP-version blocker that previously forced --ignore-platform-reqs or a fork on PHP 8 projects. It does not tell you anything about Laravel support: still confirm the illuminate/support (or laravel/framework) constraint against your app's major before adopting. Three stars and zero dependents still means you're effectively the maintainer if something breaks — for a package this small, vendoring the two or three relevant classes into app/Support/ remains a sane hedge.composer update bmatovu/oauth-negotiator on a branch and diff composer.lock to confirm no transitive surprises.expires_in; if the package caches for the full duration, requests racing the expiry will 401. Confirm there's an expiry skew/buffer (typically 30–60s) and add one yourself if not.array store vanish per request; tokens cached in a shared Redis store can collide across staging/production if you use the same Redis. Namespace your cache keys by environment and client name.Cache::lock() around the refresh if the package doesn't already, and treat upstream 429s from the token endpoint as a signal you need it.forget/refresh on the cached token and one retry — no more. Infinite retry loops against a misconfigured client are the classic failure mode.client_secret as env('...'). Watch out for config:cache — if the package reads env() at runtime instead of config time, cached config will silently break credentials. On PHP 8.0+ this fails just as quietly, so verify with php artisan config:cache locally before deploying.invalid_client, invalid_scope, unauthorized_client) are the actual diagnosis. Also verify grant_type casing and that scopes are space-delimited in the request body even if your config uses an array. On PHP 8, TypeError/ErrorException from untyped internals is a new failure mode worth catching around the token call.How can I help you explore Laravel packages today?