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

carlos-mg89/oauth

OAuth 2.0 package for Laravel/PHP that simplifies authorization flows, token management, and securing API routes, with configurable grants and middleware integration for adding delegated authentication to your app.

View on GitHub
Deep Wiki
Context7

Getting Started

There is no Laravel service provider here — carlos-mg89/oauth is a plain PHP OAuth 1/2 client, so "getting started" means wiring three objects yourself: credentials, token storage, and the service factory.

1. Install and check requirements

composer require carlos-mg89/oauth
php -m | grep curl   # the default CurlClient needs ext-curl

2. Look at these four things first, in this order

File Why
src/OAuth/ServiceFactory.php The only entry point you need; maps short names (github, google) to service classes
src/OAuth/Common/Consumer/Credentials.php Holds client id/secret/redirect URI
src/OAuth/Common/Storage/TokenStorageInterface.php The extension point you will almost certainly reimplement for Laravel
src/OAuth/OAuth2/Service/AbstractService.php Where getAuthorizationUri(), requestAccessToken(), request() live — read this before debugging anything

3. First use case: OAuth2 authorization-code login (GitHub)

use OAuth\Common\Consumer\Credentials;
use OAuth\Common\Storage\Session;
use OAuth\ServiceFactory;

$factory = new ServiceFactory();
$credentials = new Credentials(
    config('services.github.client_id'),
    config('services.github.client_secret'),
    route('oauth.callback', 'github')
);
$storage = new Session(); // native $_SESSION — see Gotchas

$github = $factory->createService('github', $credentials, $storage, ['user:email']);

if (request()->missing('code')) {
    return redirect()->away($github->getAuthorizationUri());
}

$token = $github->requestAccessToken(request('code'));
$user  = json_decode($github->request('user')->getBody(), true);

4. Register it in the container so controllers stay clean

// app/Providers/AppServiceProvider.php
$this->app->singleton(ServiceFactory::class, fn () => new ServiceFactory());
$this->app->bind(TokenStorageInterface::class, LaravelTokenStorage::class);

Once that binding exists, app(ServiceFactory::class)->createService(...) is the whole API surface you use day to day.

Implementation Patterns

Pattern 1 — A factory wrapper bound to the container

Don't scatter new Credentials(...) across controllers. Centralise provider config:

// config/services.php
'github' => [
    'client_id'     => env('GITHUB_CLIENT_ID'),
    'client_secret' => env('GITHUB_CLIENT_SECRET'),
    'scopes'        => ['user:email', 'repo'],
],

// app/Services/OAuth/ProviderFactory.php
class ProviderFactory
{
    public function __construct(private ServiceFactory $factory) {}

    public function make(string $provider): ServiceInterface
    {
        $cfg = config("services.{$provider}");

        return $this->factory->createService(
            $provider,
            new Credentials($cfg['client_id'], $cfg['client_secret'], route('oauth.callback', $provider)),
            app(TokenStorageInterface::class),
            $cfg['scopes'] ?? []
        );
    }
}

Pattern 2 — Eloquent-backed token storage (the important one)

The shipped Session storage is unusable behind Laravel's session middleware. Implement the interface against a table so tokens survive and can be refreshed offline:

class LaravelTokenStorage implements TokenStorageInterface
{
    public function __construct(private ?int $userId = null) {}

    private function key(string $service): string
    {
        return "{$service}:{$this->userId}";   // per-user isolation
    }

    public function retrieveAccessToken($service) { /* return TokenInterface from row */ }
    public function storeAccessToken($service, TokenInterface $token) { /* upsert, encrypt */ }
    public function hasAccessToken($service) { ... }
    public function clearToken($service) { ... }
    public function clearAllTokens() { ... }

    // Do not forget these four — they exist for the OAuth2 state parameter
    public function retrieveAuthorizationState($service) { ... }
    public function storeAuthorizationState($service, $state) { ... }
    public function hasAuthorizationState($service) { ... }
    public function clearAuthorizationState($service) { ... }
}

Store access_token, refresh_token, expires_at, scopes as separate columns and cast the token to encrypted — the library's Token object serialises cleanly, but a JSON blob is easier to query and to migrate.

Pattern 3 — Controller flow

public function redirect(string $provider)
{
    return redirect()->away(app(ProviderFactory::class)->make($provider)->getAuthorizationUri());
}

public function callback(Request $request, string $provider)
{
    // verify state before anything else
    $service = app(ProviderFactory::class)->make($provider);
    $token   = $service->requestAccessToken($request->string('code'));

    OAuthAccount::updateOrCreate(
        ['user_id' => $request->user()->id, 'provider' => $provider],
        ['token' => $token]
    );

    return redirect()->route('dashboard');
}

