spawnrad/oauth-bundle
spawnrad/oauth-bundle provides OAuth authentication support for PHP applications, making it easier to connect external providers, manage access tokens, and streamline login and authorization flows. Configure providers quickly and keep OAuth integration organized in one reusable bundle.
First, resolve the framework question before you write a line of code. spawnrad/oauth-bundle is described as a Laravel package, but the repository it points at (Spawnrad/HWIOAuthBundle) is a fork of HWIOAuthBundle, which is a Symfony bundle. Read composer.json first and confirm what you actually have:
composer show spawnrad/oauth-bundle # inspect type, require, autoload
If type is symfony-bundle and the require block pulls symfony/security-bundle and symfony/http-kernel, you are not looking at a Laravel package. There will be no Illuminate\Support\ServiceProvider, no extra.laravel.providers entry, no oauth.php config to publish, and no middleware alias. Composer's Laravel auto-discovery will silently do nothing — you'll composer require, the app will boot, and nothing will be registered. That silence is the first gotcha, not the last.
Where to look, in order:
composer.json — framework type and version constraints. This decides everything else.hwi/oauth-bundle. Clone both and run git log --oneline upstream/master..HEAD and git diff upstream/master --stat. "Extended" appears nowhere in the description, so the diff is the only documentation of intent.OAuth/ResourceOwner/ (or the fork's equivalent path) — this is the genuinely valuable part of the repo: one class per provider mapping raw API payloads (Google, GitHub, Facebook, LinkedIn, etc.) into a normalized user object.DependencyInjection/ and Resources/config/ — Symfony wiring you will not use on Laravel, and reading it is a time sink unless you're actually on Symfony.The realistic first use case is extraction, not installation. Pick the one provider your product needs that Laravel Socialite doesn't ship a driver for. Open that Resource Owner class, read its getUserInformation() / field mapping, and treat it as the spec for a Socialite custom provider. That's a few hundred lines and it stays inside the framework's idioms:
// app/Socialite/AcmeProvider.php
class AcmeProvider extends AbstractProvider
{
protected function getAuthUrl($state) { /* copy from ResourceOwner */ }
protected function getTokenUrl() { /* copy from ResourceOwner */ }
protected function getUserByToken($token)
{
// The Resource Owner class is your field-mapping reference here.
// Note scopes, endpoint version, and required headers from the fork.
}
protected function mapUserToObject(array $user)
{
return (new User)->setRaw($user)->map([
'id' => $user['sub'], 'name' => $user['name'], 'email' => $user['email'],
]);
}
}
Register it with Socialite's extend() in a service provider, add the client_id / client_secret / redirect to config/services.php, and you have a working login within a sprint.
If you are genuinely on Symfony — and only then — the bundle is installable as designed: enable it in config/bundles.php, import the routing, and configure a firewall resource. Even in that case, install upstream hwi/oauth-bundle, not this fork, unless the diff review gives you a specific reason.
Pattern 1 — Provider-spec extraction (Laravel, recommended). Treat the repo as a catalog of provider quirks and never load its runtime. Day to day this looks like: a SocialiteProviders-style package for the common IdPs, plus your own AbstractProvider subclasses for the remainder, written from the fork's Resource Owner classes. Vendor the extracted mapping logic into your own namespace with the MIT header retained and a comment pointing at the source commit — this preserves attribution and makes future upstream comparison trivial.
Pattern 2 — Socialite as the runtime, fork as the reference. Your controllers and tests never know the bundle exists:
Route::get('/auth/{provider}/redirect', fn ($p) => Socialite::driver($p)->redirect());
Route::get('/auth/{provider}/callback', function ($p) {
$user = Socialite::driver($p)->user();
// stateless=false; validate state; then Auth::login(...)
});
Guard the callback route with web middleware (session state), not api.
Pattern 3 — Separate Symfony service (escape hatch). If a provider genuinely needs an OAuth 1.0a handshake that Socialite can't express, isolate it: run a small Symfony worker exposing one token-exchange endpoint, and have Laravel call it over HTTP behind your own TokenExchanger interface. Never bootstrap the bundle's DI container inside the Laravel app — shared symfony/* transitive packages make that fragile across composer update.
Pattern 4 — Symfony-native use (if you actually are on Symfony). Configure a firewall resource:
hwi_oauth:
firewall_names: [secured_area]
resource_owners:
acme:
type: acme
client_id: "%acme_id%"
client_secret: "%acme_secret%"
…and let the bundle's security listener handle the redirect/callback cycle. Your day-to-day work then becomes reading the EventDispatcher hooks for post-authentication user provisioning.
Integration tips that apply to every pattern:
error=access_denied), and expired-code paths.The package will not register itself in Laravel. No service provider, no facade, no publishable config. If you composer require it expecting php artisan vendor:publish, you'll get nothing and lose an hour. Confirm this in the first five minutes.
Composer will likely refuse to install it into a Laravel app. Both ecosystems depend on symfony/http-foundation, symfony/console, and symfony/finder at different constraints. Expect resolver conflicts on Laravel 10/11. Don't "solve" this with --ignore-platform-reqs — you'll get a broken autoloader or, worse, a silently downgraded Symfony component that breaks framework internals.
Description/artifact mismatch is the top bug source. Everything in the description — service container integration, route middleware for scopes, Artisan commands for key generation and client registration — describes a package that isn't in the repo. If a teammate wrote a ticket from the description, kill it before it reaches a sprint.
Zero stars, zero dependents is a structural risk, not a vanity metric. You are the only consumer. There is no CVE path to you, no provider-API-breakage patch, and no release cadence. If you adopt the runtime, you own the fork — including a named maintainer and a documented diff against upstream. Auth code is the highest-blast-radius surface in the product; an unmaintained fork there is a standing audit finding.
Provider drift is the day-to-day failure mode, and it's silent. Google migrated to OIDC, Twitter/X deprecated OAuth 1.0a, PKCE requirements tightened. When a provider changes a response shape, login fails for a subset of users before any monitoring fires. Mitigate with per-provider integration tests in CI, not with hope.
Debugging tips: log the raw token and userinfo payloads (redacting secrets) behind a debug flag — field-mapping bugs are almost always a renamed JSON key. For state mismatches, verify session persistence on the callback route and that the callback URL registered with the provider matches byte-for-byte, including trailing slash and scheme. redirect_uri_mismatch is the single most common "the package is broken" that isn't.
Version pinning. No CI matrix and no documented PHP constraint means 8.2/8.3 support is unverified. If you extract code, it's plain PHP and portable — that's exactly why extraction beats installation here.
Extension points worth reading in the source: the Resource Owner mapUserToObject-equivalent (field normalization), the owner_details URL and scope declarations per provider (often the only place a required scope or API version is recorded), and any custom ResourceOwnerInterface implementation for non-standard handshakes. These are the pieces with residual value if upstream is abandoned.
Licensing. MIT, so extraction is fine — but keep the copyright header and add a comment linking to the source repository and commit hash. That's your upgrade path back to upstream if the fork ever dies or the provider gets adopted upstream.
The decision gate. Require a written, one-sentence justification before any Symfony-bundle runtime enters a Laravel application. In practice, the answer is almost always "extract the mapping, use Socialite."
How can I help you explore Laravel packages today?