oauth2-framework/server-library
OAuth2 Framework Server Library gives PHP applications the components needed to run an OAuth2 authorization server, including grant handling, token issuance, client authentication, and standards-compliant endpoint behavior for secure API access.
This is a framework-agnostic OAuth 2.0 authorization server (PSR-7/PSR-15 oriented), not a Laravel package — there is no service provider, no publishable config, no migrations, no facade. Budget a half-day of wiring before your first token.
Install and orient yourself:
composer require oauth2-framework/server-library
Read in this order: README.md → src/ top-level factories (look for AuthorizationServer / ResourceServer classes) → the GrantType/ directory → the Repository/ interfaces. The repository interfaces are the actual contract you implement; everything else is plumbing you assemble.
First use case — client_credentials. Skip authorization_code initially: no redirect handling, no consent screen, no PKCE. You can get a token out of POST /oauth/token in an hour and validate your wiring end-to-end.
// routes/api.php — deliberately NOT in the 'web' group
Route::post('/oauth/token', [TokenController::class, 'issue']);
// TokenController
public function issue(Request $request)
{
$psrRequest = (new Psr17Factory())->createServerRequest(
$request->method(), $request->fullUrl()
)->withParsedBody($request->post())->withQueryParams($request->query());
$psrResponse = $this->authorizationServer->respondToAccessTokenRequest(
$psrRequest, new Response()
);
return response($psrResponse->getBody()->getContents(), $psrResponse->getStatusCode())
->withHeaders($psrResponse->getHeaders());
}
The core checklist: implement ClientRepository, AccessTokenRepository, ScopeRepository against Eloquent; generate an RSA keypair; register ClientCredentialsGrant; hit the endpoint with curl -u client_id:client_secret -d grant_type=client_credentials.
Repository-as-adapter. Treat the package's repository interfaces as your seam. Implement them with Eloquent models (OauthClient, OauthAccessToken, OauthRefreshToken, OauthAuthCode, OauthScope) and keep all SQL in those classes. You'll write your own migrations — the package ships none. Bind the interfaces in a service provider so you can swap in fake implementations for tests.
Resource-server middleware. Wrap token validation in a Laravel middleware rather than validating per-controller:
class AuthenticateBearer
{
public function handle($request, Closure $next)
{
$psrRequest = $this->toPsr($request);
$result = $this->resourceServer->validateAuthenticatedRequest($psrRequest);
$request->attributes->set('oauth', $result->getAttributes());
return $next($request);
}
}
Apply to your api group, then resolve Auth::user() from the token's sub claim inside a custom guard or a small resolveUser() helper.
Grant selection by client type. authorization_code + PKCE for SPAs and mobile; client_credentials for machine-to-machine; avoid password — it's deprecated in the spec and locks you into collecting user passwords. Register only the grants you actually expose; each one you add is attack surface.
Bridge once, not everywhere. The Laravel↔PSR-7 impedance mismatch is the biggest friction. Install symfony/psr-http-message-bridge + nyholm/psr7, write two conversion functions (toPsr / toLaravel), and put them in a single trait or base controller. Do not scatter conversions across controllers.
Config as a first-class concern. Keep TTLs, key paths, and enabled grants in config/oauth.php — the package won't provide this. Read them once in a service provider, not inside grant handlers.
Testing. Test at the HTTP boundary with $this->postJson('/oauth/token', ...) — you're asserting the assembled server, not the library. For unit tests, hand-roll in-memory repositories (three small classes) rather than fighting the container.
Maturity signal is the headline risk. Zero stars and zero dependents means no community burn-in: no Stack Overflow corpus, no battle-tested edge cases around clock skew, token replay, or malformed headers. Pin an exact version, read the source, and seriously evaluate league/oauth2-server (the de facto PHP OAuth2 server, with far more production mileage) before committing. If you proceed, isolate it behind your repository interfaces so swapping later is a contained change.
Exceptions won't render as OAuth JSON. The package will throw OAuth2-specific exceptions that Laravel turns into HTML 500s. Register renderable callbacks in bootstrap/app.php (or Handler::render) mapping them to {"error": "...", "error_description": "..."} with the correct HTTP status. Without this, clients can't distinguish invalid_client from a server crash.
Middleware group matters. Session and CSRF middleware will break token endpoints. Put them in a stateless group that still runs throttle — rate limiting /oauth/token is mandatory, not optional.
Authorization header stripping. Apache/CGI and some Nginx fastcgi setups drop the Authorization header. If bearer tokens validate locally but fail in production, check $_SERVER['HTTP_AUTHORIZATION'] and add SetEnvIf Authorization / fastcgi_param HTTP_AUTHORIZATION before debugging your code.
Key storage and rotation. The private signing key must live outside the docroot with 0600 perms and must never be committed. Plan rotation now: publish a JWKS-style endpoint keyed by kid so tokens don't all die at once. Losing the key invalidates every issued token.
Redirect URI exactness. Match schemes, hosts, ports, trailing slashes, and query strings byte-for-byte. Wildcard matching is how open redirects happen.
Refresh token rotation. On grant, revoke the presented refresh token and issue a new one. Check the repository interface actually exposes revocation — if not, add a revoked_at column and enforce it in your implementation.
Clock discipline. Store all expiry timestamps in UTC (Laravel's default Carbon timezone may differ from date_default_timezone_get()). Allow ~30s skew when validating iat/exp.
Debugging. Log the grant type, client id, and error code on every rejection — never the token or secret. The response body's error field is your primary signal; invalid_grant almost always means a mismatched redirect URI, expired code, or reused PKCE verifier.
Extension points. Custom grants extend the base grant class; custom responses implement the response-type interface. Scope narrowing belongs in ScopeRepository::finalizeScopes() — that's the hook for role-based scope restriction.
How can I help you explore Laravel packages today?