lusitanian/oauth
A PHP library for OAuth 1 and OAuth 2 authentication, offering ready-made service wrappers for dozens of providers like Google, Facebook, Twitter, and GitHub. It handles token requests, refreshes, and API calls, and integrates with Laravel via a service provider and facade.
Install and understand the four moving parts. composer require lusitanian/oauth:^0.8.11 gives you a library with no Laravel glue. Everything hangs off four objects you will wire yourself:
OAuth\Common\Consumer\Credentials — client id, secret, and callback URL.OAuth\Common\Storage\TokenStorageInterface — where access/request tokens and OAuth2 state live (session, cache, database).OAuth\Common\Http\Client\ClientInterface — the HTTP transport (CurlClient by default, StreamClient if you lack ext-curl).OAuth\ServiceFactory — maps a provider name ('Google', 'GitHub', 'Twitter') to a service class.Look first at: src/OAuth/ServiceFactory.php (supported provider names and createService() signature), src/OAuth/OAuth2/Service/AbstractService.php (getAuthorizationUri, requestAccessToken, refreshAccessToken, request), src/OAuth/Common/Storage/Session.php, and the README examples for Facebook/Twitter. The provider classes themselves (src/OAuth/OAuth2/Service/*.php) are the authoritative list of scopes and endpoints.
Note the 0.8.11 declaration fix before you wire anything. The current release corrects the declaration of AbstractService::requestAccessToken() — i.e. its signature, not its behaviour. Read the method head in AbstractService.php on the tag you install, and diff v0.8.10...v0.8.11, before you (a) subclass a provider service, (b) call requestAccessToken() with named arguments, or (c) copy a snippet from an older tutorial. Everything below assumes you are on ^0.8.11 and pin it.
First use case: a single-provider "connect GitHub" spike. Start in a controller, using a storage adapter that does not touch PHP's native session (see below), because the bundled Session storage calls session_start() and will fight Laravel's session handling.
// app/Providers/OAuthServiceProvider.php
$this->app->singleton(\OAuth\ServiceFactory::class, fn () => new \OAuth\ServiceFactory(new \OAuth\Common\Http\Client\CurlClient()));
// routes/web.php
Route::get('/auth/{provider}', [OAuthController::class, 'redirect']);
Route::get('/auth/{provider}/callback', [OAuthController::class, 'callback']);
// app/Http/Controllers/OAuthController.php
public function redirect(string $provider)
{
$service = $this->makeService($provider);
$state = Str::random(40);
session(["oauth.state.$provider" => $state]);
return redirect()->away($service->getAuthorizationUri(['state' => $state]));
}
public function callback(string $provider)
{
abort_unless(hash_equals(session()->pull("oauth.state.$provider"), request('state')), 403);
$service = $this->makeService($provider);
$token = $service->requestAccessToken(request('code'));
$profile = json_decode($service->request('/user'), true);
// persist $profile, associate with auth()->user(), etc.
return redirect('/dashboard');
}
private function makeService(string $provider)
{
$cfg = config("services.$provider");
$credentials = new \OAuth\Common\Consumer\Credentials(
$cfg['client_id'],
$cfg['client_secret'],
$cfg['redirect']
);
return app(\OAuth\ServiceFactory::class)->createService(
$cfg['service'], // exact provider class name, e.g. 'GitHub'
$credentials,
app(TokenStorage::class),
$cfg['scopes'] ?? []
);
}
// config/services.php
'github' => [
'service' => 'GitHub',
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('APP_URL').'/auth/github/callback',
'scopes' => ['user:email'],
],
Custom TokenStorageInterface backed by Laravel session (or cache/DB). This is the first thing to build on every project; it removes the native-session conflict and lets you swap to Redis or a table for queue/CLI flows. The interface keys everything by service name (retrieveAccessToken($service), storeAccessToken($service, TokenInterface $token), hasAccessToken, clearToken, plus the authorization-state methods), so one instance can serve many providers.
class LaravelSessionTokenStorage implements \OAuth\Common\Storage\TokenStorageInterface
{
public function retrieveAccessToken($service) { return session("oauth.$service.token"); }
public function storeAccessToken($service, \OAuth\Common\Token\TokenInterface $token) { session(["oauth.$service.token" => $token]); }
public function hasAccessToken($service) { return session()->has("oauth.$service.token"); }
public function clearToken($service) { session()->forget("oauth.$service.token"); }
public function clearAllTokens() { /* iterate keys */ }
// retrieve/store/has/clear AuthorizationState map to oauth.$service.state
}
For OAuth1 (Twitter), the request token secret must survive the redirect; use the same storage's authorization-state slots rather than a random session key, or you will lose the verifier handshake.
Provider-agnostic service builder. Keep one makeService() method (as above) driven by config/services.php. Provider name strings are used both as factory lookup keys and as storage keys, so centralize them — mixing 'google' and 'Google' will produce "no token" bugs that look like auth failures.
Guzzle HTTP client for observability. CurlClient gives you no logging, retries, or timeouts. Implement OAuth\Common\Http\Client\ClientInterface::retrieveResponse(UriInterface $endpoint, $method, array $params = [], array $extraHeaders = []) over Guzzle: GET params as query, everything else as http_build_query, return the raw string body, and throw on non-2xx so the library's token exceptions surface normally.
Refresh before you call. The service stores the token but does not proactively refresh for you. In a middleware or a withToken() helper:
$token = $storage->retrieveAccessToken('Google');
if ($token->isExpired()) {
$token = $service->refreshAccessToken($token);
$storage->storeAccessToken('Google', $token);
}
Custom providers by extension. When a provider isn't in ServiceFactory, extend OAuth\OAuth2\Service\AbstractService, set $authorizationEndpoint, $accessTokenEndpoint, $scopes, $scopesSeparator, override getAuthorizationMethod() (bearer header vs query string), and instantiate it directly with the same constructor arguments. If you also override requestAccessToken() — a common way to add token-response logging or to normalise a non-standard grant — the override must be signature-compatible with the declaration shipped in v0.8.11, otherwise PHP fatals at class-load time. This is also the escape hatch for self-hosted OAuth servers and enterprise tenants with custom endpoints.
Testing with Memory storage and a fake client. OAuth\Common\Storage\Memory plus a stub ClientInterface returning canned JSON lets you test controllers and token-refresh logic without network calls or a real browser. Seed it with OAuth\OAuth2\Token\StdOAuth2Token('fake-token', 3600).
Where this beats Socialite: providers Socialite has dropped or never supported, OAuth1 flows with non-Twitter providers, and services where you need to control the raw authorization URI and scopes. For standard Google/GitHub/Facebook logins, Socialite is less code and is maintained.
^0.8.11 and run the full auth flow on your target PHP version before committing.requestAccessToken" means the method head changed. Three concrete risks: (1) a custom service that overrides requestAccessToken() with the old signature now throws a fatal "declaration must be compatible" error; (2) callers using named arguments break if any parameter was renamed; (3) code copied from pre-0.8.11 tutorials/StackOverflow may no longer type-check. Grep your codebase for requestAccessToken and re-run your auth tests after upgrading.OAuth\Common\Storage\Session calls PHP session functions. Under Laravel, tokens written there are invisible to the framework and can break stateless APIs/Octane. Default to a custom storage adapter.state. Add it to getAuthorizationUri(['state' => ...]) and verify it in the callback, or you ship a CSRF hole. OAuth1's protection is the request-token secret — persist it across the redirect.APP_URL in local vs production, and the callback route must be registered outside auth middleware. Mismatches produce opaque provider errors, not library errors. vs ,) — let the provider class handle it, don't pre-join.access_type=offline (and often prompt=consent) as additional authorization parameters. Store the refresh token separately; isExpired() returns null-safe results when expires_in is absent, so treat "no expiry" as long-lived, not broken.OAuth\Common\Http\Exception\TokenResponseException with the provider's body attached — log that body. API request() failures come back as raw non-2xx responses depending on your HTTP client, so wrap calls in your own exception layer.request($path, 'POST', $body, $headers) sends form-encoded data by default. JSON APIs need Content-Type: application/json plus json_encode, and some providers reject Bearer headers in favor of query-string access tokens (check getAuthorizationMethod()).encrypted cast), and never log tokens or client secrets.ClientInterface (retries/timeouts/logging), your own TokenStorageInterface (multi-tenant tokens keyed by user + service), subclassing ServiceFactory to register custom providers, and AbstractService endpoint properties for non-standard hosts. When you use any of these, add a test that instantiates the subclass — that's what catches a future signature change like 0.8.11's.How can I help you explore Laravel packages today?