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 Laravel Package

psx/oauth2

PSX OAuth2 is a PHP OAuth2 server library for building secure authorization and token endpoints. It supports standard grant types, scopes, access and refresh tokens, and pluggable storage adapters, making it easy to add OAuth2 authentication to your API.

View on GitHub
Deep Wiki
Context7

Getting Started

psx/oauth2 is the OAuth2 consumer and provider implementation from the PSX framework (apioo/chriskapp). It is not a Laravel package: there is no service provider, no facade, no config file, and no Laravel-specific documentation. Treat it as a low-level library you wire yourself. With 0 stars and 0 dependents, assume you are the first production user and budget time for reading the source.

Install and pin

composer require psx/oauth2
composer why-not psx/oauth2   # check for PHP / psx-http conflicts with your Laravel version

Check composer.lock immediately afterwards. This package drags in psx/http, psx/schema, psx/data-adjacent abstractions and PSR-7/PSR-17 dependencies; version pins from older PSX releases frequently collide with Laravel 10/11's PHP requirement. If why-not shows a hard conflict, stop — do not force it.

Where to look first

  1. src/Consumer/ — the client side (AuthorizationCode, ClientCredentials, Password, RefreshToken, ConsumerInterface).
  2. src/Provider/ — the server side (AuthorizationCode, ProviderAbstract, token/authorization abstractions).
  3. src/AccessToken.php — the value object returned by both sides (getAccessToken(), getRefreshToken(), getExpiresIn(), getScope(), getTokenType()).
  4. The repo's tests — with no docs and no dependents, the test suite is the actual specification of method signatures.

First use case

Pick the consumer side and the client credentials grant, used in a queued job or a scheduled command that calls a third-party API. It is the least framework-coupled path: one HTTP call to the token endpoint, one value object back, no redirects, no session, no storage. Prove that works in a standalone php artisan tinker snippet before touching the authorization-code flow or the provider side.

// config/services.php
'vendor_api' => [
    'token_url'    => env('VENDOR_TOKEN_URL'),
    'client_id'    => env('VENDOR_CLIENT_ID'),
    'client_secret'=> env('VENDOR_CLIENT_SECRET'),
],

Implementation Patterns

1. Wrap the consumer in a Laravel service, bind it once

Never instantiate PSX consumers in controllers. The library takes an HTTP client in the constructor, so build it in a service provider and resolve it from the container. Keep your own interface in front of it so you can swap to league/oauth2-client later without touching call sites.

namespace App\Services\OAuth;

interface TokenProvider
{
    public function token(): string;
    public function forget(): void;
}

final class PsxClientCredentialsProvider implements TokenProvider
{
    public function __construct(
        private \PSX\Oauth2\Consumer\ConsumerInterface $consumer,
        private \Illuminate\Contracts\Cache\Repository $cache,
    ) {}

    public function token(): string
    {
        return $this->cache->remember('oauth:vendor_api', now()->addMinutes(50), function () {
            $token = $this->consumer->getAccessToken();      // confirm signature in src/Consumer
            // Cache for expires_in minus a safety margin, not expires_in itself.
            $this->cache->put('oauth:vendor_api', $token->getAccessToken(), now()->addSeconds(max(60, $token->getExpiresIn() - 60)));
            return $token->getAccessToken();
        });
    }
}

Bind it in AppServiceProvider::register() and inject TokenProvider — not the PSX class — into jobs, commands, and HTTP clients.

2. Authorization-code flow: two routes, state in the session

Keep the redirect and the callback in a dedicated controller and use named routes. PSX hands you a URI string for the redirect; you return a plain Laravel redirect. On the callback, validate state before exchanging the code.

public function redirect(Request $request)
{
    $state = Str::random(40);
    $request->session()->put('oauth.state', $state);

    return redirect()->away(
        $this->consumer->getAuthorizationUri('read write', $state)
    );
}

public function callback(Request $request, TokenRepository $tokens)
{
    abort_unless(hash_equals($request->session()->pull('oauth.state'), (string) $request->query('state')), 403);
    abort_if($request->query('error'), 400, $request->query('error_description'));

    $token = $this->consumer->getAccessToken($request->query('code'));

    $tokens->storeForUser($request->user(), $token); // persist refresh_token + absolute expiry
    return redirect()->route('dashboard');
}

