kamermans/guzzle-oauth2-subscriber
Guzzle OAuth2 Subscriber adds OAuth 2.0 authorization to Guzzle HTTP clients in PHP. It provides middleware for authorization code, client credentials, password, and refresh token grants, automatically attaching and refreshing access tokens on outgoing requests.
This package is a Guzzle middleware that handles the OAuth 2.0 dance for you: it fetches a token via a grant type, injects Authorization: Bearer … into outgoing requests, and transparently refreshes/retries on 401. The mental model is two Guzzle clients:
base_uri).OAuth2Middleware pushed onto its handler stack.composer require kamermans/guzzle-oauth2-subscriber:^1.1.1
Pin
^1.1.1or later if you run PHP 8.4 — v1.1.1 fixes nullable-parameter deprecations that otherwise flood logs on 8.4+.
Minimal Client Credentials example:
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use kamermans\OAuth2\GrantType\ClientCredentials;
use kamermans\OAuth2\OAuth2Middleware;
// 1. Token-endpoint client
$reauthClient = new Client([
'base_uri' => 'https://auth.example.com/oauth/token',
]);
// 2. Grant type – only the credentials live here, NOT grant_type
$grant = new ClientCredentials($reauthClient, [
'client_id' => env('OAUTH_CLIENT_ID'),
'client_secret' => env('OAUTH_CLIENT_SECRET'),
'scope' => ['read', 'write'], // array or space-separated string
]);
// 3. API client with middleware
$stack = HandlerStack::create();
$stack->push(new OAuth2Middleware($grant));
$api = new Client(['base_uri' => 'https://api.example.com', 'handler' => $stack]);
$api->get('/me'); // token fetched once, reused, refreshed on 401
Where to look first: OAuth2Middleware (the entry point), GrantType\* (which flow applies to you), Persistence\* (where tokens live between requests), and Token\TokenInterface (how tokens are represented). First use case: server-to-server API calls with Client Credentials — that’s ~80% of day-to-day work in a Laravel app.
Wrap it in a Laravel service provider / singleton. Don’t build the client inline in controllers.
// config/services.php
'my_api' => [
'base_uri' => env('API_BASE_URI'),
'token_uri' => env('API_TOKEN_URI'),
'client_id' => env('API_CLIENT_ID'),
'client_secret' => env('API_CLIENT_SECRET'),
'scope' => ['read'],
],
// app/Providers/ApiServiceProvider.php
$this->app->singleton(MyApiClient::class, function ($app) {
$cfg = $app['config']['services.my_api'];
$reauth = new Client(['base_uri' => $cfg['token_uri']]);
$grant = new ClientCredentials($reauth, [
'client_id' => $cfg['client_id'],
'client_secret' => $cfg['client_secret'],
'scope' => $cfg['scope'],
]);
$stack = HandlerStack::create();
$stack->push(new OAuth2Middleware(
$grant,
new FileTokenPersistence(storage_path('app/oauth/my_api.json'))
));
return new Client(['base_uri' => $cfg['base_uri'], 'handler' => $stack]);
});
Persist the token — it’s not optional in production. The only built-in implementation is FileTokenPersistence; for real apps implement TokenPersistenceInterface backed by the cache or DB so multiple workers share one token and you don’t hammer the token endpoint:
final class CacheTokenPersistence implements TokenPersistenceInterface
{
public function __construct(private CacheStore $cache, private string $key) {}
public function getToken(): ?SerializableTokenInterface {
return ($raw = $this->cache->get($this->key)) ? unserialize($raw) : null;
}
public function saveToken(SerializableTokenInterface $token): void {
$this->cache->put($this->key, serialize($token), $token->getExpiresIn() - 60);
}
public function deleteToken(): void { $this->cache->forget($this->key); }
}
Chain grant types to add refresh. The RefreshToken grant wraps your primary grant — when access-token fetch fails, it falls back to the refresh token, and vice versa. Build one middleware with the chained grant, not two:
$grant = new ClientCredentials($reauth, $config);
$grant = new RefreshToken($reauth, $grant); // wraps; shares token config
$middleware = new OAuth2Middleware($grant, $persistence);
The middleware catches 401, calls $grant->getToken() again (which goes through the refresh path), retries the original request exactly once. Combine with ReauthorizationException handling in a global exception handler if you want a 503/re-auth UX.
Authorization Code flow for user-facing OAuth. Generate the authorize URL, redirect, exchange the code, then use the same middleware for subsequent API calls.
$grant = new AuthorizationCode($reauth, [...$config, 'redirect_uri' => route('oauth.callback')]);
$url = $grant->getAuthorizationUrl(); // store $grant and state in session
// ... on callback:
$grant->setCode($request->query('code'));
// then push OAuth2Middleware($grant) on the API client
Custom grant types (JWT bearer, password, Azure-specific) implement GrantTypeInterface::getToken(). Return a Token\RawToken (or a subclass) built from the token endpoint’s JSON response. This is the extension point when the built-in grants don’t match your IdP.
Testing. Push the middleware on an HandlerStack with a MockHandler; assert the Authorization header. For token-endpoint tests, use a second mock for the reauth client so you can verify form_params and headers independently.
grant_type yourself. ClientCredentials, Password, etc. inject it. Duplicate will 400 on most IdPs.form_params, not JSON. The package handles this, but if you inject your own Client/handler with a JSON-body middleware, the token request will silently fail. Isolate the reauth client — don’t reuse your API client for it.NullTokenPersistence is the default and re-authorizes every request. Great for tests, terrible in production. Always pass a persistence instance.ReauthorizationException — catch it in Laravel’s Handler and invalidate the cached token ($persistence->deleteToken()) before showing the user anything.AccessDeniedException means bad credentials (don’t retry); ReauthorizationException means it tried and failed (retry after fixing persistence/creds); OAuth2Exception is the generic base. Catch narrow, not \Exception.['a','b'] and 'a b' work — don’t pass a comma-separated string, the package joins with a space.AuthorizationCode::getAuthorizationUrl() includes state only if you put state in the config array; validate it on callback yourself.HandlerStack::create() and push(); don’t pass middleware (Guzzle 5 style). On Guzzle 4 you’d use a Subscriber/EventSubscriber — check the README version matrix.RawToken (or Token/SerializableToken) if your IdP returns extra fields (e.g. expires_in as a string, refresh_token_expires_in). Override getExpiresIn() to avoid immediate re-auth thrash.RawToken uses expires_in seconds; on a fleet with drifting clocks, subtract a safety window in your persistence’s TTL (60s is common) or override the token’s isExpired().Middleware::history on the reauth client — the API client side is easy, the token side is where the bugs hide.RawToken, your GrantTypeInterface implementations, and your TokenPersistenceInterface — write ?SerializableTokenInterface $token = null / ?string $key = null instead of Type $token = null. On 8.1–8.3 the older versions still work; the fix is only a hard requirement on 8.4+ where implicit nullables emit E_DEPRECATED.composer require kamermans/guzzle-oauth2-subscriber:^1.1.1 and run a grant/refresh integration test in CI whenever you bump PHP — the token/refresh path is where version drift bites.OAuth2Middleware after any logging/retry middleware you want to see the 401→retry transition, but before the handler. Order determines whether your logger sees the retried request.How can I help you explore Laravel packages today?