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.
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.
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.
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.
How can I help you explore Laravel packages today?