Persist refresh_token in a oauth_connections table keyed by (user_id, provider). The library gives you relative expires_in — always store expires_at = now()->addSeconds($expiresIn) yourself.

3. Auto-refresh on 401 in an outbound HTTP middleware

The cleanest integration for day-to-day use: a small wrapper around Http::withToken() that refreshes once and retries. Do not scatter refresh logic across jobs.

Http::withToken($provider->token())
    ->retry(2, 200, function ($e) use ($provider) {
        if ($e instanceof ConnectionException || $e->response?->status() !== 401) return false;
        $provider->forget();          // force a re-fetch / refresh-token exchange
        return true;
    })
    ->get($url);

4. Provider side: implement the storage abstractions against Eloquent

The provider half expects you to subclass an abstract (authorization-code provider) and answer questions like "what is the client secret for this client id", "is this redirect URI registered", "who is this user", "store this token". Implement those against Eloquent models (OauthClient, OauthAccessToken, OauthRefreshToken). Two integration notes:

  • The provider returns PSR-7 responses, not Laravel responses. Convert with response($psr7->getBody()->getContents(), $psr7->getStatusCode(), $psr7->getHeaders()).
  • Register the token/authorize routes in routes/api.php (or exclude them from VerifyCsrfToken), because OAuth2 token requests are application/x-www-form-urlencoded POSTs without a CSRF token.

5. Testing

Because everything goes through a PSR-7 client you can substitute, inject a fake/stub HTTP client in the container during tests rather than hitting Http::fake() (which only intercepts Laravel's client, not PSX's). Assert on token caching and on the retry-on-401 path — those are the two places bugs hide.


Gotchas and Tips

  • The package is unproven. 0 stars, 0 dependents, score 0. There is no community to answer questions and no guarantee of maintenance. For a new Laravel project, league/oauth2-client (consumer) and league/oauth2-server (provider) are the default choices; psx/oauth2 is defensible mainly if you are already in the PSX/psx/api ecosystem or want a compact reference implementation. Decide this before writing code, not after.
  • Namespace/version drift. PSX has gone through namespace and version changes; tutorials and snippets online often target an older release. Trust src/ and the tests over any blog post, and verify every method signature you copy.
  • No Laravel glue at all. No config publishing, no container bindings, no events, no exception rendering. Any exception the library throws is a raw PHP exception — catch and map it to HttpException/ValidationException at your controller boundary, or your API will return 500s on invalid grants.
  • Relative expiry only. getExpiresIn() is seconds-from-now. Store absolute timestamps, and always subtract a margin (30–60s) to absorb clock skew between you and the provider.
  • No automatic refresh. Neither consumer class will refresh a token for you on expiry; you must detect 401s or check expires_at yourself and run the refresh-token grant explicitly.
  • PKCE support is not a given. Verify in src/Consumer before designing a public/native-client flow. If it is absent, do not use this library for public clients — use confidential clients only, or switch libraries.
  • Redirect URI matching is byte-exact. Trailing slashes, http vs https, and port numbers must match what is registered with the provider. This is the #1 cause of "invalid_grant" during setup.
  • Provider routes and middleware. Token endpoints must be outside web (session/CSRF) and usually outside auth — authentication happens inside the grant. Getting this wrong yields confusing 419s.
  • Debugging checklist when an exchange fails: log the raw request body and response body (mask client_secret and code), confirm Content-Type: application/x-www-form-urlencoded, confirm the grant_type string, and check whether the provider expects credentials in the body or in a Basic auth header.
  • Extension points worth knowing: subclass the consumer/provider abstracts to override token request construction or error parsing; inject your own PSR-18 client to add logging/middleware; and for the provider, the storage callbacks are the natural seam for Eloquent, caching, and audit logging.
  • Exit strategy. Because you wrapped it behind a TokenProvider interface, migrating to league/oauth2-client should be a one-class change. Keep that interface honest — do not leak PSX\Oauth2\AccessToken into your domain layer.
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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource