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

Oauth2 Client Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

league/oauth2-client is a low-level OAuth 2.0 client, not a full auth scaffold. You get the authorization URL builder, the token endpoint exchange, token/expiry objects, and a resource-owner fetch. Everything else (state validation, user persistence, token storage) is yours.

Install the base and a provider package. The base package alone only ships GenericProvider; real-world providers live in separate packages:

composer require league/oauth2-client
composer require league/oauth2-google   # or oauth2-github, oauth2-facebook, etc.

Where to look first

  1. The usage guide at https://oauth2-client.thephpleague.com/usage/ — the canonical redirect → callback → token → resource-owner loop.
  2. The provider list (/providers/league/ and /providers/thirdparty/) before writing anything custom.
  3. AbstractProvider in src/Provider/AbstractProvider.php — almost every customization point (getBaseAccessTokenUrl, createResourceOwner, checkResponse, getDefaultScopes) lives there.

First use case: "Sign in with Google" in a Laravel app. Read credentials from config/services.php so nothing is hard-coded:

// config/services.php
'google' => [
    'client_id'     => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect'      => env('GOOGLE_REDIRECT_URI'), // must match the console exactly
],
// routes/web.php
Route::get('/auth/google/redirect', [GoogleController::class, 'redirect'])->name('oauth.google');
Route::get('/auth/google/callback', [GoogleController::class, 'callback']);
// app/Http/Controllers/GoogleController.php
use League\OAuth2\Client\Provider\Google;

public function redirect()
{
    $provider = $this->provider();

    $url = $provider->getAuthorizationUrl([
        'scope' => ['openid', 'email', 'profile'],
    ]);

    // getAuthorizationUrl() also generates the state; persist it yourself.
    session(['oauth2.google.state' => $provider->getState()]);

    return redirect()->away($url);
}

public function callback(Request $request)
{
    $expected = session()->pull('oauth2.google.state'); // pull => one-time use
    abort_unless($expected && hash_equals($expected, (string) $request->input('state')), 403);

    $provider = $this->provider();

    $token  = $provider->getAccessToken('authorization_code', [
        'code' => $request->input('code'),
    ]);

    $owner  = $provider->getResourceOwner($token); // $owner->toArray() for raw claims

    // ... match/create user, log in ...
}

private function provider(): Google
{
    return new Google([
        'clientId'     => config('services.google.client_id'),
        'clientSecret' => config('services.google.client_secret'),
        'redirectUri'  => config('services.google.redirect'),
    ]);
}

That five-line callback (validate state → exchange code → fetch owner) is the whole package in practice. Everything below is variations on it.

Implementation Patterns

Wrap the provider in a container binding. Re-instantiating new Google([...]) per request is fine, but a singleton or factory keeps config in one place and makes testing trivial:

// app/Providers/AppServiceProvider.php
$this->app->bind(Google::class, fn () => new Google([
    'clientId'     => config('services.google.client_id'),
    'clientSecret' => config('services.google.client_secret'),
    'redirectUri'  => config('services.google.redirect'),
]));

Use GenericProvider for anything spec-compliant without a dedicated package — including your own Laravel Passport server:

$provider = new League\OAuth2\Client\Provider\GenericProvider([
    'clientId'                => config('services.passport.client_id'),
    'clientSecret'            => config('services.passport.client_secret'),
    'redirectUri'             => route('oauth.callback'),
    'urlAuthorize'            => url('/oauth/authorize'),
    'urlAccessToken'          => url('/oauth/token'),
    'urlResourceOwnerDetails' => url('/api/user'),
]);

Tokens are objects, not strings. Persist the whole thing; it round-trips through JSON cleanly (access_token, refresh_token, expires_in):

$user->forceFill([
    'oauth_access_token'  => $token->getToken(),
    'oauth_refresh_token' => $token->getRefreshToken(),
    'oauth_expires_at'    => $token->getExpires(), // unix timestamp, null if no expires_in
])->save();

Cast secrets with Laravel's encrypted cast and never put tokens in logs or toArray() dumps.

Refresh proactively, in a job, not inline. Check expiry before making calls and refresh in a queued job so a slow provider never blocks a request:

