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

Oauth Subscriber Laravel Package

guzzlehttp/oauth-subscriber

OAuth 1.0 middleware for Guzzle that signs outgoing requests with your consumer key, secret, token, and token secret when the auth option is oauth. Add it to a Guzzle handler stack for APIs that require OAuth 1.0 request signing instead of OAuth 2.0 bearer tokens.

View on GitHub
Deep Wiki
Context7

Getting Started

guzzlehttp/oauth-subscriber is not a Laravel package — it has no service provider, no config file, no facade. It is a single Guzzle middleware (GuzzleHttp\Subscriber\Oauth\Oauth1) that you push onto a HandlerStack, and it only signs a request when that request carries 'auth' => 'oauth'. Everything else (credential storage, token acquisition, retries) stays in your app.

composer require guzzlehttp/oauth-subscriber

Pick your major version against your Guzzle major first. Laravel's HTTP client ships Guzzle 7.x, so Composer will resolve ^0.9 (currently 0.9.6). The 1.x line requires guzzlehttp/guzzle ^8.1 and is only appropriate if you've moved Laravel off its bundled Guzzle. Check with composer why guzzlehttp/guzzle before assuming.

The 60-second smoke test — a standalone Guzzle client, which is also the fastest way to prove a partner API accepts your credentials before wiring anything into Laravel:

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Subscriber\Oauth\Oauth1;

$stack = HandlerStack::create();
$stack->push(new Oauth1([
    'consumer_key'    => config('services.partner.key'),
    'consumer_secret' => config('services.partner.secret'),
    'token'           => config('services.partner.token'),
    'token_secret'    => config('services.partner.token_secret'),
]));

$client = new Client([
    'base_uri' => 'https://api.partner.test/',
    'handler'  => $stack,
    'auth'     => 'oauth', // client-wide default
]);

$response = $client->get('resource');

Where to look first: docs/oauth-1-0-middleware-usage.md in the package for signature methods and the exact config-array keys, and CHANGELOG.md for the two things that will bite you — nonce entropy (0.8.1, CVE-2025-21617) and per-request token/token_secret overrides (0.9.0). Pin ^0.9.6 unless you have a reason not to.

First real use case: a legacy or partner API that still requires OAuth 1.0 — a shipping provider, an old payment gateway, a data-sync feed. If the API speaks OAuth 2.0, stop here and send a bearer token instead.

Implementation Patterns

Wrap the stack in a dedicated integration class

Don't build HandlerStacks inline in controllers. One class per external service, bound as a singleton, with the credentials read from config at construction time:

namespace App\Http\Integrations;

use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Subscriber\Oauth\Oauth1;

class PartnerStack
{
    public static function make(array $creds): HandlerStack
    {
        $stack = HandlerStack::create();

        // Signing goes in first (executes last / innermost) ...
        $stack->push(new Oauth1([
            'consumer_key'    => $creds['consumer_key'],
            'consumer_secret' => $creds['consumer_secret'],
            'token'           => $creds['token'] ?? null,
            'token_secret'    => $creds['token_secret'] ?? null,
            'signature_method'=> 'HMAC-SHA1',
        ]), 'oauth1');

        // ... retry goes in last (executes first / outermost), so every
        // retry re-enters the signing middleware and gets a fresh nonce.
        $stack->push(Middleware::retry(
            fn ($retries, $req, $res = null, $ex = null) => $retries < 3
                && ($ex || ($res && $res->getStatusCode() >= 500)),
            fn ($retries) => 250 * (2 ** $retries),
        ), 'retry');

        return $stack;
    }
}

Stack order is the single most important design decision here. push() puts middleware at the top of the stack, so the last thing pushed runs first. Retry must be outside OAuth, otherwise a retried request reuses the original nonce and timestamp and gets rejected as a replay.

Drive it through Laravel's HTTP client

Laravel's Http facade accepts Guzzle options, including handler:

$response = Http::withOptions([
    'handler'  => PartnerStack::make(config('services.partner')),
    'base_uri' => config('services.partner.base_uri'),
    'auth'     => 'oauth',
    'timeout'  => 15,
])->asForm()->post('orders', [
    'reference' => $order->reference,
    'amount'    => (string) $order->total_cents,
]);

$response->throw()->json();

asForm() matters — see the gotchas on JSON bodies. If you use the Http client heavily, consider a small macro or a typed PartnerClient service that pre-applies the handler/auth options so callers never forget them.

Per-request credentials for multi-tenant integrations

Since 0.9.0 you can share one stack (consumer key/secret) and swap tokens per request — the right shape for a Laravel app where each tenant or connected account has its own token:

Http::withOptions([
    'handler'       => $stack,          // consumer credentials only
    'auth'          => 'oauth',
    'token'         => $connection->oauth_token,
    'token_secret'  => $connection->oauth_token_secret,
])->get('account/profile');

Store token secrets encrypted (encrypted cast) or in a secrets manager; they are credentials, not settings.

Credential resolution as middleware

The package has no "token refresh" hook. Implement it as a tiny middleware pushed after Oauth1 (so it runs before signing) that mutates the options array with a freshly loaded token:

$stack->push(function (callable $handler) use ($resolve) {
    return function ($request, array $options) use ($handler, $resolve) {
        [$options['token'], $options['token_secret']] = $resolve($request);
        return $handler($request, $options);
    };
}, 'credentials');

That's your extension point for 401-retry-with-refreshed-token, queue-based rotation, or reading from a per-tenant cache. Leave Oauth1 itself alone.

Two-legged OAuth and non-default signatures

For server-to-server calls with no user context, omit token/token_secret entirely. For providers requiring RSA-SHA1, set 'signature_method' => 'RSA-SHA1' and 'private_key' => ... (PEM contents, not a path). RSA keys are validated before signing in 0.8.2+, so misconfiguration fails loudly rather than producing a bad signature.

Gotchas and Tips

Silent no-op signing. If you forget 'auth' => 'oauth', the middleware does nothing and you get a 401 with no error from the library. Set it as a client default, and add one integration test that asserts the Authorization header is present.

Http::fake() defeats you. Faking replaces the handler, so the OAuth middleware never executes and your signature is never tested. For signing tests use a real HandlerStack with MockHandler + Middleware::history() and assert on $history[0]['request']->getHeaderLine('Authorization'). You can't golden-file a signature (nonce and timestamp are random) — assert structure, or recompute the base string in the test.

JSON bodies are not in the signature base string. OAuth 1.0 signs query parameters and application/x-www-form-urlencoded bodies. Send ->asForm()/form_params when the provider verifies the body; a json payload will fail body-hash or signature validation on strict servers.

Pin at least 0.9.6. The 0.8.1 CVE fix (nonce entropy), the 0.9.0 duplicate-parameter normalization and bare-parameter signing fixes, and 0.9.3's parameterized/case-insensitive Content-Type form signing fix are all things you do not want to debug yourself.

Don't serialize the middleware. 1.0 explicitly rejects native PHP serialization of Oauth1, and holding a client in a queued job's payload has never been a good idea. Build the stack inside handle().

Keep the option array out of logs. Guzzle's 'debug' => true, exception contexts, and Telescope's HTTP client watcher can all capture request headers and options containing consumer_secret/token_secret. Add the oauth, token, token_secret keys to Telescope's hideRequestParameters/hideRequestHeaders, never Log::debug($options), and lean on Laravel's #[\SensitiveParameter] for any of your own wrappers that take credentials.

Parameter normalization is where 99% of 401s come from. Booleans, null, floats, and nested arrays are all coerced or dropped differently by the package than by your provider. Normalize everything to strings ('true' vs 1, amounts as "12.00") before passing them in. Duplicate query parameters and pre-existing oauth_signature values in the query are handled in recent versions — but only in recent versions.

URLs must match exactly. The signature base string covers the full request URI. Trailing slashes, default ports, and any proxy that rewrites the path (load balancers, base_uri with/without a trailing slash) will invalidate signatures. Sign against the URL the provider will actually see.

Redirects re-sign. Because the middleware sits in the handler stack, a followed redirect is signed again — potentially for a different host. Pass 'allow_redirects' => ['strict' => true] or false for auth'd calls.

Clock skew. Timestamps are generated at signing time. A container with drifting time will produce 401 timestamp refused errors that look like credential problems. NTP on every worker.

Not a full OAuth client. No request-token/access-token exchange, no refresh, no callback handling, no revocation. Budget for that work explicitly; if the provider's flow is complex, an official SDK may still win on total cost.

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.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core