Pattern 4 — Swap in a Guzzle client

The default CurlClient blocks testing. Register a custom client implementing OAuth\Common\Http\Client\ClientInterface that delegates to Laravel's HTTP client — you get logging, retries, and Http::fake() for free:

$this->app->bind(ClientInterface::class, GuzzleClientAdapter::class);
$factory->setHttpClient(app(ClientInterface::class));

Pattern 5 — Refresh tokens in a queued job

Nothing refreshes automatically. Schedule a command that selects accounts expiring within the hour and calls the provider's refresh endpoint:

$service = app(ProviderFactory::class)->make($account->provider);
$service->refreshAccessToken();   // only where the version/provider supports it

If refreshAccessToken() isn't available for your provider, POST the token endpoint via $service->request($tokenUrl, 'POST', $params) and persist the returned token yourself.

Pattern 6 — Custom providers and overridden endpoints

ServiceFactory::registerService('acme', AcmeService::class) lets you subclass OAuth\OAuth2\Service\AbstractService and override getAuthorizationEndpoint(), getAccessTokenEndpoint(), getAuthorizationMethod(), and the scope constants. This is the standard workaround when a bundled service lags behind a provider's API version.

Pattern 7 — OAuth 1 flow

OAuth1 has an extra leg; the shape differs:

$token = $service->requestRequestToken();
return redirect()->away($service->getAuthorizationUri(['oauth_token' => $token->getRequestToken()]));

// callback
$service->requestAccessToken($request->string('oauth_token'), $request->string('oauth_verifier'));

Pattern 8 — Testing

Bind OAuth\Common\Storage\Memory instead of the Eloquent adapter and a stub ClientInterface returning canned JSON. Assert the redirect URI contains client_id, scope, and state; assert the callback stores a token row. That covers ~90% of the code you actually own.

Gotchas and Tips

Session storage is native $_SESSION. OAuth\Common\Storage\Session writes to the PHP superglobal, not to Laravel's session driver. Under Laravel's StartSession middleware with a cache/redis/DB driver, $_SESSION is empty and the token vanishes between the redirect and the callback. Always replace it.

Storage keys are scoped by service name only. Two users logging into the same provider overwrite each other's token. Either construct one storage instance per user (as in Pattern 2) or namespace the key with the user id — the library does not do this for you.

The state methods are easy to miss. If you implement TokenStorageInterface by hand and skip retrieveAuthorizationState / storeAuthorizationState, you get a fatal error or silent CSRF bypass on the callback. Validate the returned state against the session yourself; the library generates and sends it but leaves verification to you.

Errors surface as exceptions, not return values. Catch OAuth\Common\Http\Exception\TokenResponseException (token endpoint 4xx) and OAuth\Common\Exception\Exception at the callback. A denied consent screen returns ?error=access_denied with no code — check for that before calling requestAccessToken(), or you'll get a confusing exception instead of a clean "user declined" message.

Bundled endpoints go stale. Service classes hardcode API URLs and often a version prefix. When a provider deprecates v3 for v4, you're editing a vendor file — instead subclass, override the constant, and registerService() your class. Keep a note of every override so upgrades don't silently revert them.

Scopes: constants vs strings. Many services expose SCOPE_* constants, but the factory accepts plain strings too. Passing an unregistered scope can throw InvalidScopeException on services that validate against a whitelist. When in doubt, pass the raw scope string the provider documents.

Token expiry isn't tracked for you. TokenInterface carries an expiry, but nothing checks it before request(). A 401 from the provider is your only signal. Wrap API calls and refresh on 401 rather than guessing.

Low bus factor. 7 stars, zero known dependents, and a NOASSERTION license (check the LICENSE file before commercial use — this is a fork of Lusitanian/PHPoAuthLib). Pin an exact version in composer.json, commit the lockfile, and assume you may need to fork. Diff against upstream before upgrading.

It may be the wrong tool. If you only need "log in with Google/GitHub/Facebook," Laravel Socialite is maintained, has a provider in the container, and handles the state parameter correctly. Reach for this library when you need OAuth 1, a provider Socialite doesn't ship, or direct API access with a stored token outside of the login flow.

ServiceFactory is cheap but not stateless-enough to share blindly. It holds the registered service map and HTTP client; create it once as a singleton (as in the binding above) rather than per-request new calls, and configure the HTTP client before the first createService().

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console