Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Guzzle Oauth2 Subscriber Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. A reauth client that talks to the token endpoint (usually different base_uri).
  2. Your API client that has the OAuth2Middleware pushed onto its handler stack.
composer require kamermans/guzzle-oauth2-subscriber:^1.1.1

Pin ^1.1.1 or 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.

Implementation Patterns

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.

Gotchas and Tips

  • Never pass grant_type yourself. ClientCredentials, Password, etc. inject it. Duplicate will 400 on most IdPs.
  • The reauth client must use 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.
  • Only one retry on 401. The middleware does not loop. If the refreshed token is also rejected you get ReauthorizationException — catch it in Laravel’s Handler and invalidate the cached token ($persistence->deleteToken()) before showing the user anything.
  • Exception hierarchy matters. 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.
  • Scope format. Both ['a','b'] and 'a b' work — don’t pass a comma-separated string, the package joins with a space.
  • State/CSRF is your job. AuthorizationCode::getAuthorizationUrl() includes state only if you put state in the config array; validate it on callback yourself.
  • Guzzle 7 handler stack. Use 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.
  • Custom token payloads. Subclass 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.
  • Clock skew. 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().
  • Log the token-endpoint request/response during debugging with Guzzle’s Middleware::history on the reauth client — the API client side is easy, the token side is where the bugs hide.
  • PHP 8.4: use v1.1.1+, and write explicit nullable types in your own extensions. v1.1.1 (PR #62) fixes implicit-nullable parameter deprecations in the package itself. The same deprecation applies to your subclasses of 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.
  • Pin the minor and smoke-test on PHP upgrades. The 1.x line only just reached 8.4 compatibility, so keep 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.
  • Pushing multiple middlewares: add 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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk