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 Laravel Package

jenssegers/oauth

OAuth 1 and OAuth 2 authentication for Laravel. Provides clean provider and consumer abstractions with ready-made drivers for Twitter, Facebook, Google, LinkedIn, GitHub and more, making social logins quick to wire into your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (Laravel 4/5 era).

composer require jenssegers/oauth

Register the service provider and facade manually in config/app.php — package auto-discovery will not pick this one up on modern Laravel, and on 5.5+ you may need to add the extra.laravel block yourself or just register explicitly:

'providers' => [
    Jenssegers\OAuth\OAuthServiceProvider::class,
],

'aliases' => [
    'OAuth' => Jenssegers\OAuth\Facades\OAuth::class,
],

Publish the config and put credentials in .env, not in the file:

php artisan vendor:publish --provider="Jenssegers\OAuth\OAuthServiceProvider"
// config/oauth.php — shape follows PHPoAuthLib's credentials array
return [
    'github' => [
        'client_id'     => env('GITHUB_CLIENT_ID'),
        'client_secret' => env('GITHUB_CLIENT_SECRET'),
        'redirect_uri'  => env('GITHUB_REDIRECT_URI'),
        'scope'         => ['user', 'repo'],
    ],
    'twitter' => [ // OAuth 1.0a uses different keys
        'identifier'    => env('TWITTER_CONSUMER_KEY'),
        'secret'        => env('TWITTER_CONSUMER_SECRET'),
        'callback_uri'  => env('TWITTER_CALLBACK_URI'),
    ],
];

Where to look first. vendor/jenssegers/oauth/src/ is a few hundred lines — read OAuthManager/the factory and the facade. Then read the provider class you actually care about inside vendor/lusitanian/oauth/src/OAuth/Service/. That service class is the real API: it tells you which credential keys are honored and what request() returns. Everything else is convention.

First use case — the redirect/callback pair. This is the whole reason the package exists:

// routes
Route::get('auth/{provider}', 'Auth\SocialController@redirect');
Route::get('auth/{provider}/callback', 'Auth\SocialController@callback');
public function redirect($provider)
{
    $service = OAuth::consumer($provider);
    return redirect((string) $service->getAuthorizationUri());
}

public function callback($provider, Request $request)
{
    $service = OAuth::consumer($provider);
    $service->requestAccessToken($request->get('code'));

    $profile = json_decode($service->request('user'), true);

    // $service->getAccessToken(), ->getRefreshToken(), ->getTokenExpires()
}

That is 20 lines to a working GitHub login. Treat it as a spike, not production code, until you've read the Gotchas section.


Implementation Patterns

1. Wrap the facade behind one internal adapter. Never let OAuth:: leak past a single class. The consumed surface is tiny, so an interface costs you an hour and buys you a one-file swap to Socialite later.

interface OAuthAdapter
{
    public function authorizationUrl(string $provider, string $state): string;
    public function exchangeCode(string $provider, string $code): TokenSet;
    public function get(string $provider, string $url, array $query = []): array;
}

class JenssegersAdapter implements OAuthAdapter
{
    public function authorizationUrl(string $provider, string $state): string
    {
        return (string) OAuth::consumer($provider)
            ->getAuthorizationUri(['state' => $state]);
    }

    public function exchangeCode(string $provider, string $code): TokenSet
    {
        $service = OAuth::consumer($provider);
        try {
            $service->requestAccessToken($code);
        } catch (\Lusitanian\OAuth\Exception\OAuthException $e) {
            throw new OAuthFailed('token exchange failed', 0, $e);
        }

        return new TokenSet(
            $service->getAccessToken(),
            $service->getRefreshToken(),
            $service->getTokenExpires()
        );
    }

    public function get(string $provider, string $url, array $query = []): array
    {
        $service = OAuth::consumer($provider);
        return json_decode($service->request($url, 'GET', $query), true) ?? [];
    }
}

2. Persist tokens properly. One table, provider-agnostic, encrypted cast:

Schema::create('oauth_accounts', function (Blueprint $t) {
    $t->increments('id');
    $t->unsignedInteger('user_id');
    $t->string('provider');
    $t->string('provider_user_id');
    $t->text('access_token');
    $t->text('refresh_token')->nullable();
    $t->timestamp('expires_at')->nullable();
    $t->timestamps();
    $t->unique(['provider', 'provider_user_id']);
});
protected $casts = [
    'access_token'  => 'encrypted',
    'refresh_token' => 'encrypted',
    'expires_at'    => 'datetime',
];

Store the identity (provider + provider_user_id) separately from the token so you can still resolve a user when the token has rotted.

3. Refresh on demand, not on a schedule. Tokens die lazily; check expires_at immediately before the API call and refresh inside the same try/catch:

if ($account->expires_at && $account->expires_at->isPast()) {
    $service = OAuth::consumer($account->provider);
    $service->refreshAccessToken($account->refresh_token);
    $account->update(['access_token' => $service->getAccessToken(), ...]);
}

Not every provider refreshes (Twitter's OAuth 1 flow won't), and not every provider gives you a refresh_token — make refresh a capability flag on your account record rather than assuming.

4. Account linking beats "create account from nothing." The flow that actually fits this package's shape: a logged-in user hits /connect/github, you run the same redirect/callback, but in the callback you attach the returned identity to auth()->user() instead of logging in — and use the resulting token for imports, repo listing, profile enrichment. This is where the package is genuinely pleasant and genuinely useful.

5. OAuth 1.0a needs a stored request token. The flow differs and the request-token secret must survive the redirect:

// redirect
$token = $service->requestToken();
session(['oauth1_request_token' => $token->getRequestToken()]);
session(['oauth1_request_token_secret' => $token->getRequestTokenSecret()]);
return redirect((string) $service->getAuthorizationUri([
    'oauth_token' => $token->getRequestToken(),
]));

// callback
$service->requestAccessToken(
    $request->get('oauth_token'),
    $request->get('oauth_verifier'),
    session('oauth1_request_token_secret')
);

Keep that secret in the session (or a short-lived DB row) — losing it means the verifier is useless.

6. Test by faking the facade, not the network.

OAuth::shouldReceive('consumer')->once()->andReturn($fakeService);

Because the whole integration is one call to OAuth::consumer(), this is a clean seam. Assert on the redirect URL and the persistence of tokens; stub request() for the profile fetch.


Gotchas and Tips

getAuthorizationUri() returns an object, not a string. Cast it — return redirect((string) $service->getAuthorizationUri()). Passing the object straight to redirect() fails silently or produces a __toString surprise depending on version.

request() returns the raw response body as a string. No JSON parsing, no status-aware wrapper. Always json_decode(..., true) and always check for null. Some providers return XML or plain text for certain endpoints; the service doesn't tell you which.

Exceptions are your only error channel. Lusitanian\OAuth\Exception\OAuthException, TokenResponseException, and HTTP-level exceptions from the underlying client all surface as throws. Wrap every call. Also handle the consent-denied callback explicitly — the provider redirects to your callback with ?error=access_denied and no code; calling requestAccessToken(null) there produces a confusing upstream exception instead of a clean user message.

State parameter is not handled for you. The package does not generate, store, or validate OAuth2 state. You must pass it via getAuthorizationUri(['state' => $state]) and compare it in the callback against a session value. This is CSRF protection on your login path — do not skip it because the README doesn't mention it.

Config keys differ between OAuth1 and OAuth2. OAuth2 services in PHPoAuthLib expect client_id / client_secret / redirect_uri; OAuth1 services expect identifier / secret / callback_uri. Mixing them produces a provider class that constructs but fails at token exchange with an unhelpful message. If a credential seems ignored, read the concrete service class constructor.

Redirect URI must match byte-for-byte. http vs https, trailing slash, localhost vs 127.0.0.1, and port all matter and all produce different, confusing provider errors. Keep it in .env and never hand-edit it per environment without updating the provider console.

Provider name resolution is a class-name lookup. OAuth::consumer('GitHub') maps to OAuth\Service\GitHub. A typo or an unsupported provider throws a "service not supported" style exception rather than a null — good, but the message names the map, not your config key. Case sensitivity varies; be consistent with whatever the README/class map uses.

config:cache will freeze your credentials. If you're on a version where env() works inside config/oauth.php, remember that after php artisan config:cache the .env is not re-read. Standard Laravel behavior, but it bites precisely on a forgotten callback URL in staging.

Don't hold the service object across requests or queue jobs. It's a per-request factory artifact with tokens in memory. Persist the token strings, reconstruct the service, then call. Trying to serialize or reuse it across workers leads to weird stale-token behavior.

No PKCE, no OpenID Connect, no revocation helper. If a provider now requires PKCE (increasingly common for mobile/native), this package can't do it — you'd be passing code_challenge by hand and still be stuck on the verifier exchange. Accept that as a hard scope boundary rather than fighting it.

Upstream is unmaintained — pin and fork. No jenssegers/oauth releases, and lusitanian/oauth is likewise abandoned. Day-to-day implications: pin exact versions in composer.json, commit composer.lock, be prepared to vendor or fork the provider class when an endpoint moves, and add a security CI job that watches transitive advisories. Treat "a provider broke" as a ticket you own, because nobody upstream will ship the fix.

Verify installability before you write feature code. Run a throwaway composer require against your exact PHP/Laravel pair in a scratch branch. Composer constraints on this package predate modern PHP; you may need --ignore-platform-reqs or a replace block, and you want to discover that in five minutes, not after the controller is written.

Extension point: the PHPoAuthLib service class, not the Laravel package. If you need to patch a provider (changed endpoint, new header), the right fork target is the service class in lusitanian/oauth, which the Laravel wrapper barely touches. Keep the patch as a thin subclass registered via your own OAuth::extend-style binding if your version exposes one; otherwise a composer.json repositories VCS entry with a pinned branch is the least surprising approach.

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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource