league/oauth2-client
league/oauth2-client is a PHP library providing a base for OAuth 2.0 integrations. It includes a GenericProvider for Bearer-token services, follows RFC 6749, and can be extended for provider-specific behavior.
Architecture fit
league/oauth2-client is a framework-agnostic protocol library, not a Laravel package. It ships no service provider, facade, config file, migration, or Blade views. In a Laravel codebase it belongs in the integration/adapter layer, wrapped by our own service classes — not called directly from controllers.AbstractProvider / GenericProvider, AccessToken, ResourceOwnerInterface, plus option providers and the ProviderRedirectTrait. That is a healthy seam: we can subclass per provider and keep vendor quirks out of domain code.league/oauth2-client directly is justified primarily for third-party API authorization — "connect our app to a customer's Google/Microsoft/HubSpot/Salesforce account to sync data" — and for machine-to-machine client-credentials flows. If the requirement is end-user login, the correct decision is Socialite, and this evaluation should be closed.Integration feasibility
composer require league/oauth2-client, PSR-4 autoloading, PSR-7 messages, Guzzle as the HTTP transport. Guzzle is already present in every modern Laravel app, so dependency weight is effectively neutral.AbstractProvider::getHttpClient() accepts an injected Guzzle client, which means we can push our standard middleware (timeouts, retry, structured logging with credential redaction, tracing) into the OAuth calls rather than bolting it on.ProviderFactory/OAuthClientManager singleton in a dedicated service provider is a half-day of work.SettableRefreshTokenInterface), which clears the two security/UX features most commonly demanded by security review.Technical risk
state generation/validation, PKCE verifier storage, token encryption at rest, refresh-token rotation and race conditions, and preventing client secrets or bearer tokens from reaching logs. The library gives us the primitives; it does not enforce a safe composition.UnexpectedValueException on non-JSON access-token responses; token expiry typing has been patched repeatedly (e.g. expires not numeric, expires_in of 0). Each provider we integrate needs its own subclass and its own tests.prepareAccessTokenResponse() docblock type hint, explicitly flagged as potentially affecting downstream providers. Teams running PHPStan at high levels will see findings on minor upgrades.Dependents: 0 is almost certainly a metadata artifact (this is one of the most widely depended-upon OAuth packages in PHP), and the reported last release date of 2026-09-16 is inconsistent with the newest changelog entry (2.9.0, 2025-11-25). Neither should be used to justify or reject adoption; both should be re-verified from Packagist/GitHub before this assessment is circulated.Key questions
state, PKCE, and redirect URI allowlisting?Stack fit
OAuthConnectionService with authorizationUrl(), exchangeCode(), refresh(), revoke(). Domain code should never type against League\OAuth2\Client\Provider\AbstractProvider.oauth_connections: provider, tenant/user id, encrypted access + refresh token, scopes, expires_at, revoked_at). Use Laravel's encrypted casting and never log the token attributes.Cache::lock() keyed on the connection id around refresh to prevent thundering-herd duplicate refreshes; queue the refresh rather than doing it inline in a web request.Migration path
GenericProvider plus a thin subclass is usually a smaller and better-tested implementation than hand-rolled cURL, so the first migration pays for itself.league/oauth2-client-based; reuse is possible but check provider-client maintenance status before inheriting it.Compatibility
Sequencing
state/PKCE handling, redirect route + controller.Maintenance
Support
Scaling
expires_in, minus a skew buffer.Failure modes
needs_reauth, and surface it in-app and via alert.UnexpectedValueException; unwrapped, this produces 500s in the callback route. Catch and translate to a user-facing "authorization failed, try again."state mismatch or expired state: users returning from a slow provider login. Decide the UX (restart flow vs. error) up front; store state with a short TTL.expires_in: historically a source of bugs; treat unknown expiry conservatively (assume short TTL, refresh early).Ramp-up
state, refresh semantics, scope negotiation — not the package.How can I help you explore Laravel packages today?