splash/oauth2
Splash OAuth2 brings OAuth2 authentication to Laravel/PHP applications, simplifying secure token handling, authorization flows, and API integrations. It offers a clean, framework-friendly way to protect routes and connect services with standard OAuth2 providers.
This package's adoption is essentially zero — 0 stars, 0 dependents, GitLab-hosted. That means there is no tutorial, no blog post, and no Stack Overflow corpus to lean on. The source is the documentation, so budget the first hour to read src/, the service provider, and the published config before touching your app.
Minimal setup
composer require splash/oauth2
php artisan vendor:publish --provider="Splash\Oauth2\Oauth2ServiceProvider"
php artisan migrate
Then immediately inventory what you got:
cat vendor/splash/oauth2/composer.json # PHP + Laravel constraints
find vendor/splash/oauth2/src -name "*.php" # actual surface area
cat config/oauth2.php # the real API reference
ls database/migrations/*oauth* # tables it creates
The config file is the map: it tells you what client/token/scope tables exist, what grant types are enabled, what the default token lifetime and refresh-token lifetime are, and which middleware aliases get registered. Cross-reference it against the service provider — anything the provider registers and the config doesn’t expose is a package you’ll be editing in a fork.
First use case — protect one route with one client. Pick the client-credentials grant: no redirect flow, no consent screen, no user table involvement. Insert a client row manually (or via tinker), hit POST /oauth/token with grant_type=client_credentials, get a bearer token back, and guard a single Route::get('/ping', ...)->middleware('auth.oauth'). Confirm the 401 → token → 200 loop end-to-end before wiring any real endpoint. That single loop validates that the middleware alias, the token table, and the config line up — and it’s the fastest way to discover if the package is even installable against your Laravel version.
Treat grant types as separate subsystems. The package ships three: authorization code, client credentials, and password. They share token storage but not much else, and each fails differently. Adopt them in order of least-risk:
state handling, PKCE (verify whether it's enforced or just supported), and code expiry. Read the controller code yourself — this is where OAuth2 servers get their bugs.Middleware layering. Standard pattern — two middlewares, one for authentication, one for scope:
Route::middleware('auth.oauth')->group(function () {
Route::get('/me', MeController::class);
Route::middleware('oauth.scopes:orders.read')->group(function () {
Route::get('/orders', OrderController::class);
});
});
Verify in the source that the scope middleware rejects rather than silently passes when a scope is missing — a subtle inversion here is a classic Laravel OAuth bug. Write a feature test that hits a scoped route with a token missing the scope and asserts a 403.
Token issuance as a first-class action. Don’t hand-issue tokens in tests or seeders by inserting into the tokens table. Find the package’s token service (or the controller’s internal method) and use it, or you’ll create tokens that fail the middleware’s validation on expiry/revocation checks. If the package doesn’t expose one, that absence is itself a signal about its maturity — you’ll be writing that helper.
Testing patterns. For feature tests, generate a client + token per test (or a factory fixture in setUp), hit Authorization: Bearer ..., and assert. Don’t mock the middleware — mock the network layer of whatever client library is underneath, so you’re exercising the actual token verification path.
Revocation and refresh as explicit flows. Wire revocation into your logout / "disconnect account" path as its own endpoint call. Wire refresh into whatever your client SDK does, not into the resource server — the resource server should return 401 with a WWW-Authenticate: Bearer error="invalid_token" and let the client refresh. Get this separation right early; it’s hard to untangle later.
repositories entry in composer.json pinned to a commit SHA — never a branch). Assign an owner. The moment you depend on it, you own it.oauth_clients, oauth_tokens, oauth_scopes, etc. will fight with laravel/passport if it’s also installed, and with your own users foreign keys. Check the migrations before running php artisan migrate in an existing app — and never run them blind in production.access_token column sitting in MySQL is fine for a demo and unacceptable in production. If the package stores them plaintext, add a Crypt mutator or wrap the model — but note that middleware lookups by token then require deterministic hashing, not Crypt (which is salted). This distinction is the most common mistake here.Cache::lock("refresh:{$clientId}:{$userId}") at the application layer — do not trust the package to do this correctly.expires_at > now()->addSeconds(30)) produce intermittent 401s in load tests and under NTP drift. Confirm the margin, and add one if absent.Token model you can subclass, an Authenticate middleware you can swap, and a service binding for the token repository. If the package hard-codes new Token, you cannot swap storage without forking.How can I help you explore Laravel packages today?