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

Tmhoauth Laravel Package

themattharris/tmhoauth

tmhOAuth is a lightweight PHP library for OAuth 1.0A authentication, commonly used to connect to the Twitter API. It simplifies signed requests, supports GET and POST calls, and provides a straightforward cURL-based interface for building Twitter integrations.

Deep Wiki
Context7

Getting Started

composer require themattharris/tmhoauth

The package is a single-class OAuth 1.0A client with cURL transport. It ships two global classes — TMHOAuth (the client) and TMHUtilities (static helpers) — with no service provider, config file, facade, or contract. In a Laravel app you wire it yourself.

First thing to read is tmhOAuth.php. The constructor is the entire API surface you'll touch 90% of the time:

new TMHOAuth(array $config)   // or the legacy positional form

Register a binding once in AppServiceProvider:

use TMHOAuth;

$this->app->singleton('tmh.oauth.app', fn () => new TMHOAuth([
    'consumer_key'        => config('services.twitter.key'),
    'consumer_secret'     => config('services.twitter.secret'),
    'curl_ssl_verifypeer' => true,
    'curl_ssl_verifyhost' => 2,
]));

First use case: the three-legged handshake. Request the temporary token, redirect, then swap the verifier for an access token.

Route::get('/connect/twitter', function () {
    $tmh = app('tmh.oauth.app');
    $tmh->request('POST', $tmh->url('oauth/request_token', ''));

    $token = $tmh->extract_params($tmh->response['response']);

    session([
        'oauth_token'  => $token['oauth_token'],
        'oauth_secret' => $token['oauth_token_secret'],
    ]);

    return redirect($tmh->url('oauth/authorize', [
        'oauth_token' => $token['oauth_token'],
    ]));
});

Route::get('/connect/twitter/callback', function () {
    $tmh = new TMHOAuth([
        'consumer_key'    => config('services.twitter.key'),
        'consumer_secret' => config('services.twitter.secret'),
        'user_token'      => session('oauth_token'),
        'user_secret'     => session('oauth_secret'),
    ]);

    $tmh->request('POST', $tmh->url('oauth/access_token', ''),
        ['oauth_verifier' => request('oauth_verifier')]);

    $creds = $tmh->extract_params($tmh->response['response']);
    // persist $creds['oauth_token'] and $creds['oauth_token_secret']
});

After that, every signed call is the same shape: $tmh->request('GET', $tmh->url('statuses/home_timeline', ['count' => 20])).

Implementation Patterns

Per-user client, not one shared instance. Tokens are part of the constructor config, so build a fresh client for each user rather than mutating a singleton:

function twitterFor(User $user): TMHOAuth
{
    return new TMHOAuth([
        'consumer_key'    => config('services.twitter.key'),
        'consumer_secret' => config('services.twitter.secret'),
        'user_token'      => $user->twitter_token,
        'user_secret'     => $user->twitter_secret,
    ]);
}

Wrap it in a small gateway. The library returns no DTO and throws nothing, so isolate the response['code'] handling in one place:

class TwitterGateway
{
    public function __construct(private TMHOAuth $tmh) {}

    public function get(string $path, array $query = []): array
    {
        $this->tmh->request('GET', $this->tmh->url($path, $query));

        if ($this->tmh->response['code'] >= 400) {
            throw new TwitterException(
                $this->tmh->response['response'],
                $this->tmh->response['code']
            );
        }

        return json_decode($this->tmh->response['response'], true) ?? [];
    }

    public function post(string $path, array $body = []): array { /* ... */ }
}

Queue jobs hold the credentials. Because signing is local and synchronous, this plays nicely inside jobs — load the user, build the client, post, log the HTTP code. Don't store the TMHOAuth instance in a queue payload (it holds secrets and a live cURL handle); store the user and rebuild.

One class, many providers. Override host and use_ssl to point the same client at Tumblr, Flickr, or the old LinkedIn API. Endpoint helpers ($tmh->url()) use that host value, so a generic OAuth1 gateway is viable:

new TMHOAuth(['host' => 'api.flickr.com', 'use_ssl' => true, /* ... */]);

Multipart/upload. Pass multipart: true as the fifth argument to request() for @file style bodies (media upload endpoints); OAuth 1 signing rules change there and the library handles it — do not try to build the payload by hand.

Testing seam. Bind a fake gateway in a test service provider so feature tests never touch Twitter. If you want to test the signing itself, inject a fixed oauth_timestamp/oauth_nonce via config and assert the Authorization header your code produced.

Migration path. Every new integration should sit behind your gateway interface, because league/oauth1-client is the maintained replacement. Keeping the surface to get/post/exchangeToken means swapping later is a one-file change.

Gotchas and Tips

  • It is unmaintained. Pin the exact version in composer.json and treat it as frozen. Budget time to move to league/oauth1-client.
  • No namespaces. If TMHOAuth isn't autoloadable (older tags shipped an inconsistent autoload map), add a classmap entry in your own composer.json for vendor/themattharris/tmhoauth/tmhOAuth.php and dump-autoload.
  • SSL verification is off by default. Always set curl_ssl_verifypeer => true and curl_ssl_verifyhost => 2 explicitly. This is the single biggest production risk with this package.
  • No exceptions. Failures surface as $tmh->response['code'] (HTTP) and $tmh->response['errno'] + ['error'] (cURL). Checking only one of them means transport failures look like empty 200s.
  • A missing user_secret produces a 401, not an error. The signature is computed with an empty key and silently rejected. Validate that both halves of the token pair exist before calling.
  • Callback URL must match byte-for-byte. Persist the exact callback you sent with the request token; generating it from url()->current() inside a subdomain or with a trailing slash appended later is a classic source of 401 / oauth_problem=signature_invalid.
  • PIN/OOB flow: pass 'oob' as the callback to oauth/request_token and collect the verifier from a form instead of redirecting.
  • Force the login prompt with $tmh->getAuthorizeURL($token, true) (adds force_login), useful when a user needs to switch accounts on a shared browser.
  • Clock skew kills requests. oauth_timestamp is rejected if the host's clock drifts by more than a few minutes — usually seen first in containers with broken NTP. Check date inside the container.
  • Rate limits are in headers, not the body. Inspect $tmh->response['headers']['x-rate-limit-remaining'] and back off on 429. The library does not retry.
  • Debug with $tmh->getLastApiURL(), which gives the normalized URL including query — safe to log. Never log Authorization headers or user_secret; the signature embeds both consumer and token secrets.
  • Streaming mode (setStreaming(true)) is a long-lived blocking cURL loop. It's unusable in a normal FPM request — put it behind an artisan command or dedicated worker.
  • Reusing an instance across different users in a loop is a footgun; the config carries the token, and one forgotten reset leaks one user's credentials into another's request. Always construct per user.
  • response['response'] vs response['body'] — different versions of the library populate them differently; check both if you're migrating between tags, or normalize inside your gateway.
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.
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route