if ($token->hasExpired()) {
    $new = $provider->getAccessToken('refresh_token', [
        'refresh_token' => $user->oauth_refresh_token,
    ]);
    // Many providers rotate refresh tokens — always overwrite, never assume the old one survives.
    $user->forceFill([
        'oauth_access_token'  => $new->getToken(),
        'oauth_refresh_token' => $new->getRefreshToken() ?? $user->oauth_refresh_token,
        'oauth_expires_at'    => $new->getExpires(),
    ])->save();
    $token = $new;
}

Use PKCE for public/mobile/SPA clients (supported since 2.7.0). Generate the verifier before redirect, store it in the session, and pass it back on exchange:

$provider->setPkceCode($verifier = $provider->getPkceCode()); // after getAuthorizationUrl()
session(['oauth2.pkce' => $provider->getPkceCode()]);

// callback
$provider->setPkceCode(session()->pull('oauth2.pkce'));
$token = $provider->getAccessToken('authorization_code', ['code' => $request->code]);

Extend AbstractProvider for vendor quirks. When a provider wants a non-standard param, HTTP Basic auth on the token endpoint, or a weird JSON envelope, subclass and override the narrowest method: getBaseAccessTokenUrl(), getAccessTokenRequest(), prepareAccessTokenResponse(), createResourceOwner(), or checkResponse(). HttpBasicAuthOptionProvider and GuardedPropertyTrait (both added in 2.4.0) exist precisely for these cases.

Inject a configured Guzzle client for logging, retries, or a corporate proxy. Every provider accepts an httpClient option:

new Google([
    'clientId' => /* ... */,
    'httpClient' => new \GuzzleHttp\Client([
        'handler' => $stack, // middleware stack with logging/retry
    ]),
]);

Account linking. Always key on (provider, resource_owner_id) — $owner->getId() maps to sub for OIDC providers — rather than on email, which can change or be absent. Store email separately and only auto-link to an existing user if the provider asserts email_verified.

Testing. Mock the two HTTP calls with Guzzle's MockHandler (passed via httpClient), assert the state round-trip, and use the library's time-override hook (added in 2.6.0) to test expiry without sleep(). Don't hit real providers in CI.

Gotchas and Tips

  • The library does not validate state for you. It generates and exposes it; comparing it to your session value is your job. Use hash_equals and pull it from the session so it can't be replayed.
  • redirect_uri mismatch is the #1 failure. It must be byte-identical to what's registered with the provider — scheme, host, port, trailing slash. Prefer generating it once with route()/url() and using that same value in both the config and the console.
  • Scopes are arrays, not space-delimited strings in getAuthorizationUrl(['scope' => ['a', 'b']]). Also note 2.8.1 changed scope-sending behavior on the token request ("only provide scopes in access token when set in options") — if a provider rejects an unexpected scope param, upgrade or don't pass one.
  • Refresh tokens aren't always issued. Google returns one only with access_type=offline and typically only on the first consent (prompt=consent forces re-issue). Handle getRefreshToken() === null gracefully — don't assume a refresh path exists.
  • Catch IdentityProviderException, not generic Guzzle errors. Provider error payloads land in $e->getResponseBody(); that's where invalid_grant, invalid_client, and rate-limit messages live. getAccessToken() also throws UnexpectedValueException when the token endpoint returns non-JSON (2.3.1+).
  • expires_in => 0 is valid (treated as immediately expired since 2.1.0) and expires values that aren't numeric were fixed in 2.8.0 — pin a recent 2.8+ release if you're parsing expiry yourself.
  • Store the raw token response, not just access_token. AccessToken::getValues() exposes vendor extras that some providers only return once at issue time.
  • Don't share one session key across providers. If a user can start a GitHub and a Google flow simultaneously, key state and PKCE verifiers per provider name.
  • Laravel Socialite overlaps but isn't a substitute. Socialite covers common providers well; reach for oauth2-client when Socialite doesn't ship your provider, when you need PKCE or a custom token endpoint, or when you want the token objects rather than Socialite's two()/user() façade. Both can coexist in one app.
  • Logging tokens is a real incident waiting to happen. Guzzle middleware that dumps request bodies will capture client_secret and code; scope any debug middleware to strip the token endpoint.
  • Guzzle version floor matters for security. 2.8.0 raised the minimum to 6.5.8 / 7.4.5 — make sure nothing in your dependency tree pins you below that.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style